Skip to content

stm32/sdcard: Add support for UHS-I SD cards. - #19623

Open
kwagyeman wants to merge 2 commits into
micropython:masterfrom
kwagyeman:kwabena/stm32n6_sd_uhs1
Open

stm32/sdcard: Add support for UHS-I SD cards.#19623
kwagyeman wants to merge 2 commits into
micropython:masterfrom
kwagyeman:kwabena/stm32n6_sd_uhs1

Conversation

@kwagyeman

Copy link
Copy Markdown
Contributor

Summary

This adds UHS-I support (1.8V signalling, SDR50/SDR104) to the stm32 port's SD card driver, for boards whose hardware can switch the SD bus I/O rail between 3.3V and 1.8V, and enables it on the OPENMV_N6 board at SDR104/200MHz.

On the OpenMV N6, same board/card/workload (16MB file, 512KB chunks), unpatched vs patched firmware:

3.3V 50MHz (current) SDR104 200MHz (this PR)
file read 21.8 MB/s 70.8 MB/s
file write 19.7 MB/s 56.1 MB/s
raw block read 22.4 MB/s 83.3 MB/s

SDR50 at 100MHz (43.5 MB/s raw reads) is available as a conservative alternative via one board macro.

A board opts in by defining MICROPY_HW_SDCARD_VSELECT_PIN (drives the SD rail voltage select) and MICROPY_HW_SDCARD_RESET_PIN (active-low SD card power). The reset pin doubles as a hardware-revision detect: it is sampled at startup and reads high only on UHS-I capable hardware, so one firmware supports old and new board revisions. The voltage switch itself (ACMD41 S18R / CMD11) is the HAL's USE_SD_TRANSCEIVER flow; the transceiver callback drives the select pin and, on STM32N6, moves the configured VDDIO domain (MICROPY_HW_SDCARD_VDDIO, defaulting from the SDMMC instance) to the 1.8V pad range. Cards that don't support 1.8V get a proper CMD6 high-speed switch at 3.3V; any UHS failure power-cycles the card via the reset pin and falls back to the existing 3.3V behaviour.

Two STM32N6 hardware findings shaped the implementation:

  1. The SDMMC data-path status flags read stale after the 1.8V switch until the FIFO is accessed. Command-path flags are fine and IDMA/IRQ transfers are unaffected, but any CPU-polled receive loop that gates FIFO reads on RXFIFOHF/DATAEND deadlocks — which is exactly what the HAL's own SD_UltraHighSpeed()/SD_SwitchSpeed() do (against a 0xFFFFFFFF ms software timeout), so HAL_SD_ConfigSpeedBusOperation() hangs forever on this part. The driver therefore performs the CMD6 switch itself using the CMDTRANS mechanism, drains the 64-byte status from the FIFO after the transfer time has elapsed, and only then consults the (by then refreshed) status flags. This was proven at register level: the FIFO contained the complete, correct switch status while STA/DCOUNT still claimed no data had arrived.

  2. VDDIOxVRSEL writes are silently ignored unless the matching HSLV_VDDIOx OTP fuse is programmed. Without the fuse the pads stay in 3.3V mode: the (slow) initialisation still passes, then every transfer at UHS rates fails. Verified by A/B on two boards (fused vs unfused). The driver reads the range back after switching and treats a non-stick as a failed attempt, so unfused boards cleanly fall back to 3.3V — and once a bootloader update programs the fuse, the same firmware starts running SDR104 automatically. The OPENMV_N6 board commit programs the fuse (RM0486 Table 18, OTP word 124 bit 14) alongside the existing VDDIO2/3 fuses.

Testing

  • OpenMV N6 (STM32N657, fused): SDR104 at 200MHz — numbers above, data CRC-verified against 3.3V-mode reads of the same blocks, 10/10 power-cycle/init soak loops at both SDR104 and SDR50, plus 16MB file write/read-back CRC verification.
  • OpenMV N6 (second board, OTP fuse unprogrammed): VRSEL non-stick detected, clean automatic fallback to 3.3V/50MHz — 10/10 init soaks, 22.35 MB/s (identical to unpatched firmware), OTP verified untouched before/after.
  • Regression on non-UHS boards: builds without the new config macros compile to byte-identical binaries (verified by hash for four board targets — the feature preprocesses away entirely). On-hardware SD regression runs (raw reads, file write/read, CRC verification) passed on STM32F427, STM32F765 and STM32H743 (x2 boards) hardware with speeds unchanged.
  • Build-tested: PYBV11, NUCLEO_F767ZI, NUCLEO_H743ZI, NUCLEO_N657X0 (the latter verifying the USE_SD_TRANSCEIVER=0 default path on N6).
  • Non-UHS boards keep ClockPowerSave enabled and the exact current init sequence; UHS-I init disables clock power saving (as the ST BSPs do) because the voltage-switch and CMD6 sequences stall with it enabled.

