feat(sdcard): SD / microSD card component (SDSPI + SDMMC) with separate init and mount; port every uSD BSP onto it - #800
Conversation
…te card init and FAT mount espp::SdCard brings up a card over SDSPI (any target) or the SDMMC / SDIO peripheral (ESP32, ESP32-S3, ESP32-P4) and keeps card initialization and FAT mounting as two steps: initialize() probes the card (card() is then a valid sdmmc_card_t), mount() / unmount() register the FAT volume at Config::mount_point any number of times. That is what USB mass storage needs -- espp::UsbDevice's MSC function hands the raw card to a PC, which must not happen while the firmware has the volume mounted -- and what ESP-IDF's all-in-one esp_vfs_fat_sd*_mount() cannot offer (its split variants only arrived in v6.1). - Config::interface is a variant of SpiConfig (host, cs, optionally own and free the bus) and SdmmcConfig (slot, 1/4-bit, GPIO-matrix pins, clock, optional on-chip LDO channel -- the ESP32-P4 powers its SD pads from LDO 4). - mount / unmount / format use the FatFs diskio + esp_vfs_fat_register calls IDF's helper uses internally (version-guarded for v5.0+), so the card is never re-probed; format() runs f_mkfs on the card's own drive. - card_info() / volume_info() / print_info(); std::error_code everywhere. - Example (SDMMC or SPI, pins via Kconfig; defaults = T-Dongle-S3 slot) records a boot counter, lists files, unmounts and remounts. Builds for esp32s3, esp32p4 (LDO) and esp32 (SPI). - README, docs page (storage/sdcard.rst), Doxyfile and CI matrix entries. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU
…SC docs
- drop the 2-character `SD` manifest tag (the registry requires 3-32
characters: "Manifest is not valid")
- add components/sdcard to upload_components.yml
- release the SDMMC host the way IDF does when slot init fails
- plain ESP_IDF_VERSION_MAJOR/MINOR tests (cppcheck can't parse
ESP_IDF_VERSION_VAL) and `= SpiConfig{}` initializers
- point the usb_device README and MSC example README at espp::SdCard for
the probe-without-mount step
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU
Replace the hand-rolled esp_vfs_fat_sd*_mount() code in t-deck, t-dongle-s3, m5stack-tab5, m5stack-cardputer, lilygo-t5-47, smartpanlee-sc01-plus, ws-s3-geek, ws-s3-lcd-1-47, xiao-esp32s3-sense, esp32-p4-eth, esp32-p4-function-ev-board, esp32-p4-module-dev-kit, esp32-p4-nano and esp32-p4-wifi6-dev-kit with a std::unique_ptr<espp::SdCard>. initialize_sdcard(SdCardConfig) and sdcard() keep their signatures; a new sdcard_component() accessor exposes the component (unmount / remount / format / volume info / MSC hand-off). Tab5 no longer keeps its own LDO handle. Manager-on examples list components/sdcard in EXTRA_COMPONENT_DIRS and the BSP manifests depend on espp/sdcard. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU
|
✅Static analysis result - no issues found! ✅ |
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Pull request overview
Introduces a new sdcard component (espp::SdCard) that supports SD / microSD over SDSPI or SDMMC with split initialize vs mount/unmount, and migrates multiple BSPs and docs to use it (including guidance for USB MSC hand-off).
Changes:
- Added
components/sdcardcomponent + example (Kconfig, docs, CI integration). - Ported several BSP SD card bring-up paths to
espp::SdCardand exposedsdcard_component()accessors. - Updated documentation (storage docs + USB MSC docs) to describe the split init/mount model and MSC usage.
Reviewed changes
Copilot reviewed 82 out of 82 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| doc/en/storage/sdcard_example.md | Adds storage docs page including the component example README. |
| doc/en/storage/sdcard.rst | New SD card documentation page describing the new component and its init/mount split. |
| doc/en/storage/index.rst | Adds sdcard to the storage docs toctree. |
| doc/Doxyfile | Adds sdcard header and example source to Doxygen inputs. |
| .github/workflows/upload_components.yml | Publishes the new sdcard component. |
| .github/workflows/build.yml | Adds CI build for components/sdcard/example. |
| components/sdcard/src/sdcard.cpp | Implements the espp::SdCard component (init host, probe, mount/unmount, format). |
| components/sdcard/include/sdcard.hpp | Public API for espp::SdCard (SPI/SDMMC config + split lifecycle). |
| components/sdcard/idf_component.yml | Component manager manifest for espp/sdcard. |
| components/sdcard/CMakeLists.txt | IDF-version-dependent driver dependencies + component registration. |
| components/sdcard/README.md | Component README describing configuration and MSC sharing. |
| components/sdcard/example/CMakeLists.txt | Top-level example project CMake. |
| components/sdcard/example/sdkconfig.defaults | Example defaults (target, FATFS LFN/label, stack). |
| components/sdcard/example/README.md | Example README showing init/mount split and MSC hand-off. |
| components/sdcard/example/main/CMakeLists.txt | Example “main” component registration. |
| components/sdcard/example/main/Kconfig.projbuild | Menuconfig options for SDMMC vs SPI and pins. |
| components/sdcard/example/main/sdcard_example.cpp | Example app demonstrating mount/unmount and basic file I/O. |
| components/usb_device/msc_example/README.md | Updates MSC example docs to reference espp::SdCard and BSP accessors. |
| components/usb_device/README.md | Updates MSC docs snippet to show using sdcard.card() and probe-without-mount. |
| components/xiao-esp32s3-sense/src/xiao-esp32s3-sense.cpp | Ports Xiao ESP32-S3 Sense SD init to espp::SdCard (SPI). |
| components/xiao-esp32s3-sense/include/xiao-esp32s3-sense.hpp | Switches SD handle to std::unique_ptr<SdCard> and adds sdcard_component(). |
| components/xiao-esp32s3-sense/idf_component.yml | Adds espp/sdcard dependency. |
| components/xiao-esp32s3-sense/CMakeLists.txt | Adds sdcard to REQUIRES. |
| components/xiao-esp32s3-sense/example/CMakeLists.txt | Adds local components/sdcard to example build. |
| components/ws-s3-lcd-1-47/src/sdcard.cpp | Ports WS-S3-LCD-1.47 SD init to espp::SdCard (SDMMC). |
| components/ws-s3-lcd-1-47/include/ws-s3-lcd-1-47.hpp | Switches SD handle to std::unique_ptr<SdCard> and adds sdcard_component(). |
| components/ws-s3-lcd-1-47/idf_component.yml | Adds espp/sdcard dependency. |
| components/ws-s3-lcd-1-47/CMakeLists.txt | Adds sdcard to REQUIRES. |
| components/ws-s3-geek/src/sdcard.cpp | Ports WS-S3-Geek SD init to espp::SdCard (SPI). |
| components/ws-s3-geek/include/ws-s3-geek.hpp | Switches SD handle to std::unique_ptr<SdCard> and adds sdcard_component(). |
| components/ws-s3-geek/idf_component.yml | Adds espp/sdcard dependency. |
| components/ws-s3-geek/CMakeLists.txt | Adds sdcard to REQUIRES. |
| components/t-dongle-s3/src/sdcard.cpp | Ports T-Dongle-S3 SD init to espp::SdCard (SDMMC). |
| components/t-dongle-s3/include/t-dongle-s3.hpp | Switches SD handle to std::unique_ptr<SdCard> and adds sdcard_component(). |
| components/t-dongle-s3/idf_component.yml | Adds espp/sdcard dependency. |
| components/t-dongle-s3/CMakeLists.txt | Adds sdcard to REQUIRES. |
| components/t-deck/src/sdcard.cpp | Ports T-Deck SD init to espp::SdCard (SPI on shared bus). |
| components/t-deck/include/t-deck.hpp | Switches SD handle to std::unique_ptr<SdCard> and adds sdcard_component(). |
| components/t-deck/idf_component.yml | Adds espp/sdcard dependency. |
| components/t-deck/CMakeLists.txt | Adds sdcard to REQUIRES. |
| components/smartpanlee-sc01-plus/src/smartpanlee-sc01-plus.cpp | Ports SmartPanlee SC01 Plus SD init to espp::SdCard (SPI) and uses volume_info(). |
| components/smartpanlee-sc01-plus/include/smartpanlee-sc01-plus.hpp | Switches SD handle to std::unique_ptr<SdCard> and adds sdcard_component(). |
| components/smartpanlee-sc01-plus/idf_component.yml | Adds espp/sdcard dependency. |
| components/smartpanlee-sc01-plus/CMakeLists.txt | Adds sdcard to REQUIRES. |
| components/m5stack-tab5/src/sdcard.cpp | Ports Tab5 SD init to espp::SdCard (SDMMC with LDO) and uses volume_info(). |
| components/m5stack-tab5/include/m5stack-tab5.hpp | Switches SD handle to std::unique_ptr<SdCard> and adds sdcard_component(). |
| components/m5stack-tab5/idf_component.yml | Adds espp/sdcard dependency. |
| components/m5stack-tab5/CMakeLists.txt | Adds sdcard to REQUIRES. |
| components/m5stack-tab5/example/CMakeLists.txt | Adds local components/sdcard to example build. |
| components/m5stack-cardputer/src/sdcard.cpp | Ports Cardputer SD init to espp::SdCard (SPI on shared bus). |
| components/m5stack-cardputer/include/m5stack-cardputer.hpp | Switches SD handle to std::unique_ptr<SdCard> and adds sdcard_component(). |
| components/m5stack-cardputer/idf_component.yml | Adds espp/sdcard dependency. |
| components/m5stack-cardputer/CMakeLists.txt | Adds sdcard to REQUIRES. |
| components/lilygo-t5-47/src/sdcard.cpp | Ports LilyGo T5-4.7 SD init to espp::SdCard (SPI on shared bus). |
| components/lilygo-t5-47/include/lilygo-t5-47.hpp | Switches SD handle to std::unique_ptr<SdCard> and adds sdcard_component(). |
| components/lilygo-t5-47/idf_component.yml | Adds espp/sdcard dependency. |
| components/lilygo-t5-47/CMakeLists.txt | Adds sdcard to REQUIRES. |
| components/lilygo-t5-47/example/CMakeLists.txt | Adds local components/sdcard to example build. |
| components/esp32-p4-wifi6-dev-kit/src/sdcard.cpp | Ports P4 WiFi6 Dev Kit SD init to espp::SdCard (SDMMC with LDO) and uses volume_info(). |
| components/esp32-p4-wifi6-dev-kit/include/esp32-p4-wifi6-dev-kit.hpp | Switches SD handle to std::unique_ptr<SdCard> and adds sdcard_component(). |
| components/esp32-p4-wifi6-dev-kit/idf_component.yml | Adds espp/sdcard dependency. |
| components/esp32-p4-wifi6-dev-kit/CMakeLists.txt | Adds sdcard to component REQUIRES list. |
| components/esp32-p4-wifi6-dev-kit/example/CMakeLists.txt | Adds local components/sdcard to example build. |
| components/esp32-p4-nano/src/sdcard.cpp | Ports P4 Nano SD init to espp::SdCard (SDMMC with LDO) and uses volume_info(). |
| components/esp32-p4-nano/include/esp32-p4-nano.hpp | Switches SD handle to std::unique_ptr<SdCard> and adds sdcard_component(). |
| components/esp32-p4-nano/idf_component.yml | Adds espp/sdcard dependency. |
| components/esp32-p4-nano/CMakeLists.txt | Adds sdcard to component REQUIRES list. |
| components/esp32-p4-nano/example/CMakeLists.txt | Adds local components/sdcard to example build. |
| components/esp32-p4-module-dev-kit/src/sdcard.cpp | Ports P4 Module Dev Kit SD init to espp::SdCard (SDMMC with LDO) and uses volume_info(). |
| components/esp32-p4-module-dev-kit/include/esp32-p4-module-dev-kit.hpp | Switches SD handle to std::unique_ptr<SdCard> and adds sdcard_component(). |
| components/esp32-p4-module-dev-kit/idf_component.yml | Adds espp/sdcard dependency. |
| components/esp32-p4-module-dev-kit/CMakeLists.txt | Adds sdcard to component REQUIRES list. |
| components/esp32-p4-module-dev-kit/example/CMakeLists.txt | Adds local components/sdcard to example build. |
| components/esp32-p4-function-ev-board/src/sdcard.cpp | Ports P4 Function EV Board SD init to espp::SdCard (SDMMC with LDO) and uses volume_info(). |
| components/esp32-p4-function-ev-board/include/esp32-p4-function-ev-board.hpp | Switches SD handle to std::unique_ptr<SdCard> and adds sdcard_component(). |
| components/esp32-p4-function-ev-board/idf_component.yml | Adds espp/sdcard dependency. |
| components/esp32-p4-function-ev-board/CMakeLists.txt | Adds sdcard to component REQUIRES list. |
| components/esp32-p4-eth/src/sdcard.cpp | Ports P4 ETH SD init to espp::SdCard (SDMMC with LDO) and uses volume_info(). |
| components/esp32-p4-eth/include/esp32-p4-eth.hpp | Switches SD handle to std::unique_ptr<SdCard> and adds sdcard_component(). |
| components/esp32-p4-eth/idf_component.yml | Adds espp/sdcard dependency. |
| components/esp32-p4-eth/CMakeLists.txt | Adds sdcard to component REQUIRES list. |
| components/esp32-p4-eth/example/CMakeLists.txt | Adds local components/sdcard to example build. |
Suppressed comments (5)
components/sdcard/src/sdcard.cpp:1
- If
host_.init()succeeds butsdspi_host_init_device()fails, the SDSPI host is not deinitialized (only the SPI bus is freed). This can leak driver state/resources and make subsequent attempts fail. Track whetherhost_.init()ran successfully and callcall_host_deinit(host_)on this failure path before freeing the bus / returning.
components/sdcard/src/sdcard.cpp:1 ff_diskio_register_sdmmc()returns anesp_err_t, but the result is currently ignored. If registration fails, subsequent VFS registration /f_mount()will operate on an unregistered drive and can fail in confusing ways (and may skip proper cleanup). Check the return value, log the ESP-IDF error name, and abort/cleanup early when it is notESP_OK.
components/sdcard/src/sdcard.cpp:1FR_INT_ERRindicates an internal FatFs error, not 'no filesystem'. Treating it as 'no filesystem' can trigger an unnecessary (and destructive) format whenformat_if_mount_failedis enabled. Suggestion: only enter the format path forFR_NO_FILESYSTEM(and possiblyFR_INVALID_DRIVEdepending on your supported cases) and handleFR_INT_ERRby returning anio_errorwith a clear log message instead.
components/sdcard/src/sdcard.cpp:1- This relies on an implicit invariant that
bus_initialized_can only be true whenconfig_.interfaceholdsSpiConfig. If that invariant is ever broken (e.g., future refactor or config mutation),std::get<SpiConfig>will throw. To make this more robust, consider storing the SPI host used for the bus in its own member when initializing (e.g.spi_bus_host_), or usestd::get_if<SpiConfig>here and only free when it matches.
components/sdcard/example/main/Kconfig.projbuild:1 - The Kconfig
range 1 4allows values 2 and 3, but the implementation rejects anything other than 1 or 4 (SdmmcConfig::bus_width must be 1 or 4). To prevent invalid configurations at build time, consider changing this to achoice(1-bit vs 4-bit) and mapping the choice to bus_width, or otherwise constraining the config so only 1 or 4 can be selected.
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
🟡 Changes recommended
Publication ordering, invalid menu values, and stale BSP mount-status reporting must be corrected.
Get a fresh assessment by requesting another Copilot review.
Review details
- Files reviewed: 82/82 changed files
- Comments generated: 12
- Review effort level: Balanced
- upload_components.yml: list sdcard ahead of the BSPs that depend on it (first-time publish ordering), with a note like ethernet / stream_frame - build.yml + Doxyfile: alphabetize the sdcard entries after rx8130ce - example Kconfig: SDMMC bus width is a 1-bit / 4-bit choice instead of an int range that also allowed 2 and 3 - esp32-p4-* / m5stack-tab5 / smartpanlee-sc01-plus: is_sd_card_available() reports the component's current mount state instead of a sticky initialized flag (false after sdcard_component()->unmount()) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU
There was a problem hiding this comment.
🟡 Changes recommended
Three mount-failure paths leave FatFs referencing freed filesystem state.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (2)
components/sdcard/src/sdcard.cpp:323
- When formatting fails after the initial
f_mount, theFATFSobject remains registered with FatFs. Unregistering the VFS then frees that object while the drive table still references it. Detach the failed mount before releasing the VFS and diskio, matching ESP-IDF's cleanup order.
components/sdcard/src/sdcard.cpp:332 - This general mount-failure path also frees the VFS
FATFSwhile it is still registered in FatFs's drive table. Callf_mount(nullptr, drive, 0)first; otherwise a later access to that logical drive can dereference freed state.
- Files reviewed: 82/82 changed files
- Comments generated: 1
- Review effort level: Balanced
…s or the T-Dongle-S3 BSP) menuconfig "MSC Example Configuration" picks the medium: the flash FAT partition (default), an SD card probed with espp::SdCard on configurable SDMMC pins (defaults: T-Dongle-S3 slot, optional P4 LDO channel), or the T-Dongle-S3 BSP's card via initialize_sdcard() + sdcard_component()->unmount() + sdcard() -- the hand-off pattern for every espp BSP with a uSD slot. SD cards are never formatted by the example. The BSP and its components are always compiled (manager-off dirs listed explicitly) so the choice lives in menuconfig; the BSP is only instantiated when selected. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU
|
Added db9cb0a: the 🤖 Generated with Claude Code |
…nyusb submodule Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU
…off dirs A stray working-tree edit slipped into ab15905 and replaced the tinyusb entry with a second esp_tinyusb one, so manager-off builds failed to resolve 'tinyusb'. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU
…ount failure f_mount(fs, drive, 1) registers fs in FatFs's drive table even when the volume fails to mount, so every mount_locked() failure path now calls f_mount(nullptr, drive, 0) before esp_vfs_fat_unregister_path() and ff_diskio_unregister(), matching ESP-IDF's own cleanup order. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU
…ent card validity while unmounted Self-review follow-ups on #800: - format_locked() used ff_memalloc(), which FatFs only declares when CONFIG_FATFS_LFN_HEAP is set (every build so far had it), and which may prefer PSRAM. Use heap_caps_malloc(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT) like IDF; verified with CONFIG_FATFS_LFN_NONE. - every BSP's sdcard() note said the card needs a valid mount point; it now says the card stays valid while the volume is unmounted (MSC hand-off). - MscMedium::sd_card doc points at espp::SdCard::card(). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU
Summary
New
sdcardcomponent (espp::SdCard) that brings up an SD / microSD card over SDSPI (any target) or the SDMMC / SDIO peripheral (ESP32, -S3, -P4), and ports every BSP with a microSD slot onto it.The one design point: card initialization and FAT mounting are separate steps.
initialize()brings up the host (SPI device, or SDMMC slot with an optional on-chip LDO) and probes the card;mount()/unmount()register the FAT volume any number of times while the card stays initialized.card()returns the initializedsdmmc_card_t *, which is exactly what theusb_deviceMSC function (#798) needs: probe the card withmount_on_initialize = falseand handcard()toMscMedium::sd_card, or on a BSP callinitialize_sdcard()thensdcard_component()->unmount().Component
Config::interfaceis astd::variant<SpiConfig, SdmmcConfig>:SpiConfig: host, CS, optional bus ownership (initialize_bus+ MOSI/MISO/SCLK/max transfer), frequency, CD/WP pins.SdmmcConfig: slot, 1- or 4-bit width, CLK/CMD/D0–D3 (GPIO-matrix targets; the ESP32 uses its fixed pins), frequency, CD/WP,ldo_channel(ESP32-P4 powers the SD pads from LDO 4).mount_on_initialize,format_if_mount_failed(default off: never wipe an unknown card),max_files,allocation_unit_size,disk_status_check,log_levellast.initialize / mount / unmount / format / deinitialize(with and withoutstd::error_code),card_info(),volume_info(),print_info(),is_initialized(),is_mounted().sdmmc_card_init+ff_diskio_register_sdmmc+esp_vfs_fat_register+f_mount) instead ofesp_vfs_fat_sd*_mount(): IDF's own split init/mount helpers only exist from IDF 6.1, and this keeps the floor at IDF 5.0 (esp_vfs_fat_registersignature is version-guarded; driver deps switch toesp_driver_*on 5.4+).components/sdcard/example, esp32s3 in CI; also built for esp32p4 and esp32/SPI): boot counter, file listing, unmount/remount. Menuconfig selects SDMMC or SPI and the pins (defaults are the T-Dongle-S3 / P4 LDO).storage/sdcard, Doxygen, CI build entry,upload_components.ymlentry.BSP ports (14)
t-deck,t-dongle-s3,m5stack-tab5,m5stack-cardputer,lilygo-t5-47,smartpanlee-sc01-plus,ws-s3-geek,ws-s3-lcd-1-47,xiao-esp32s3-sense,esp32-p4-eth,esp32-p4-function-ev-board,esp32-p4-module-dev-kit,esp32-p4-nano,esp32-p4-wifi6-dev-kit.Each BSP's
initialize_sdcard(SdCardConfig)andsdcard()keep their signatures. The hand-rolledesp_vfs_fat_sd*_mount()code becomes astd::unique_ptr<espp::SdCard>;sdcard()returnscard()as before and a newsdcard_component()accessor exposes the object (unmount / remount / format / volume info / MSC hand-off). Tab5's LDO handle member is gone (the component owns it). BSPSdCardConfigstructs are unchanged.Things to know
espp/sdcarduntil it is published (tab5, esp32-p4-eth / module-dev-kit / nano / wifi6-dev-kit, lilygo-t5-47, xiao-esp32s3-sense):EXTRA_COMPONENT_DIRSdoes not override a namespaced registry dependency, same asstream_framein feat(dispatcher): stream_frame codec + Dispatcher multiplexer; OTA rides them #747. They were all verified locally with a temporaryoverride_path(not committed). The manager-off examples and the component example build regardless.t-deck's untouchedaudio.cppfails locally oni2s_chan_config_t::rx_destination, but itssdcard.cppcompiles and CI on main is green for t-deck.format()/format_if_mount_faileduse FatFsf_mkfson the card's own drive number (unlikeesp_vfs_fatwhich formats drive 0).usb_deviceREADME and MSC example README now point atespp::SdCardfor the probe-without-mount step.Test plan
components/sdcard/examplebuilds for esp32s3, esp32p4, esp32 (SPI)initialize_sdcard()→unmount()→sdcard()): drive appears on the PC, about 1 MB/s🤖 Generated with Claude Code
https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU