fix(sdmmc): back off between CMD13 polls while waiting for card to be ready

The loops waiting for the card to leave its busy state started their yield
backoff at 100 ms. A card is typically busy for a few milliseconds after a
write, so the backoff never fired and every write was followed by hundreds
of back-to-back CMD13 commands. Occupying the host controller like this
slows down unrelated work on both cores, not just the calling task.

Delay between polls instead, starting at CONFIG_SD_READY_POLL_PERIOD_START_US
(100 us) and doubling. Both the delay and the configured start period are
capped at one FreeRTOS tick period, where vTaskDelay() already yields and one
command per tick is not a storm. A typical wait now costs a handful of
commands instead of hundreds.

Applies to sdmmc_wait_for_idle(), sdmmc_init_sd_wait_data_ready() and
read_tuning_block().

Closes https://github.com/espressif/esp-idf/issues/19034
This commit is contained in:
Adam Múdry
2026-09-02 15:39:59 +02:00
parent dce5812654
commit 568ae4126b
4 changed files with 98 additions and 15 deletions

View File

@@ -5,4 +5,39 @@ menu "SD Protocol Layer Configuration"
help
Enable SDIO support.
Disabling this will skip SDIO-specific initialization steps
config SD_READY_POLL_PERIOD_START_US
int "Initial delay between card status polls (us)"
default 100
range 1 10000
help
While waiting for the card to leave its busy state (for example after a write),
the driver polls it with CMD13 (SEND_STATUS). This is the delay before the
first re-poll; the delay then doubles after every poll, up to one FreeRTOS
tick period.
Polling back-to-back without a delay turns every write into a storm of
hundreds of CMD13 commands, which occupies the host controller and slows down
unrelated work on both cores.
The exponential backoff means a card that becomes ready after 1.5 ms is
detected at about 1.5 ms rather than at the next FreeRTOS tick, using a
handful of commands instead of hundreds. Once the delay reaches one tick
period it stops growing, so a long busy period costs one poll per tick and
is detected within one tick of the card becoming ready.
Lower this value to detect very short busy periods sooner, at the cost of
more commands on the bus. Values larger than one FreeRTOS tick period are
clamped to it, so the largest useful setting depends on CONFIG_FREERTOS_HZ.
Delays shorter than one FreeRTOS tick period are spent busy-waiting, since
there is no shorter blocking sleep available; a delay of one tick blocks on
vTaskDelay() and lets other tasks run.
The busy-waited part of the ramp does not yield, so lower-priority tasks on
that core are held off for its duration. It lasts at most until the backoff
reaches one tick: with the default 100 us start, about 1.5 ms at a 1 kHz
tick (100+200+400+800 us) and about 12.7 ms at a 100 Hz tick. A card that
becomes ready sooner ends the wait sooner. No CMD13 is issued during the
delays, so the host controller and the bus stay idle throughout.
endmenu

View File

