F-9735: roll back the 1.8V signaling switch when the SDHCI UHS retry fails

disk_read() called sdhci_uhs_recover() after any nonzero status from a
data transfer. The recovery is a guess - no platform or retained state
in wolfBoot identifies a warm-reset UHS condition, so on a plain 3.3V
cold boot any first transfer failure (CRC, DMA, media, controller)
switched the host to 1.8V signaling (SDHCI_SRS15_V18SE) without a CMD11
voltage negotiation. When the retry also failed, the voltage setting
was never rolled back and g_uhs_recovered prevented further recovery:
a transient error left the host - for the rest of the boot and the
next stage - in a signaling state the card is not using.

Make the guess self-correcting: if the retry of the failed chunk also
fails, restore the previous signaling (clear V18SE, re-enable the SD
clock) via sdhci_uhs_recover_rollback() before reporting the error. If
the retry succeeds the switch stays - that is the warm-reset UHS case
the recovery exists for. A later failure in a subsequent chunk no
longer rolls back a configuration that is actually working. The
unconditional break-on-error for eMMC builds is unchanged.

Added tools/unit-tests/unit-sdhci-uhs-recover: it compiles the real
src/sdhci.c (generated host copy with the three asm statements blanked
and the real sdhci_read() renamed so the test can provide a scripted
one) with register access backed by a host array, and asserts on the
SDHCI_SRS15/SDHCI_SRS11 state for: a clean read (no switch), a failed
first attempt with a successful retry (switch kept), a failed retry
(roll back), and a later failure after a working recovery (switch
kept). The rollback case fails pre-fix.

Verified: unit-sdhci-uhs-recover 4/4 green post-fix (rollback case
fails pre-fix), src/sdhci.c syntax-clean under DISK_EMMC,
tools/unit-tests suite green (110 binaries).
pull/862/head
Daniele Lacamera 2026-08-18 02:25:56 +02:00
parent ceeaf2f24d
commit d85127c173
3 changed files with 287 additions and 3 deletions

View File

@ -394,8 +394,14 @@ static int sdhci_set_power(uint32_t voltage)
*
* This restores signaling the card is already using rather than initiating a
* voltage switch, so no CMD11 sequence is involved. It runs at most once per
* boot, and only after a transfer has already failed, so a cold boot never
* reaches it.
* boot, and only after a transfer has already failed.
*
* Note: on a plain 3.3V cold boot this is a guess - any first transfer
* failure (CRC, DMA, media, controller) triggers it. The guess is
* self-correcting: the caller must undo the switch with
* sdhci_uhs_recover_rollback() if the retry fails, so a wrong guess
* leaves the host where it started rather than in a voltage state the
* card is not using.
*
* Returns 0 if the switch was applied and the caller should retry. */
static int sdhci_uhs_recover(void)
@ -419,6 +425,21 @@ static int sdhci_uhs_recover(void)
return 0;
}
/* Undo a failed sdhci_uhs_recover(): the retry at 1.8V did not fix the
* transfer, so this is not a warm-reset UHS condition. Restore 3.3V
* signaling and the clock so the host is not left (for this boot and
* the next stage) in a voltage state the card is not using. */
static void sdhci_uhs_recover_rollback(void)
{
sdhci_reg_and(SDHCI_SRS11, ~SDHCI_SRS11_SDCE);
sdhci_reg_and(SDHCI_SRS15, ~SDHCI_SRS15_V18SE);
udelay(5000); /* let the level shifter settle */
sdhci_reg_or(SDHCI_SRS11, SDHCI_SRS11_SDCE);
udelay(1000);
}
#endif /* DISK_SDCARD */
/* ============================================================================
@ -1739,6 +1760,9 @@ int disk_read(int drv, uint64_t start, uint32_t count, uint8_t *buf)
uint32_t read_sz, block_addr;
uint32_t tmp_block[SDHCI_BLOCK_SIZE/sizeof(uint32_t)];
uint32_t start_offset = (start % SDHCI_BLOCK_SIZE);
#ifdef DISK_SDCARD
int uhs_switched = 0;
#endif /* DISK_SDCARD */
(void)drv; /* only one drive supported */
#ifdef DEBUG_SDHCI
@ -1844,9 +1868,15 @@ int disk_read(int drv, uint64_t start, uint32_t count, uint8_t *buf)
#endif
}
#ifdef DISK_SDCARD
if (status != 0 && sdhci_uhs_recover() == 0) {
if (status != 0 && !uhs_switched && sdhci_uhs_recover() == 0) {
uhs_switched = 1;
continue; /* retry this chunk with matched signaling */
}
if (status != 0 && uhs_switched) {
/* The 1.8V retry failed: this is not a warm-reset UHS
* condition, restore 3.3V signaling before giving up. */
sdhci_uhs_recover_rollback();
}
#endif /* DISK_SDCARD */
if (status != 0) {
break;
@ -1855,6 +1885,9 @@ int disk_read(int drv, uint64_t start, uint32_t count, uint8_t *buf)
start += read_sz;
buf += read_sz;
count -= read_sz;
#ifdef DISK_SDCARD
uhs_switched = 0; /* chunk read cleanly */
#endif /* DISK_SDCARD */
}
return status;
}

