Merge branch 'fix/sdmmc_bdl_casting_v6.0' into 'release/v6.0'

fix(sdmmc): BDL calculate sectors cast fix + Add a better handling of devices accessing PSRAM through DMA (v6.0)

See merge request espressif/esp-idf!51044
This commit is contained in:
Martin Vychodil
2026-07-23 20:51:14 +08:00
11 changed files with 245 additions and 48 deletions

View File

@@ -460,6 +460,16 @@ bool sd_host_check_buffer_alignment(sd_host_sdmmc_slot_t *slot, const void *buf,
return false;
}
#if !SOC_SDMMC_PSRAM_DMA_CAPABLE
// The SDMMC peripheral's DMA cannot reach PSRAM on this target, so a PSRAM
// buffer can never be used directly regardless of its alignment. Reporting
// it as not directly usable makes the protocol layer fall back to an
// internal DMA-capable buffer.
if (esp_ptr_external_ram(buf)) {
return false;
}
#endif
esp_err_t ret = ESP_FAIL;
int cache_flags = 0;
size_t cache_alignment_bytes = 0;

View File

@@ -73,6 +73,31 @@ void sdmmc_test_rw_highprio_task(sdmmc_card_t* card);
*/
void sdmmc_test_rw_unaligned_buffer_multiblock(sdmmc_card_t* card, size_t chunk_size);
/**
* @brief Test read/write with PSRAM-allocated buffers
*
* This function verifies that the driver correctly handles buffers allocated in PSRAM.
* When the host reports the PSRAM buffer as directly usable (via check_buffer_alignment),
* data is transferred directly without intermediate copying. Otherwise, the driver uses
* double-buffering through internal RAM.
*
* The test covers:
* - Writing from PSRAM, reading to internal RAM
* - Writing from internal RAM, reading to PSRAM
* - Both writing and reading from PSRAM
*
* If PSRAM is not enabled (CONFIG_SPIRAM), this function is a no-op. Callers
* must skip the test (TEST_IGNORE) BEFORE initializing the slot/controller, so
* that this function is not relied upon to abort the test. Doing the skip here
* would longjmp out of the test before the caller's cleanup runs and leak the
* SD slot.
*
* This test function works both with SDMMC and SDSPI hosts.
*
* @param card Pointer to the card object, must be initialized before calling this function.
*/
void sdmmc_test_rw_psram_buffer(sdmmc_card_t *card);
#ifdef __cplusplus
};
#endif

View File