Trade-offs and Alternatives

  • Code size: +992 bytes text on OPENMV_N6 (the board that enables it). Zero impact on all other boards — without the config macros the binaries are byte-identical.
  • Using the HAL's HAL_SD_ConfigSpeedBusOperation() instead of a driver-side CMD6: attempted first, but it hard-hangs on the N6 (finding 1 above), so the CMD6 transaction is implemented in sdcard.c with the same command-transfer mechanism regular block reads use.
  • Unfused/older hardware cost: on UHS-capable board revisions whose OTP fuse isn't programmed yet, each SD init spends two failed UHS attempts (two card power cycles, ~300-400ms) before falling back to 3.3V. This is once per mount and buys automatic activation when the fuse is later programmed; caching the result to skip retries was considered but rejected as unnecessary complexity for a once-per-boot cost.
  • SDR104 vs SDR50 default: the generic driver defaults to the conservative SDR50; OPENMV_N6 explicitly selects SDR104, which soaked cleanly. The receive path uses the feedback clock via the delay block with its default configuration, matching what the ST HAL does for UHS modes; per-unit DLYB tuning was not needed at 200MHz but could be added later if a marginal card/board combination shows CRC errors.

Add support for running an SD card in UHS-I mode (1.8V signalling, up to
SDR104) on boards whose hardware can switch the SD bus I/O rail between
3.3V and 1.8V.  A board enables this by defining:

- MICROPY_HW_SDCARD_VSELECT_PIN: output pin selecting the SD bus I/O rail
  voltage, low for 3.3V and high for 1.8V.
- MICROPY_HW_SDCARD_RESET_PIN: active-low output pin controlling the SD
  card supply.  It is sampled at startup to detect whether the hardware
  supports UHS-I (it reads high if so), allowing one firmware to support
  both UHS-I capable and older hardware revisions.
- MICROPY_HW_SDCARD_UHS_SWITCH_PATTERN/MICROPY_HW_SDCARD_UHS_CLK_DIV:
  optional speed mode (default SDR50) and final clock divider.
- MICROPY_HW_SDCARD_VDDIO: on STM32N6, which VDDIO power domain the SD
  bus pins are in (2-5), defaulting to the domain of the default pins of
  the SDMMC in use (4 for SDMMC1, 5 for SDMMC2).

Card identification and the CMD11 voltage switch are handled by the HAL
(USE_SD_TRANSCEIVER); HAL_SD_DriveTransceiver_1_8V_Callback drives the
voltage select pin and, on STM32N6, also switches the configured VDDIO
domain's pad voltage range in the PWR peripheral.  The pad range is read
back after the switch: on the N6 the VDDIOxVRSEL write is silently
ignored unless the matching HSLV_VDDIOx OTP fuse is set, in which case
the pads stay in 3.3V mode and fail at UHS-I rates even though the
(slower) initialisation succeeds, so an unfused device deliberately fails
the attempt and falls back.  A card that does not support 1.8V
signalling continues at 3.3V and is switched to high speed mode via
CMD6, and any UHS-I failure falls back to the previous plain 3.3V
behaviour after power cycling the card via the reset pin.

The CMD6 switch itself is implemented here with the command-transfer
(CMDTRANS) mechanism instead of HAL_SD_ConfigSpeedBusOperation: the HAL
arms the data path via DCTRL.DTEN without linking it to the command, and
additionally, after the 1.8V switch the SDMMC data path status flags read
stale until the FIFO is accessed, so the HAL's flag-driven receive loop
deadlocks (against its 0xFFFFFFFF ms software timeout).  The switch
status is instead drained from the FIFO after the transfer time has
elapsed, and the status flags are only consulted afterwards, once they
have refreshed.