View File

@ -105,6 +105,7 @@ TESTS+=unit-p1021-fcm-bytes
TESTS+=unit-ls1028a-xspi-write
TESTS+=unit-zynq-erase-loop
TESTS+=unit-versal-qspi-dma
TESTS+=unit-sdhci-uhs-recover
# The x86-64 EFI unit test needs the gnu-efi development headers (same
# dependency as the CMake x86_64_efi target). Probe and only add the test
@ -833,6 +834,22 @@ versal_host.c: ../../hal/versal.c versal_host.h
unit-versal-qspi-dma: unit-versal-qspi-dma.c versal_host.c
gcc -o $@ unit-versal-qspi-dma.c -DTARGET_versal -DEXT_FLASH -DARCH_AARCH64 -DARCH_64BIT $(CFLAGS) $(LDFLAGS)
# unit-sdhci-uhs-recover drives disk_read()'s UHS recovery path from the
# real src/sdhci.c (F-9735: any read error permanently switched the host
# to 1.8V signaling with no rollback). sdhci_host.c (generated below) is
# identical to the real file except the three `asm volatile` statements
# are blanked (they cannot assemble on x86) and the real sdhci_read() is
# renamed to sdhci_read_hw() so the test can provide a scripted one. The
# platform-provided sdhci_reg_read()/sdhci_reg_write() are backed by a
# host register array; hal_get_timer_us() and wolfBoot_printf() are
# stubbed in the test.
sdhci_host.c: ../../src/sdhci.c
sed -E -e 's/^[[:space:]]*asm volatile\("[^"]*"[^;]*\);[[:space:]]*//' -e 's/^int sdhci_read\(/int sdhci_read_hw(/' $< > $@
unit-sdhci-uhs-recover: unit-sdhci-uhs-recover.c sdhci_host.c
gcc -o $@ unit-sdhci-uhs-recover.c -DDISK_SDCARD $(CFLAGS) $(LDFLAGS)
# unit-efi-x86-open-image includes hal/x86_64_efi.c directly, with in-test
# host mocks standing in for the gnu-efi runtime (BS, LibFileInfo, the
# x86-64 efi_callN() trampolines). This is the only host build coverage

View File

