diff --git a/components/esp_driver_sdmmc/src/sd_host_sdmmc.c b/components/esp_driver_sdmmc/src/sd_host_sdmmc.c index f3b0fb4b21d..c39cd792fbc 100644 --- a/components/esp_driver_sdmmc/src/sd_host_sdmmc.c +++ b/components/esp_driver_sdmmc/src/sd_host_sdmmc.c @@ -536,6 +536,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; diff --git a/components/esp_driver_sdmmc/test_apps/sd_test_utils/components/common_test_flows/include/sdmmc_test_rw_common.h b/components/esp_driver_sdmmc/test_apps/sd_test_utils/components/common_test_flows/include/sdmmc_test_rw_common.h index d31c917b2e2..6061b821ea3 100644 --- a/components/esp_driver_sdmmc/test_apps/sd_test_utils/components/common_test_flows/include/sdmmc_test_rw_common.h +++ b/components/esp_driver_sdmmc/test_apps/sd_test_utils/components/common_test_flows/include/sdmmc_test_rw_common.h @@ -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 diff --git a/components/esp_driver_sdmmc/test_apps/sd_test_utils/components/common_test_flows/sdmmc_test_rw_common.c b/components/esp_driver_sdmmc/test_apps/sd_test_utils/components/common_test_flows/sdmmc_test_rw_common.c index ee28ce83e47..df331a6db97 100644 --- a/components/esp_driver_sdmmc/test_apps/sd_test_utils/components/common_test_flows/sdmmc_test_rw_common.c +++ b/components/esp_driver_sdmmc/test_apps/sd_test_utils/components/common_test_flows/sdmmc_test_rw_common.c @@ -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,96 @@ 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)); + + /* Test D: Write from a PSRAM buffer that is NOT cache/DMA aligned. + * + * The host (or the underlying SPI master) must transparently handle the + * unaligned PSRAM source - either by an internal aligned copy or by direct + * DMA when the hardware supports it. This exercises the fallback path of the + * SDSPI write transfer, which no longer copies the buffer itself but relies + * on the SPI master to accept PSRAM buffers (SPI_TRANS_DMA_USE_PSRAM). The + * data read back must still match. + * + * NOTE: this sub-test requires real SD-card + PSRAM hardware to be meaningful. + */ + { + const size_t misalign = 1; /* 1-byte offset breaks 4-byte/cache alignment */ + uint8_t *unaligned_psram = heap_caps_malloc(buffer_size + misalign, + MALLOC_CAP_SPIRAM | MALLOC_CAP_DMA); + TEST_ASSERT_NOT_NULL_MESSAGE(unaligned_psram, "Failed to allocate PSRAM buffer"); + uint8_t *src = unaligned_psram + misalign; + TEST_ASSERT_MESSAGE(esp_ptr_external_ram(src), "Buffer not in PSRAM"); + + const uint32_t seed_d = 0x13572468; + for (size_t i = 0; i < buffer_size; i++) { + src[i] = (uint8_t)(seed_d + i); + } + TEST_ESP_OK(sdmmc_write_sectors(card, src, 0, block_count)); + memset(internal_buf, 0xcc, buffer_size); + TEST_ESP_OK(sdmmc_read_sectors(card, internal_buf, 0, block_count)); + for (size_t i = 0; i < buffer_size; i++) { + TEST_ASSERT_EQUAL_HEX8((uint8_t)(seed_d + i), internal_buf[i]); + } + free(unaligned_psram); + } + + free(psram_buf); + free(internal_buf); + printf("PSRAM buffer R/W test passed\n"); +#endif // CONFIG_SPIRAM +} diff --git a/components/esp_driver_sdmmc/test_apps/sdmmc/components/sdmmc_tests/sdmmc_test_rw_sd.c b/components/esp_driver_sdmmc/test_apps/sdmmc/components/sdmmc_tests/sdmmc_test_rw_sd.c index 8c1a5c36a76..51cc9999f7f 100644 --- a/components/esp_driver_sdmmc/test_apps/sdmmc/components/sdmmc_tests/sdmmc_test_rw_sd.c +++ b/components/esp_driver_sdmmc/test_apps/sdmmc/components/sdmmc_tests/sdmmc_test_rw_sd.c @@ -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); +} diff --git a/components/esp_driver_sdspi/src/sdspi_host.c b/components/esp_driver_sdspi/src/sdspi_host.c index 99c2a0b5b5e..b8e914f3af6 100644 --- a/components/esp_driver_sdspi/src/sdspi_host.c +++ b/components/esp_driver_sdspi/src/sdspi_host.c @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2015-2025 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2015-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -20,7 +20,6 @@ #include "esp_timer.h" #include "freertos/FreeRTOS.h" #include "freertos/semphr.h" -#include "soc/soc_memory_layout.h" #include "esp_private/esp_cache_private.h" /// Max number of transactions in flight (used in start_command_write_blocks) @@ -931,22 +930,17 @@ static esp_err_t start_command_write_blocks(slot_info_t *slot, sdspi_hw_cmd_t *c // Prepare data to be sent size_t will_send = MIN(tx_length, SDSPI_MAX_DATA_LEN); - const uint8_t* tx_data = data; - if (!esp_ptr_in_dram(tx_data)) { - // If the pointer can't be used with DMA, copy data into a new buffer - uint8_t* tmp; - ret = get_block_buf(slot, &tmp); - if (ret != ESP_OK) { - return ret; - } - memcpy(tmp, tx_data, will_send); - tx_data = tmp; - } - // Write data + // Write data. The SPI master driver handles buffers that are not + // directly DMA-usable (e.g. PSRAM, or unaligned): with + // SPI_TRANS_DMA_USE_PSRAM it transfers a suitable PSRAM buffer directly + // (no copy), and otherwise falls back to an internal aligned buffer + // itself. This avoids the redundant copy the SDSPI driver used to do. + // The flag is ignored for buffers that are not in PSRAM. spi_transaction_t t_data = { .length = will_send * 8, - .tx_buffer = tx_data, + .tx_buffer = data, + .flags = SPI_TRANS_DMA_USE_PSRAM, }; ret = spi_device_transmit(slot->spi_handle, &t_data); if (ret != ESP_OK) { diff --git a/components/esp_driver_sdspi/test_apps/sdspi/components/sdspi_tests/sdmmc_test_rw_spi.c b/components/esp_driver_sdspi/test_apps/sdspi/components/sdspi_tests/sdmmc_test_rw_spi.c index 986ff591ad1..954f34bc245 100644 --- a/components/esp_driver_sdspi/test_apps/sdspi/components/sdspi_tests/sdmmc_test_rw_spi.c +++ b/components/esp_driver_sdspi/test_apps/sdspi/components/sdspi_tests/sdmmc_test_rw_spi.c @@ -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); +} diff --git a/components/sdmmc/include/sd_protocol_types.h b/components/sdmmc/include/sd_protocol_types.h index 4de7229d176..07e77f17b66 100644 --- a/components/sdmmc/include/sd_protocol_types.h +++ b/components/sdmmc/include/sd_protocol_types.h @@ -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; diff --git a/components/sdmmc/sdmmc_cmd.c b/components/sdmmc/sdmmc_cmd.c index eb9622521df..881d515902a 100644 --- a/components/sdmmc/sdmmc_cmd.c +++ b/components/sdmmc/sdmmc_cmd.c @@ -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); diff --git a/components/sdmmc/test_apps/main/test_sdmmc_app.c b/components/sdmmc/test_apps/main/test_sdmmc_app.c index 1fe319e4e7a..3b45429dd88 100644 --- a/components/sdmmc/test_apps/main/test_sdmmc_app.c +++ b/components/sdmmc/test_apps/main/test_sdmmc_app.c @@ -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,16 @@ #include #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" -TEST_GROUP(sdmmc); +#define TEST_MEMORY_LEAK_THRESHOLD (200) -TEST_SETUP(sdmmc) -{ -} - -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 +63,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 +75,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(); } diff --git a/components/sdmmc/test_apps/pytest_sdmmc_extra.py b/components/sdmmc/test_apps/pytest_sdmmc_extra.py index 51c186ee2dc..a067d5bf7bc 100644 --- a/components/sdmmc/test_apps/pytest_sdmmc_extra.py +++ b/components/sdmmc/test_apps/pytest_sdmmc_extra.py @@ -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()