mirror of
https://github.com/espressif/esp-idf.git
synced 2026-09-22 13:01:16 +03:00
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:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -150,7 +150,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)) {
|
||||
@@ -159,10 +159,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;
|
||||
@@ -396,7 +393,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)) {
|
||||
@@ -405,10 +402,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);
|
||||
|
||||
Reference in New Issue
Block a user