@ -0,0 +1,234 @@
/* unit-sdhci-uhs-recover.c
*
* Regression test for F-9735: disk_read() called sdhci_uhs_recover()
* after any nonzero status from a data transfer, which permanently
* switched the host to 1.8V signaling (SDHCI_SRS15_V18SE) on a plain
* 3.3V cold boot whenever the first transfer failed for any reason
* (CRC, DMA, media, controller). If the retry also failed, the voltage
* setting was never rolled back and g_uhs_recovered prevented further
* recovery - so a transient error left the host with a signaling level
* the card is not using, for the rest of the boot and the stage that
* follows it.
*
* The test compiles the real src/sdhci.c (as sdhci_host.c, generated by
* the Makefile). The generated copy differs from the real file only in:
* - three `asm volatile` barrier/nop statements blanked (cannot
* assemble on x86);
* - the real sdhci_read() renamed to sdhci_read_hw() so this file can
* provide a scripted sdhci_read() that stands in for the command /
* data-transfer state machine.
* Register access goes through the platform-provided sdhci_reg_read()/
* sdhci_reg_write(), backed here by a host register array, so the test
* asserts directly on the SDHCI_SRS15 / SDHCI_SRS11 state.
*
* Copyright (C) 2026 wolfSSL Inc.
*
* This file is part of wolfBoot.
*
* wolfBoot is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* wolfBoot is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA
*/
#include <check.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <stdarg.h>
#include "sdhci.h"
#include "disk.h"
/* Host register file for the platform-provided register accessors. */
static uint32_t g_sdhci_regs[0x400 / sizeof(uint32_t)];
uint32_t sdhci_reg_read(uint32_t offset)
{
return g_sdhci_regs[offset / sizeof(uint32_t)];
}
void sdhci_reg_write(uint32_t offset, uint32_t val)
{
g_sdhci_regs[offset / sizeof(uint32_t)] = val;
}
/* Timer for udelay(): advance on every read so busy loops terminate. */
static uint64_t g_timer_us;
uint64_t hal_get_timer_us(void)
{
return ++g_timer_us;
}
void wolfBoot_printf(const char *format, ...)
{
(void)format;
}
/* Platform hooks the SD init path touches; irrelevant to the recovery
* logic under test. */
void sdhci_platform_init(void)
{
}
void sdhci_platform_irq_init(void)
{
}
void sdhci_platform_set_bus_mode(int is_emmc)
{
(void)is_emmc;
}
/* Scripted sdhci_read(): plays back g_read_script one entry per call.
* 0 = success (fills dst with a per-block pattern), nonzero = failure. */
#define MAX_READ_CALLS 32
static int g_read_script[MAX_READ_CALLS];
static int g_read_calls;
static int g_script_len;
int sdhci_read(uint32_t cmd_index, uint32_t block_addr, uint32_t *dst,
uint32_t sz)
{
int ret;
uint32_t i;
(void)cmd_index;
if (g_read_calls >= g_script_len)
return -1;
ret = g_read_script[g_read_calls];
g_read_calls++;
if (ret == 0) {
for (i = 0; i < sz / sizeof(uint32_t); i++)
dst[i] = (uint32_t)(0xA5 + block_addr + i);
}
return ret;
}
/* The real HAL (identical to src/sdhci.c apart from the transforms
* documented above). */
#include "sdhci_host.c"
static void setup(void)
{
memset(g_sdhci_regs, 0, sizeof(g_sdhci_regs));
g_timer_us = 0;
g_read_calls = 0;
g_script_len = 0;
}
static void teardown(void)
{
}
static void script(const int *vals, int n)
{
if (n > MAX_READ_CALLS)
n = MAX_READ_CALLS;
memcpy(g_read_script, vals, (size_t)n * sizeof(int));
g_script_len = n;
}
/* A clean read must not touch the signaling level at all. */
START_TEST(test_clean_read_no_switch)
{
static uint8_t __attribute__((aligned(4))) buf[SDHCI_BLOCK_SIZE];
script((int[]){ 0 }, 1);
ck_assert_int_eq(disk_read(0, 0, sizeof(buf), buf), 0);
ck_assert_int_eq(g_sdhci_regs[SDHCI_SRS15 / 4] & SDHCI_SRS15_V18SE, 0);
}
END_TEST
/* First transfer fails, the 1.8V retry succeeds: the switch stays (the
* card really was left in UHS-I by a previous stage). */
START_TEST(test_retry_success_keeps_switch)
{
static uint8_t __attribute__((aligned(4))) buf[SDHCI_BLOCK_SIZE];
script((int[]){ -1, 0 }, 2);
ck_assert_int_eq(disk_read(0, 0, sizeof(buf), buf), 0);
ck_assert_int_eq(g_sdhci_regs[SDHCI_SRS15 / 4] & SDHCI_SRS15_V18SE,
SDHCI_SRS15_V18SE);
ck_assert_int_eq(g_sdhci_regs[SDHCI_SRS11 / 4] & SDHCI_SRS11_SDCE,
SDHCI_SRS11_SDCE);
ck_assert_uint_eq(buf[0], 0xA5);
}
END_TEST
/* First transfer fails and the 1.8V retry fails too: this is not a
* warm-reset UHS condition - restore 3.3V signaling and the clock so
* the host is not left in a state the card is not using. */
START_TEST(test_retry_failure_rolls_back)
{
static uint8_t __attribute__((aligned(4))) buf[SDHCI_BLOCK_SIZE];
script((int[]){ -1, -1 }, 2);
ck_assert_int_eq(disk_read(0, 0, sizeof(buf), buf), -1);
ck_assert_int_eq(g_sdhci_regs[SDHCI_SRS15 / 4] & SDHCI_SRS15_V18SE, 0);
ck_assert_int_eq(g_sdhci_regs[SDHCI_SRS11 / 4] & SDHCI_SRS11_SDCE,
SDHCI_SRS11_SDCE);
}
END_TEST
/* A later read fails after a recovery that is actually working: the
* 1.8V configuration must be kept (rolling it back would break a card
* that is really at 1.8V). Two disk_read() calls because the upper
* layer reads in chunks. */
START_TEST(test_later_failure_keeps_working_switch)
{
static uint8_t __attribute__((aligned(4))) buf1[SDHCI_BLOCK_SIZE];
static uint8_t __attribute__((aligned(4))) buf2[SDHCI_BLOCK_SIZE];
/* chunk 1: first attempt fails, 1.8V retry works */
script((int[]){ -1, 0 }, 2);
ck_assert_int_eq(disk_read(0, 0, sizeof(buf1), buf1), 0);
ck_assert_int_eq(g_sdhci_regs[SDHCI_SRS15 / 4] & SDHCI_SRS15_V18SE,
SDHCI_SRS15_V18SE);
/* chunk 2: a later, unrelated failure - the working 1.8V config
* must stay */
script((int[]){ -1 }, 1);
ck_assert_int_eq(disk_read(0, SDHCI_BLOCK_SIZE, sizeof(buf2), buf2),
-1);
ck_assert_int_eq(g_sdhci_regs[SDHCI_SRS15 / 4] & SDHCI_SRS15_V18SE,
SDHCI_SRS15_V18SE);
}
END_TEST
Suite *sdhci_uhs_suite(void)
{
Suite *s = suite_create("sdhci-uhs-recover");
TCase *tc = tcase_create("sdhci-uhs-recover");
tcase_add_checked_fixture(tc, setup, teardown);
tcase_add_test(tc, test_clean_read_no_switch);
tcase_add_test(tc, test_retry_success_keeps_switch);
tcase_add_test(tc, test_retry_failure_rolls_back);
tcase_add_test(tc, test_later_failure_keeps_working_switch);
suite_add_tcase(s, tc);
return s;
}
int main(void)
{
int fails;
Suite *s = sdhci_uhs_suite();
SRunner *sr = srunner_create(s);
srunner_run_all(sr, CK_NORMAL);
fails = srunner_ntests_failed(sr);
srunner_free(sr);
return fails;
}