Boards without the new pins compile to byte-identical binaries (verified
for OPENMV2/OPENMV3/OPENMV4/OPENMV4P), and SD operation was regression
tested on STM32F427, STM32F765 and STM32H743 hardware.  Tested on an
OpenMV N6 (STM32N657) against the same board, card and 16MB file
workload on unpatched firmware: file reads go from 21.8MB/s to 70.8MB/s
and file writes from 19.7MB/s to 56.1MB/s (raw block reads from
22.4MB/s to 83.3MB/s) at SDR104/200MHz, with SDR50/100MHz reading
43.5MB/s, data verified against 3.3V-mode reads, and stable across
repeated power cycle/init loops; the fuse-less fallback path was
verified on a second N6 with the OTP fuse unprogrammed.

Signed-off-by: Kwabena W. Agyeman <kwagyeman@live.com>
Enable UHS-I on the OpenMV N6: SD_RESET (PC7) is sampled at startup to
detect UHS-I capable hardware, and SD_VSELECT (PG6) switches the SD bus
rail (the VDDIO4 domain) to 1.8V.  The board runs SDR104 at 200MHz (the
SDMMC1 kernel clock is HCLK), raising raw SD reads from 22.4MB/s to
83.3MB/s.

The HSLV_VDDIO4 OTP fuse (RM0486 Table 18, OTP word 124 bit 14) is
programmed alongside the existing VDDIO2/3 fuses: without it the
VDDIO4VRSEL 1.8V pad range selection is ignored by the hardware, which
was verified on an unfused board (the SD driver detects this and falls
back to 3.3V operation).  VDDIO4 still boots in the 3.3V range; the SD
driver switches it at runtime only after the card acknowledges the 1.8V
voltage switch.

Signed-off-by: Kwabena W. Agyeman <kwagyeman@live.com>
@kwagyeman kwagyeman moved this to In progress in OpenMV Features Aug 14, 2026
@kwagyeman kwagyeman changed the title Kwabena/stm32n6 sd uhs1 stm32/sdcard: Add support for UHS-I SD cards. Aug 14, 2026
@kwagyeman

Copy link
Copy Markdown
Contributor Author

@dpgeorge - This script can be used to test the performance on a board. You need a microSD card with UHS-I support (pretty much any SDXC card).

# SD card benchmark: 16MB file write/read + raw block reads.
# Run on any board with pyb.SDCard (results above: OPENMV_N6).
import os, time, vfs, pyb

sd = pyb.SDCard()
sd.power(1)
print("card:", sd.info())

fs = vfs.VfsFat(sd)
vfs.mount(fs, "/sd")

CHUNK = 512 * 1024
N = 32  # 16MB total
buf = bytearray(CHUNK)
for i in range(CHUNK):
    buf[i] = i & 0xFF

# File write.
t = time.ticks_ms()
with open("/sd/sdbench.bin", "wb") as f:
    for _ in range(N):
        f.write(buf)
dt = time.ticks_diff(time.ticks_ms(), t)
print("file write: %.1f MB/s" % (N * CHUNK / 1024 / 1024 * 1000 / dt))

# File read.
t = time.ticks_ms()
with open("/sd/sdbench.bin", "rb") as f:
    while f.readinto(buf):
        pass
dt = time.ticks_diff(time.ticks_ms(), t)
print("file read: %.1f MB/s" % (N * CHUNK / 1024 / 1024 * 1000 / dt))

os.remove("/sd/sdbench.bin")
vfs.umount("/sd")

# Raw block reads (8MB from the start of the card).
t = time.ticks_ms()
for i in range(16):
    sd.readblocks(i * 1024, buf)
dt = time.ticks_diff(time.ticks_ms(), t)
print("raw read: %.1f MB/s" % (16 * CHUNK / 1024 / 1024 * 1000 / dt))

@github-actions

Copy link
Copy Markdown

Code size report:

Reference:  lib/tinyusb: Update tinyusb submodule to MicroPython's fork. [791ba6e]
Comparison: stm32/boards/OPENMV_N6: Enable SD card UHS-I mode at SDR104. [merge of b609e90]
  mpy-cross:    +0 +0.000% 
   bare-arm:    +0 +0.000% 
minimal x86:    +0 +0.000% 
   unix x64:    +0 +0.000% standard
      stm32:    +0 +0.000% PYBV10
      esp32:    +0 +0.000% ESP32_GENERIC
     mimxrt:    +0 +0.000% TEENSY40
        rp2:    +0 +0.000% RPI_PICO_W
       samd:    +0 +0.000% ADAFRUIT_ITSYBITSY_M4_EXPRESS
  qemu rv32:    +0 +0.000% VIRT_RV32

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In progress

Development

Successfully merging this pull request may close these issues.

1 participant