@@ -14,6 +14,7 @@
#include "test_utils.h"
#include "sdkconfig.h"
#include "soc/soc_caps.h"
#include "esp_memory_utils.h"
#include "unity.h"
#include "sd_protocol_defs.h"
#include "sdmmc_cmd.h"
@@ -295,3 +296,64 @@ void sdmmc_test_rw_unaligned_buffer_multiblock(sdmmc_card_t* card, size_t chunk_
free(buffer);
}
void sdmmc_test_rw_psram_buffer(sdmmc_card_t *card)
{
#if !CONFIG_SPIRAM
/* The PSRAM-disabled skip is intentionally NOT done here via TEST_IGNORE.
* This function runs after the slot/controller has been initialized, and
* TEST_IGNORE would longjmp out before the caller's *_end() cleanup runs,
* leaking the slot. Callers must skip before initializing the hardware.
*/
(void)card;
#else
const size_t block_size = card->csd.sector_size;
const size_t block_count = 8;
const size_t buffer_size = block_size * block_count;
/* Allocate buffer in PSRAM */
uint8_t *psram_buf = heap_caps_malloc(buffer_size, MALLOC_CAP_SPIRAM | MALLOC_CAP_DMA | MALLOC_CAP_CACHE_ALIGNED);
TEST_ASSERT_NOT_NULL_MESSAGE(psram_buf, "Failed to allocate PSRAM buffer; is PSRAM enabled?");
TEST_ASSERT_MESSAGE(esp_ptr_external_ram(psram_buf), "Buffer not in PSRAM");
/* Also allocate a reference buffer in internal RAM for comparison */
uint8_t *internal_buf = heap_caps_malloc(buffer_size, MALLOC_CAP_DMA | MALLOC_CAP_CACHE_ALIGNED);
TEST_ASSERT_NOT_NULL(internal_buf);
/* A PSRAM buffer can be transferred directly only if the host reports it as
* usable; otherwise the protocol layer transparently bounce-buffers it. This
* test verifies correctness for both cases. */
bool host_can_use_psram_directly =
card->host.check_buffer_alignment(card->host.slot, psram_buf, buffer_size);
printf("Testing PSRAM buffer R/W: %d blocks, host can use PSRAM directly=%s\n",
(int)block_count, host_can_use_psram_directly ? "true" : "false");
/* Test A: Write from PSRAM buffer, read back to internal buffer, verify */
const uint32_t seed_a = 0xABCD1234;
fill_buffer(seed_a, psram_buf, buffer_size / sizeof(uint32_t));
TEST_ESP_OK(sdmmc_write_sectors(card, psram_buf, 0, block_count));
memset(internal_buf, 0xcc, buffer_size);
TEST_ESP_OK(sdmmc_read_sectors(card, internal_buf, 0, block_count));
check_buffer(seed_a, internal_buf, buffer_size / sizeof(uint32_t));
/* Test B: Write from internal buffer, read back to PSRAM buffer, verify */
const uint32_t seed_b = 0x5678EFAB;
fill_buffer(seed_b, internal_buf, buffer_size / sizeof(uint32_t));
TEST_ESP_OK(sdmmc_write_sectors(card, internal_buf, 0, block_count));
memset(psram_buf, 0xcc, buffer_size);
TEST_ESP_OK(sdmmc_read_sectors(card, psram_buf, 0, block_count));
check_buffer(seed_b, psram_buf, buffer_size / sizeof(uint32_t));
/* Test C: Both write and read from PSRAM buffer */
const uint32_t seed_c = 0xDEAD9876;
fill_buffer(seed_c, psram_buf, buffer_size / sizeof(uint32_t));
TEST_ESP_OK(sdmmc_write_sectors(card, psram_buf, 0, block_count));
memset(psram_buf, 0xcc, buffer_size);
TEST_ESP_OK(sdmmc_read_sectors(card, psram_buf, 0, block_count));
check_buffer(seed_c, psram_buf, buffer_size / sizeof(uint32_t));
free(psram_buf);
free(internal_buf);
printf("PSRAM buffer R/W test passed\n");
#endif // CONFIG_SPIRAM
}

View File

