Merge branch 'contrib/github_pr_17642_v6.0' into 'release/v6.0'

feat(sdmmc): support multi-block read/writes (GitHub PR) (v6.0)

See merge request espressif/esp-idf!46776
This commit is contained in:
Martin Vychodil
2026-03-24 19:10:58 +08:00
16 changed files with 328 additions and 40 deletions
@@ -48,6 +48,7 @@ extern "C" {
.input_delay_phase = SDMMC_DELAY_PHASE_0, \
.set_input_delay = &sdmmc_host_set_input_delay, \
.set_input_delayline = &sdmmc_host_set_input_delayline, \
.unaligned_multi_block_rw_max_chunk_size = 16, \
.dma_aligned_buffer = NULL, \
.pwr_ctrl_handle = NULL, \
.check_buffer_alignment = &sdmmc_host_check_buffer_alignment, \
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2023-2025 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2023-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
@@ -56,6 +56,23 @@ void sdmmc_test_rw_with_offset(sdmmc_card_t* card);
*/
void sdmmc_test_rw_highprio_task(sdmmc_card_t* card);
/**
* @brief Test multi-block read/write with unaligned buffers
*
* This function verifies that multi-block chunked transfers work correctly
* when the source/destination buffers are not DMA-aligned. It exercises:
* - Multi-block unaligned writes and reads with more blocks than the configured chunk size
* - The dma_aligned_buffer reuse path (when card->host.dma_aligned_buffer is pre-allocated)
*
* This test function works both with SDMMC and SDSPI hosts.
*
* @param card Pointer to the card object, must be initialized before calling this function.
* @param chunk_size Maximum number of blocks to transfer at once when using an
* unaligned bounce buffer. This value is written to
* card->host.unaligned_multi_block_rw_max_chunk_size before the test runs.
*/
void sdmmc_test_rw_unaligned_buffer_multiblock(sdmmc_card_t* card, size_t chunk_size);
#ifdef __cplusplus
};
#endif
@@ -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
*/
@@ -221,3 +221,77 @@ void sdmmc_test_rw_highprio_task(sdmmc_card_t* card)
vSemaphoreDelete(args.stop);
vSemaphoreDelete(args.done);
}
void sdmmc_test_rw_unaligned_buffer_multiblock(sdmmc_card_t* card, size_t chunk_size)
{
const size_t block_size = card->csd.sector_size;
/* Use 10 blocks so that with chunk_size=4,
* the transfer is split into chunks: 4 + 4 + 2 blocks */
const size_t block_count = 10;
const size_t buffer_size = block_size * block_count;
const size_t extra = 4;
const size_t total_alloc = buffer_size + extra;
/* Apply the chunk_size to the card's host config */
card->host.unaligned_multi_block_rw_max_chunk_size = chunk_size;
uint8_t *buffer = heap_caps_malloc(total_alloc, MALLOC_CAP_DMA);
TEST_ASSERT_NOT_NULL(buffer);
printf("Testing multi-block unaligned R/W: %d blocks, chunk_size=%d\n",
(int)block_count, (int)chunk_size);
/* Test A: Multi-block unaligned write, then unaligned read.
* The +1 offset makes the buffer unaligned, forcing the bounce-buffer
* chunking path in sdmmc_write_sectors / sdmmc_read_sectors. */
const uint32_t seed_a = 0x12345678;
fill_buffer(seed_a, buffer + 1, buffer_size / sizeof(uint32_t));
TEST_ESP_OK(sdmmc_write_sectors(card, buffer + 1, 0, block_count));
memset(buffer, 0xcc, total_alloc);
TEST_ESP_OK(sdmmc_read_sectors(card, buffer + 1, 0, block_count));
check_buffer(seed_a, buffer + 1, buffer_size / sizeof(uint32_t));
/* Test B: Aligned write, then unaligned read — verifies read chunking path. */
const uint32_t seed_b = 0xdeadbeef;
fill_buffer(seed_b, buffer, buffer_size / sizeof(uint32_t));
TEST_ESP_OK(sdmmc_write_sectors(card, buffer, 0, block_count));
memset(buffer, 0xcc, total_alloc);
TEST_ESP_OK(sdmmc_read_sectors(card, buffer + 1, 0, block_count));
check_buffer(seed_b, buffer + 1, buffer_size / sizeof(uint32_t));
/* Test C: Unaligned write, then aligned read — verifies write chunking path. */
const uint32_t seed_c = 0xcafebabe;
fill_buffer(seed_c, buffer + 1, buffer_size / sizeof(uint32_t));
TEST_ESP_OK(sdmmc_write_sectors(card, buffer + 1, 8, block_count));
memset(buffer, 0xcc, total_alloc);
TEST_ESP_OK(sdmmc_read_sectors(card, buffer, 8, block_count));
check_buffer(seed_c, buffer, buffer_size / sizeof(uint32_t));
/* Test D: dma_aligned_buffer reuse path.
* Pre-set card->host.dma_aligned_buffer so sdmmc_write/read_sectors
* uses it instead of allocating a temporary buffer. */
void *orig_dma_buf = card->host.dma_aligned_buffer;
size_t chunk_blocks = chunk_size;
if (chunk_blocks < block_count) {
/* Allocate a DMA-capable buffer large enough for chunk_blocks */
void *dma_buf = heap_caps_malloc(block_size * chunk_blocks, MALLOC_CAP_DMA);
TEST_ASSERT_NOT_NULL(dma_buf);
card->host.dma_aligned_buffer = dma_buf;
printf("Testing dma_aligned_buffer reuse path (%d block buffer)\n", (int)chunk_blocks);
const uint32_t seed_d = 0xfeedface;
fill_buffer(seed_d, buffer + 1, buffer_size / sizeof(uint32_t));
TEST_ESP_OK(sdmmc_write_sectors(card, buffer + 1, 0, block_count));
memset(buffer, 0xcc, total_alloc);
TEST_ESP_OK(sdmmc_read_sectors(card, buffer + 1, 0, block_count));
check_buffer(seed_d, buffer + 1, buffer_size / sizeof(uint32_t));
card->host.dma_aligned_buffer = orig_dma_buf;
free(dma_buf);
} else {
printf("Skipping dma_aligned_buffer reuse test (chunk_size >= block_count)\n");
}
free(buffer);
}
@@ -61,6 +61,7 @@ typedef int sdspi_dev_handle_t;
.input_delay_phase = SDMMC_DELAY_PHASE_0, \
.set_input_delay = NULL, \
.set_input_delayline = NULL, \
.unaligned_multi_block_rw_max_chunk_size = 16, \
.dma_aligned_buffer = NULL, \
.pwr_ctrl_handle = NULL, \
.check_buffer_alignment = sdspi_host_check_buffer_alignment, \
+22 -2
View File
@@ -3,7 +3,7 @@
*
* SPDX-License-Identifier: ISC
*
* SPDX-FileContributor: 2016-2025 Espressif Systems (Shanghai) CO LTD
* SPDX-FileContributor: 2016-2026 Espressif Systems (Shanghai) CO LTD
*/
/*
* Copyright (c) 2006 Uwe Stuehler <uwe@openbsd.org>
@@ -225,7 +225,27 @@ typedef struct {
sdmmc_delay_phase_t input_delay_phase; /*!< input delay phase, this will only take into effect when the host works in SDMMC_FREQ_HIGHSPEED or SDMMC_FREQ_52M. Driver will print out how long the delay is*/
esp_err_t (*set_input_delay)(int slot, sdmmc_delay_phase_t delay_phase); /*!< set input delay phase */
esp_err_t (*set_input_delayline)(int slot, sdmmc_delay_line_t delay_line); /*!< set input delay line */
void* dma_aligned_buffer; /*!< Leave it NULL. Reserved for cache aligned buffers for SDIO mode */
/**
* @brief Maximum number of blocks to read/write at once when using an unaligned buffer.
*
* When a multi-block read/write is requested with an unaligned buffer, the driver
* splits the transfer into chunks of this many blocks. Set to 0 to use the default
* value of 1 (single-block transfers). Higher values improve throughput but require
* a larger DMA-capable temporary buffer.
*/
size_t unaligned_multi_block_rw_max_chunk_size;
/**
* @brief Cache aligned buffer for multi-block RW and IO commands
*
* Use cases:
* - Temporary buffer for multi-block read/write transactions to/from unaligned buffers.
* Allocate with DMA capable memory, size should be an integer multiple of your card's sector size.
* The number of blocks transferred per chunk is controlled by
* `unaligned_multi_block_rw_max_chunk_size`.
* - Cache aligned buffer for IO commands in SDIO mode.
* If you allocate manually, make sure it is at least SDMMC_IO_BLOCK_SIZE bytes large.
*/
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 */
esp_err_t (*is_slot_set_to_uhs1)(int slot, bool *is_uhs1); /*!< host slot is set to uhs1 or not*/
+114 -34
View File
@@ -1,15 +1,48 @@
/*
* SPDX-FileCopyrightText: 2015-2025 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2015-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <inttypes.h>
#include "freertos/FreeRTOS.h"
#include <sys/param.h> // for MIN/MAX
#include "esp_private/sdmmc_common.h"
static const char* TAG = "sdmmc_cmd";
/**
* @brief Get the effective chunk size for unaligned multi-block transfers.
*
* Returns the configured value, or 1 (single-block) when the field is left
* at its zero-initialised default.
*/
static inline size_t get_chunk_size(const sdmmc_card_t *card)
{
size_t chunk_size = card->host.unaligned_multi_block_rw_max_chunk_size;
return (chunk_size != 0) ? chunk_size : 1;
}
static esp_err_t allocate_dma_buf(size_t* actual_size, size_t block_size, void **buf)
{
if (actual_size == NULL || buf == NULL) {
return ESP_ERR_INVALID_ARG;
}
size_t size = *actual_size;
do {
if (*actual_size < block_size) {
ESP_LOGE(TAG, "%s: not enough mem, err=0x%x", __func__, ESP_ERR_NO_MEM);
return ESP_ERR_NO_MEM;
}
*buf = heap_caps_malloc(*actual_size, MALLOC_CAP_DMA);
if (!*buf) {
*actual_size /= 2;
ESP_LOGD(TAG, "%s: required space for buffer of size %d not available, trying again with size %zu", __func__, size, *actual_size);
}
} while (!*buf);
return ESP_OK;
}
esp_err_t sdmmc_send_cmd(sdmmc_card_t* card, sdmmc_command_t* cmd)
{
@@ -463,31 +496,54 @@ esp_err_t sdmmc_write_sectors(sdmmc_card_t* card, const void* src,
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
// separate single block writes, if needed, and allocate a temporary
// separate (multi) block writes, if needed, and allocate a temporary
// DMA-capable buffer.
void *tmp_buf = NULL;
size_t actual_size = 0;
// We don't want to force the allocation into SPIRAM, the allocator
// will decide based on the buffer size and memory availability.
tmp_buf = heap_caps_malloc(block_size, MALLOC_CAP_DMA);
if (!tmp_buf) {
ESP_LOGE(TAG, "%s: not enough mem, err=0x%x", __func__, ESP_ERR_NO_MEM);
return ESP_ERR_NO_MEM;
size_t chunk_size = get_chunk_size(card);
size_t blocks_per_write = MIN(chunk_size, block_count);
// prefer using DMA aligned buffer if available over allocating local temporary buffer
bool use_dma_aligned_buffer = (card->host.dma_aligned_buffer != NULL);
void* buf = card->host.dma_aligned_buffer;
size_t actual_size = block_size * blocks_per_write;
if (!use_dma_aligned_buffer) {
// Allocate a temporary DMA-capable buffer.
// We don't want to force the allocation into SPIRAM, the allocator
// will decide based on the buffer size and memory availability.
// We start with the largest buffer possible to minimize the number of read iterations, but if that fails, we try smaller sizes down to a single block.
err = allocate_dma_buf(&actual_size, block_size, &buf);
if (err != ESP_OK) {
return err;
}
blocks_per_write = actual_size / block_size;
} else {
// Check that the provided dma_aligned_buffer is large enough
actual_size = heap_caps_get_allocated_size(buf);
blocks_per_write = actual_size / card->csd.sector_size;
size_t chunk_size = get_chunk_size(card);
blocks_per_write = MIN(chunk_size, blocks_per_write);
if (blocks_per_write == 0) {
ESP_LOGE(TAG, "%s: buffer smaller than sector size: buf=%d, sector=%d", __func__, actual_size, card->csd.sector_size);
return ESP_ERR_INVALID_SIZE;
}
}
actual_size = heap_caps_get_allocated_size(tmp_buf);
const uint8_t* cur_src = (const uint8_t*) src;
for (size_t i = 0; i < block_count; ++i) {
memcpy(tmp_buf, cur_src, block_size);
cur_src += block_size;
err = sdmmc_write_sectors_dma(card, tmp_buf, start_block + i, 1, actual_size);
for (size_t i = 0; i < block_count; i += blocks_per_write) {
// make sure not to write more than the remaining blocks, i.e. block_count - i
blocks_per_write = MIN(blocks_per_write, (block_count - i));
memcpy(buf, cur_src, block_size * blocks_per_write);
cur_src += block_size * blocks_per_write;
err = sdmmc_write_sectors_dma(card, buf, start_block + i, blocks_per_write, actual_size);
if (err != ESP_OK) {
ESP_LOGD(TAG, "%s: error 0x%x writing block %d+%d",
__func__, err, start_block, i);
ESP_LOGD(TAG, "%s: error 0x%x writing blocks %d+[%d..%d]",
__func__, err, start_block, i, i + blocks_per_write - 1);
break;
}
}
free(tmp_buf);
if (!use_dma_aligned_buffer) {
free(buf);
}
}
return err;
}
@@ -601,33 +657,57 @@ esp_err_t sdmmc_read_sectors(sdmmc_card_t* card, void* dst,
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
// separate single block reads, if needed, and allocate a temporary
// separate (multi) block reads, if needed, and allocate a temporary
// DMA-capable buffer.
void *tmp_buf = NULL;
size_t actual_size = 0;
tmp_buf = heap_caps_malloc(block_size, MALLOC_CAP_DMA);
if (!tmp_buf) {
ESP_LOGE(TAG, "%s: not enough mem, err=0x%x", __func__, ESP_ERR_NO_MEM);
return ESP_ERR_NO_MEM;
size_t chunk_size = get_chunk_size(card);
size_t blocks_per_read = MIN(chunk_size, block_count);
// prefer using DMA aligned buffer if available over allocating local temporary buffer
bool use_dma_aligned_buffer = (card->host.dma_aligned_buffer != NULL);
void* buf = card->host.dma_aligned_buffer;
size_t actual_size = block_size * blocks_per_read;
if (!use_dma_aligned_buffer) {
// Allocate a temporary DMA-capable buffer.
// We don't want to force the allocation into SPIRAM, the allocator
// will decide based on the buffer size and memory availability.
// We start with the largest buffer possible to minimize the number of read iterations, but if that fails, we try smaller sizes down to a single block.
err = allocate_dma_buf(&actual_size, block_size, &buf);
if (err != ESP_OK) {
return err;
}
blocks_per_read = actual_size / block_size;
} else {
// Check that the provided dma_aligned_buffer is large enough
actual_size = heap_caps_get_allocated_size(buf);
blocks_per_read = actual_size / card->csd.sector_size;
size_t chunk_size = get_chunk_size(card);
blocks_per_read = MIN(chunk_size, blocks_per_read);
if (blocks_per_read == 0) {
ESP_LOGE(TAG, "%s: buffer smaller than sector size: buf=%d, sector=%d", __func__, actual_size, card->csd.sector_size);
return ESP_ERR_INVALID_SIZE;
}
}
actual_size = heap_caps_get_allocated_size(tmp_buf);
uint8_t* cur_dst = (uint8_t*) dst;
for (size_t i = 0; i < block_count; ++i) {
err = sdmmc_read_sectors_dma(card, tmp_buf, start_block + i, 1, actual_size);
for (size_t i = 0; i < block_count; i += blocks_per_read) {
// make sure not to read more than the remaining blocks, i.e. block_count - i
blocks_per_read = MIN(blocks_per_read, (block_count - i));
err = sdmmc_read_sectors_dma(card, buf, start_block + i, blocks_per_read, actual_size);
if (err != ESP_OK) {
ESP_LOGD(TAG, "%s: error 0x%x writing block %d+%d",
__func__, err, start_block, i);
ESP_LOGE(TAG, "%s: error 0x%x reading blocks %d+[%d..%d]",
__func__, err, start_block, i, i + blocks_per_read - 1);
break;
}
memcpy(cur_dst, tmp_buf, block_size);
cur_dst += block_size;
memcpy(cur_dst, buf, block_size * blocks_per_read);
cur_dst += block_size * blocks_per_read;
}
if (!use_dma_aligned_buffer) {
free(buf);
}
free(tmp_buf);
}
return err;
}
esp_err_t sdmmc_read_sectors_dma(sdmmc_card_t* card, void* dst,
size_t start_block, size_t block_count, size_t buffer_len)
{
@@ -1,5 +1,5 @@
idf_component_register(SRCS "test_sdmmc_app.c"
PRIV_INCLUDE_DIRS "."
PRIV_REQUIRES esp_blockdev unity sdmmc esp_driver_sdmmc sdmmc_tests
PRIV_REQUIRES esp_blockdev unity sdmmc esp_driver_sdmmc sdmmc_tests common_test_flows
WHOLE_ARCHIVE
)
@@ -12,6 +12,7 @@
#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);
@@ -69,9 +70,22 @@ TEST(sdmmc, test_bdl_interface)
sdmmc_test_sd_end(&card);
}
TEST(sdmmc, test_multiblock_unaligned_rw)
{
sdmmc_card_t card;
int slot = 1;
int width = 1;
int freq_khz = SDMMC_FREQ_DEFAULT;
sdmmc_test_sd_skip_if_board_incompatible(slot, width, freq_khz, 0, 0);
sdmmc_test_sd_begin(slot, width, freq_khz, 0, &card);
sdmmc_test_rw_unaligned_buffer_multiblock(&card, 4);
sdmmc_test_sd_end(&card);
}
TEST_GROUP_RUNNER(sdmmc)
{
RUN_TEST_CASE(sdmmc, test_bdl_interface)
RUN_TEST_CASE(sdmmc, test_multiblock_unaligned_rw)
}
void app_main(void)
@@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
# SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
import pytest
from pytest_embedded import Dut
@@ -6,6 +6,7 @@ from pytest_embedded_idf.utils import idf_parametrize
@pytest.mark.sdcard
@idf_parametrize('config', ['default'], indirect=['config'])
@idf_parametrize('target', ['esp32'], indirect=['target'])
def test_sdmmc_extra(dut: Dut) -> None:
dut.expect_unity_test_output()