@@ -17,6 +17,7 @@
#pragma once
#include <string.h>
#include "sdkconfig.h"
#include "esp_log.h"
#include "esp_check.h"
#include "esp_heap_caps.h"
@@ -38,6 +39,27 @@ extern "C" {
#define SDMMC_INIT_WAIT_DATA_READY_TIMEOUT_US (5000 * 1000)
#define SDMMC_READY_FOR_DATA_TIMEOUT_US (5000 * 1000)
/* Delay between two consecutive card status (CMD13) polls while waiting for the
* card to become ready. Starts at SDMMC_READY_POLL_PERIOD_START_US and doubles
* after every poll, up to one FreeRTOS tick period.
*
* Polling without a delay turns every write into a storm of hundreds of CMD13
* commands, which occupies the host controller and slows down unrelated work.
*
* While the delay is below one tick period it is busy-waited and doubles, so a
* handful of commands covers a busy period of a few milliseconds and a card that
* becomes ready early is noticed without waiting for the next tick.
*
* Once the delay reaches one tick period it stops growing: from there on
* vTaskDelay() already yields and one command per tick is not a storm, while a
* longer delay would only delay noticing that the card is ready. Long busy
* periods therefore cost one poll per tick and are detected within one tick.
*
* One tick period is a hard cap: a configured start period larger than that is
* clamped to it as well.
*/
#define SDMMC_READY_POLL_PERIOD_START_US CONFIG_SD_READY_POLL_PERIOD_START_US
/* These delay values are mostly useful for cases when CD pin is not used, and
* the card is removed. In this case, SDMMC peripheral may not always return
* CMD_DONE / DATA_DONE interrupts after signaling the error. These timeouts work
@@ -184,6 +206,19 @@ static inline bool sdmmc_ready_for_data(uint32_t status)
return (status & MMC_R1_READY_FOR_DATA) && (MMC_R1_CURRENT_STATE_STATUS(status) == MMC_R1_CURRENT_STATE_TRAN);
}
/**
* @brief Wait before the next card status poll, and back off for the poll after that
*
* Delays for *period_us, then doubles *period_us. Both the delay and the stored
* period are capped at one FreeRTOS tick period, so a *period_us larger than that
* on entry is clamped rather than used as-is. A delay shorter than one tick period
* is busy-waited, since no shorter blocking sleep is available; one tick period
* blocks on vTaskDelay() so that other tasks can run.
*
* @param[in,out] period_us Delay to apply now; updated to the delay to apply next time
*/
void sdmmc_poll_delay_and_backoff(uint32_t* period_us);
void sdmmc_flip_byte_order(uint32_t* response, size_t size);
esp_err_t sdmmc_fix_host_flags(sdmmc_card_t* card);

View File

@@ -18,6 +18,7 @@
#include <inttypes.h>
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "esp_rom_sys.h"
#include "esp_timer.h"
#include "esp_private/sdmmc_common.h"
@@ -441,12 +442,33 @@ uint32_t sdmmc_get_erase_timeout_ms(const sdmmc_card_t* card, int arg, size_t er
}
}
void sdmmc_poll_delay_and_backoff(uint32_t* period_us)
{
const uint32_t us_per_tick = portTICK_PERIOD_MS * 1000;
/* Clamp on entry as well: a configured start period longer than a tick would
* otherwise be used as-is and never brought back down to the cap. */
uint32_t delay_us = MIN(*period_us, us_per_tick);
if (delay_us < us_per_tick) {
/* No blocking sleep with sub-tick resolution is available, busy-wait instead.
* The point of the delay is to keep CMD13 off the bus, which this still does. */
esp_rom_delay_us(delay_us);
} else {
vTaskDelay(1);
}
/* Stop growing once the delay reaches one tick period. At that point vTaskDelay()
* already yields and one command per tick is not a storm, so a longer delay would
* only add overshoot to the time the card is detected as ready. */
*period_us = MIN(delay_us * 2, us_per_tick);
}
esp_err_t sdmmc_wait_for_idle(sdmmc_card_t* card, uint32_t status)
{
assert(!host_is_spi(card));
esp_err_t err = ESP_OK;
size_t count = 0;
int64_t yield_delay_us = 100 * 1000; // initially 100ms
uint32_t poll_period_us = SDMMC_READY_POLL_PERIOD_START_US;
int64_t t0 = esp_timer_get_time();
int64_t t1 = 0;
/* SD mode: wait for the card to become idle based on R1 status */
@@ -455,10 +477,7 @@ esp_err_t sdmmc_wait_for_idle(sdmmc_card_t* card, uint32_t status)
if (t1 - t0 > SDMMC_READY_FOR_DATA_TIMEOUT_US) {
return ESP_ERR_TIMEOUT;
}
if (t1 - t0 > yield_delay_us) {
yield_delay_us *= 2;
vTaskDelay(1);
}
sdmmc_poll_delay_and_backoff(&poll_period_us);
err = sdmmc_send_cmd_send_status(card, &status);
if (err != ESP_OK) {
ESP_LOGE(TAG, "%s: sdmmc_send_cmd_send_status returned 0x%x", __func__, err);

View File

@@ -154,7 +154,7 @@ esp_err_t sdmmc_init_sd_wait_data_ready(sdmmc_card_t* card)
/* Wait for the card to be ready for data transfers */
uint32_t status = 0;
uint32_t count = 0;
int64_t yield_delay_us = 100 * 1000; // initially 100ms
uint32_t poll_period_us = SDMMC_READY_POLL_PERIOD_START_US;
int64_t t0 = esp_timer_get_time();
int64_t t1 = 0;
while (!host_is_spi(card) && !(status & MMC_R1_READY_FOR_DATA)) {
@@ -163,10 +163,7 @@ esp_err_t sdmmc_init_sd_wait_data_ready(sdmmc_card_t* card)
ESP_LOGE(TAG, "init wait data ready - timeout");
return ESP_ERR_TIMEOUT;
}
if (t1 - t0 > yield_delay_us) {
yield_delay_us *= 2;
vTaskDelay(1);
}
sdmmc_poll_delay_and_backoff(&poll_period_us);
esp_err_t err = sdmmc_send_cmd_send_status(card, &status);
if (err != ESP_OK) {
return err;
@@ -400,7 +397,7 @@ static esp_err_t read_tuning_block(sdmmc_card_t *card)
uint32_t status = 0;
size_t count = 0;
int64_t yield_delay_us = 100 * 1000; // initially 100ms
uint32_t poll_period_us = SDMMC_READY_POLL_PERIOD_START_US;
int64_t t0 = esp_timer_get_time();
int64_t t1 = 0;
while (!host_is_spi(card) && !(status & MMC_R1_READY_FOR_DATA)) {
@@ -409,10 +406,7 @@ static esp_err_t read_tuning_block(sdmmc_card_t *card)
ESP_LOGW(TAG, "read sectors dma - timeout");
return ESP_ERR_TIMEOUT;
}
if (t1 - t0 > yield_delay_us) {
yield_delay_us *= 2;
vTaskDelay(1);
}
sdmmc_poll_delay_and_backoff(&poll_period_us);
ret = sdmmc_send_cmd_send_status(card, &status);
if (ret != ESP_OK) {
ESP_LOGW(TAG, "%s: sdmmc_send_cmd_send_status returned 0x%x", __func__, ret);