@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2022-2025 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2022-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
@@ -128,3 +128,26 @@ TEST_CASE("sdmmc read/write with concurrent high-prio task, slot 1, 4-bit", "[sd
{
do_one_sdmmc_rw_test_highprio_task(1, 4);
}
static void do_one_sdmmc_rw_test_psram_dma_buffer(int slot, int width)
{
sdmmc_card_t card;
int freq_khz = SDMMC_FREQ_HIGHSPEED;
#if !CONFIG_SPIRAM
TEST_IGNORE_MESSAGE("PSRAM is not enabled");
#endif
sdmmc_test_sd_skip_if_board_incompatible(slot, width, freq_khz, NO_DDR, NO_EMMC);
sdmmc_test_sd_begin(slot, width, freq_khz, 0, &card);
sdmmc_test_rw_psram_buffer(&card);
sdmmc_test_sd_end(&card);
}
TEST_CASE("sdmmc read/write using PSRAM DMA accessible buffer, slot 0, 4-bit", "[sdmmc]")
{
do_one_sdmmc_rw_test_psram_dma_buffer(0, 4);
}
TEST_CASE("sdmmc read/write using PSRAM DMA accessible buffer, slot 1, 4-bit", "[sdmmc]")
{
do_one_sdmmc_rw_test_psram_dma_buffer(1, 4);
}

View File

@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2022-2025 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2022-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
@@ -107,3 +107,30 @@ TEST_CASE("sdspi read/write performance - wait_for_miso == -1, slot 1", "[sdspi]
//here freq should be changed to SDMMC_FREQ_HIGHSPEED after fixing IDF-8749
do_one_sdspi_perf_test_dont_wait_for_miso(SLOT_1, SDMMC_FREQ_DEFAULT);
}
/* ========== Read/write PSRAM DMA test, SPI ========== */
static void do_one_sdspi_psram_dma_test(int slot, int freq_khz)
{
sdmmc_card_t card;
#if !CONFIG_SPIRAM
TEST_IGNORE_MESSAGE("PSRAM is not enabled");
#endif
sdmmc_test_spi_skip_if_board_incompatible(slot, freq_khz);
sdmmc_test_spi_begin(slot, freq_khz, &card, NULL, NULL, NULL);
sdmmc_card_print_info(stdout, &card);
sdmmc_test_rw_psram_buffer(&card);
sdmmc_test_spi_end(slot, &card);
}
TEST_CASE("sdspi read/write psram dma, slot 0", "[sdspi]")
{
do_one_sdspi_psram_dma_test(SLOT_0, SDMMC_FREQ_HIGHSPEED);
}
TEST_CASE("sdspi read/write psram dma, slot 1", "[sdspi]")
{
//TODO: IDF-8749
//here freq should be changed to SDMMC_FREQ_HIGHSPEED after fixing IDF-8749
do_one_sdspi_psram_dma_test(SLOT_1, SDMMC_FREQ_DEFAULT);
}

View File

@@ -0,0 +1,22 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <stddef.h>
#include <stdint.h>
#include "esp_err.h"
#ifdef __cplusplus
extern "C" {
#endif
esp_err_t sdmmc_blockdev_calculate_sectors(size_t sector_size, uint64_t addr, size_t data_len,
size_t *out_start_sector_num, size_t *out_num_of_sectors);
#ifdef __cplusplus
}
#endif

View File

@@ -247,7 +247,7 @@ typedef struct {
*/
void* dma_aligned_buffer;
sd_pwr_ctrl_handle_t pwr_ctrl_handle; /*!< Power control handle */
bool (*check_buffer_alignment)(int slot, const void *buf, size_t size); /*!< Check if buffer meets alignment requirements */
bool (*check_buffer_alignment)(int slot, const void *buf, size_t size); /*!< Check if the host can use the buffer directly for a transfer (accounts for alignment and any hardware-specific DMA reachability constraints) */
esp_err_t (*is_slot_set_to_uhs1)(int slot, bool *is_uhs1); /*!< host slot is set to uhs1 or not*/
} sdmmc_host_t;

View File

@@ -1,34 +1,37 @@
/*
* SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <inttypes.h>
#include "esp_private/sdmmc_common.h"
#include "esp_private/sdmmc_blockdev.h"
#include "esp_blockdev.h"
#include "sdmmc_cmd.h"
static esp_err_t calculate_start_sector_num_and_sector_count(size_t sector_size, uint64_t addr, size_t data_len, size_t* out_start_sector_num, size_t* out_num_of_sectors)
esp_err_t sdmmc_blockdev_calculate_sectors(size_t sector_size, uint64_t addr, size_t data_len,
size_t *out_start_sector_num, size_t *out_num_of_sectors)
{
size_t offset_in_start_sector = (size_t) addr % sector_size;
size_t offset_in_end_sector = (size_t) data_len % sector_size;
// Has to be aligned to sector boundaries
if (offset_in_start_sector != 0 || offset_in_end_sector != 0) {
if (sector_size == 0 || addr % sector_size != 0 || data_len % sector_size != 0) {
return ESP_ERR_INVALID_ARG;
}
if (data_len > UINT64_MAX - addr) {
return ESP_ERR_INVALID_SIZE;
}
size_t start_sector_num = (size_t) addr / sector_size;
size_t last_byte_addr = (size_t) (addr + data_len - 1); // Address of the last accessed byte
size_t end_sector_num = last_byte_addr / sector_size;
uint64_t start_sector_num = addr / sector_size;
size_t num_of_sectors = data_len / sector_size;
if (start_sector_num > SIZE_MAX || num_of_sectors > SIZE_MAX - start_sector_num) {
return ESP_ERR_INVALID_SIZE;
}
if (out_start_sector_num) {
*out_start_sector_num = start_sector_num;
*out_start_sector_num = (size_t) start_sector_num;
}
if (out_num_of_sectors) {
*out_num_of_sectors = end_sector_num - start_sector_num + 1;
*out_num_of_sectors = num_of_sectors;
}
return ESP_OK;
@@ -41,7 +44,7 @@ static esp_err_t sdmmc_blockdev_read(esp_blockdev_handle_t handle, uint8_t* dst_
}
sdmmc_card_t* card = (sdmmc_card_t*) handle->ctx;
size_t start_sector_num, num_of_sectors;
esp_err_t err = calculate_start_sector_num_and_sector_count((size_t) card->csd.sector_size, src_addr, data_read_len, &start_sector_num, &num_of_sectors);
esp_err_t err = sdmmc_blockdev_calculate_sectors((size_t) card->csd.sector_size, src_addr, data_read_len, &start_sector_num, &num_of_sectors);
if (err != ESP_OK) {
return err;
}
@@ -56,7 +59,7 @@ static esp_err_t sdmmc_blockdev_write(esp_blockdev_handle_t handle, const uint8_
}
sdmmc_card_t* card = (sdmmc_card_t*) handle->ctx;
size_t start_sector_num, num_of_sectors;
esp_err_t err = calculate_start_sector_num_and_sector_count((size_t) card->csd.sector_size, dst_addr, data_write_len, &start_sector_num, &num_of_sectors);
esp_err_t err = sdmmc_blockdev_calculate_sectors((size_t) card->csd.sector_size, dst_addr, data_write_len, &start_sector_num, &num_of_sectors);
if (err != ESP_OK) {
return err;
}
@@ -71,7 +74,7 @@ static esp_err_t sdmmc_blockdev_erase(esp_blockdev_handle_t handle, uint64_t sta
}
sdmmc_card_t* card = (sdmmc_card_t*) handle->ctx;
size_t start_sector_num, num_of_sectors;
esp_err_t err = calculate_start_sector_num_and_sector_count((size_t) card->csd.sector_size, start_addr, erase_len, &start_sector_num, &num_of_sectors);
esp_err_t err = sdmmc_blockdev_calculate_sectors((size_t) card->csd.sector_size, start_addr, erase_len, &start_sector_num, &num_of_sectors);
if (err != ESP_OK) {
return err;
}

View File

@@ -23,6 +23,20 @@ static inline size_t get_chunk_size(const sdmmc_card_t *card)
return (chunk_size != 0) ? chunk_size : 1;
}
/**
* @brief Whether the host can transfer the user buffer directly, without an
* intermediate DMA-capable buffer.
*
* The protocol layer does not need to reason about DMA or PSRAM: it simply asks
* the host driver whether the buffer is usable as-is. The driver accounts for
* alignment and any hardware-specific reachability constraints (e.g. whether the
* peripheral's DMA can reach PSRAM).
*/
static inline bool sdmmc_buffer_directly_usable(const sdmmc_card_t *card, const void *buf, size_t size)
{
return card->host.check_buffer_alignment(card->host.slot, buf, size);
}
static esp_err_t allocate_dma_buf(size_t* actual_size, size_t block_size, void **buf)
{
if (actual_size == NULL || buf == NULL) {
@@ -486,16 +500,10 @@ esp_err_t sdmmc_write_sectors(sdmmc_card_t* card, const void* src,
esp_err_t err = ESP_OK;
size_t block_size = card->csd.sector_size;
bool is_aligned = card->host.check_buffer_alignment(card->host.slot, src, block_size * block_count);
if (is_aligned
#if !SOC_SDMMC_PSRAM_DMA_CAPABLE
&& !esp_ptr_external_ram(src)
#endif
) {
if (sdmmc_buffer_directly_usable(card, src, block_size * block_count)) {
err = sdmmc_write_sectors_dma(card, src, start_block, block_count, block_size * block_count);
} else {
// SDMMC peripheral needs DMA-capable buffers. Split the write into
// The host cannot transfer this buffer directly. Split the write into
// separate (multi) block writes, if needed, and allocate a temporary
// DMA-capable buffer.
size_t chunk_size = get_chunk_size(card);
@@ -649,16 +657,10 @@ esp_err_t sdmmc_read_sectors(sdmmc_card_t* card, void* dst,
esp_err_t err = ESP_OK;
size_t block_size = card->csd.sector_size;
bool is_aligned = card->host.check_buffer_alignment(card->host.slot, dst, block_size * block_count);
if (is_aligned
#if !SOC_SDMMC_PSRAM_DMA_CAPABLE
&& !esp_ptr_external_ram(dst)
#endif
) {
if (sdmmc_buffer_directly_usable(card, dst, block_size * block_count)) {
err = sdmmc_read_sectors_dma(card, dst, start_block, block_count, block_size * block_count);
} else {
// SDMMC peripheral needs DMA-capable buffers. Split the read into
// The host cannot transfer this buffer directly. Split the read into
// separate (multi) block reads, if needed, and allocate a temporary
// DMA-capable buffer.
size_t chunk_size = get_chunk_size(card);

View File

@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
@@ -9,23 +9,42 @@
#include <inttypes.h>
#include "unity.h"
#include "unity_fixture.h"
#include "unity_test_utils.h"
#include "sd_protocol_defs.h"
#include "sdmmc_cmd.h"
#include "sdmmc_test_begin_end_sd.h"
#include "sdmmc_test_rw_common.h"
#include "esp_blockdev.h"
#include "esp_private/sdmmc_blockdev.h"
TEST_GROUP(sdmmc);
#define TEST_MEMORY_LEAK_THRESHOLD (200)
TEST_SETUP(sdmmc)
TEST_CASE("sdmmc blockdev converts byte ranges to sectors", "[sdmmc]")
{
size_t start_sector;
size_t sector_count;
TEST_ESP_OK(sdmmc_blockdev_calculate_sectors(512, UINT64_C(0x100000000), 1024,
&start_sector, &sector_count));
TEST_ASSERT_EQUAL_UINT32(8388608, start_sector);
TEST_ASSERT_EQUAL_UINT32(2, sector_count);
TEST_ESP_OK(sdmmc_blockdev_calculate_sectors(512, UINT64_C(0x100000000), 0,
&start_sector, &sector_count));
TEST_ASSERT_EQUAL_UINT32(8388608, start_sector);
TEST_ASSERT_EQUAL_UINT32(0, sector_count);
TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG,
sdmmc_blockdev_calculate_sectors(512, 1, 512, NULL, NULL));
TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG,
sdmmc_blockdev_calculate_sectors(512, 0, 1, NULL, NULL));
TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG,
sdmmc_blockdev_calculate_sectors(0, 0, 512, NULL, NULL));
TEST_ASSERT_EQUAL(ESP_ERR_INVALID_SIZE,
sdmmc_blockdev_calculate_sectors(512, UINT64_MAX - 511, 512, NULL, NULL));
}
TEST_TEAR_DOWN(sdmmc)
{
}
TEST(sdmmc, test_bdl_interface)
TEST_CASE("sdmmc extra, bdl interface test", "[sdcard]")
{
sdmmc_card_t card;
int slot = 1;
@@ -70,7 +89,7 @@ TEST(sdmmc, test_bdl_interface)
sdmmc_test_sd_end(&card);
}
TEST(sdmmc, test_multiblock_unaligned_rw)
TEST_CASE("sdmmc extra, multiblock unaligned rw test", "[sdcard]")
{
sdmmc_card_t card;
int slot = 1;
@@ -82,13 +101,17 @@ TEST(sdmmc, test_multiblock_unaligned_rw)
sdmmc_test_sd_end(&card);
}
TEST_GROUP_RUNNER(sdmmc)
void setUp(void)
{
RUN_TEST_CASE(sdmmc, test_bdl_interface)
RUN_TEST_CASE(sdmmc, test_multiblock_unaligned_rw)
unity_utils_record_free_mem();
}
void tearDown(void)
{
unity_utils_evaluate_leaks_direct(TEST_MEMORY_LEAK_THRESHOLD);
}
void app_main(void)
{
UNITY_MAIN(sdmmc);
unity_run_menu();
}

View File

@@ -10,4 +10,4 @@ from pytest_embedded_idf.utils import idf_parametrize
@idf_parametrize('config', ['default'], indirect=['config'])
@idf_parametrize('target', ['esp32'], indirect=['target'])
def test_sdmmc_extra(dut: Dut) -> None:
dut.expect_unity_test_output()
dut.run_all_single_board_cases()