From 6436543cfbdf01854f72b03b4206a25258bef04d Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Thu, 17 Sep 2026 12:36:02 -0500 Subject: [PATCH 1/9] feat(sdcard): SD / microSD card component (SDSPI + SDMMC) with separate 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 Claude-Session: https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU --- .github/workflows/build.yml | 2 + components/sdcard/CMakeLists.txt | 18 + components/sdcard/README.md | 115 ++++ components/sdcard/example/CMakeLists.txt | 22 + components/sdcard/example/README.md | 52 ++ components/sdcard/example/main/CMakeLists.txt | 5 + .../sdcard/example/main/Kconfig.projbuild | 70 +++ .../sdcard/example/main/sdcard_example.cpp | 122 ++++ components/sdcard/example/sdkconfig.defaults | 9 + components/sdcard/idf_component.yml | 25 + components/sdcard/include/sdcard.hpp | 285 ++++++++++ components/sdcard/src/sdcard.cpp | 519 ++++++++++++++++++ doc/Doxyfile | 2 + doc/en/storage/index.rst | 1 + doc/en/storage/sdcard.rst | 52 ++ doc/en/storage/sdcard_example.md | 2 + 16 files changed, 1301 insertions(+) create mode 100644 components/sdcard/CMakeLists.txt create mode 100644 components/sdcard/README.md create mode 100644 components/sdcard/example/CMakeLists.txt create mode 100644 components/sdcard/example/README.md create mode 100644 components/sdcard/example/main/CMakeLists.txt create mode 100644 components/sdcard/example/main/Kconfig.projbuild create mode 100644 components/sdcard/example/main/sdcard_example.cpp create mode 100644 components/sdcard/example/sdkconfig.defaults create mode 100644 components/sdcard/idf_component.yml create mode 100644 components/sdcard/include/sdcard.hpp create mode 100644 components/sdcard/src/sdcard.cpp create mode 100644 doc/en/storage/sdcard.rst create mode 100644 doc/en/storage/sdcard_example.md diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b94112a963..32dd267dfd 100755 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -278,6 +278,8 @@ jobs: target: esp32 - path: 'components/rtsp/example' target: esp32 + - path: 'components/sdcard/example' + target: esp32s3 - path: 'components/runqueue/example' target: esp32 - path: 'components/rx8130ce/example' diff --git a/components/sdcard/CMakeLists.txt b/components/sdcard/CMakeLists.txt new file mode 100644 index 0000000000..d4dd21abe1 --- /dev/null +++ b/components/sdcard/CMakeLists.txt @@ -0,0 +1,18 @@ +idf_build_get_property(idf_version IDF_VERSION) + +# sdmmc: sdmmc_card_t / sdmmc_card_init; the SD host drivers and spi_master +# (SDSPI) are named in the public header; fatfs: the FAT mount / diskio calls. +set(requires base_component sdmmc fatfs) +# The SD host / SPI drivers moved out of the umbrella `driver` component in +# ESP-IDF v5.4. +if(idf_version VERSION_LESS "5.4") + list(APPEND requires driver) +else() + list(APPEND requires esp_driver_sdspi esp_driver_sdmmc esp_driver_spi) +endif() + +idf_component_register( + INCLUDE_DIRS "include" + SRC_DIRS "src" + REQUIRES ${requires} +) diff --git a/components/sdcard/README.md b/components/sdcard/README.md new file mode 100644 index 0000000000..62ae968a26 --- /dev/null +++ b/components/sdcard/README.md @@ -0,0 +1,115 @@ +# SD Card Component + +[![Badge](https://components.espressif.com/components/espp/sdcard/badge.svg)](https://components.espressif.com/components/espp/sdcard) + +`espp::SdCard` brings up an SD / microSD card over **SDSPI** (any target) or the +**SDMMC (SDIO)** peripheral (ESP32, ESP32-S3, ESP32-P4) and mounts its FAT +volume, keeping the two as separate steps: + +- `initialize()` brings up the host (an SPI device, or an SDMMC slot with an + optional on-chip LDO powering the card) and probes the card. Afterwards + `card()` is a valid `sdmmc_card_t` for raw sector access or for handing to + another owner. +- `mount()` / `unmount()` register / unregister the FAT volume at + `Config::mount_point`, any number of times, while the card stays initialized. + +ESP-IDF's `esp_vfs_fat_sd*_mount()` helpers do both in one call and own the card +while it is mounted, which gets in the way when something else needs the raw +card. The main case is USB mass storage: `espp::UsbDevice`'s MSC function hands +the card to a PC, which must not happen while the firmware has the volume +mounted. + + +**Table of Contents** + +- [SD Card Component](#sd-card-component) + - [Configuration](#configuration) + - [API](#api) + - [Sharing the card with a USB host (MSC)](#sharing-the-card-with-a-usb-host-msc) + - [Example](#example) + - [Notes](#notes) + + + +## Configuration + +`Config::interface` selects the wiring with a `std::variant`: + +```cpp +// SDMMC, 4-bit, pins through the GPIO matrix (ESP32-S3 / -P4) +espp::SdCard::SdmmcConfig sdmmc; +sdmmc.slot = 1; +sdmmc.bus_width = 4; +sdmmc.clk = GPIO_NUM_12; sdmmc.cmd = GPIO_NUM_16; +sdmmc.d0 = GPIO_NUM_14; sdmmc.d1 = GPIO_NUM_17; sdmmc.d2 = GPIO_NUM_21; sdmmc.d3 = GPIO_NUM_18; +sdmmc.frequency_khz = SDMMC_FREQ_HIGHSPEED; // 40 MHz +sdmmc.ldo_channel = -1; // 4 on the ESP32-P4 (LDO_VO4 powers the SD pads) + +// SPI, on a bus the BSP already initialized (shared with a display) +espp::SdCard::SpiConfig spi; +spi.host = SPI2_HOST; +spi.cs = GPIO_NUM_39; +spi.initialize_bus = false; // set true (with mosi/miso/sclk) to let SdCard own the bus + +espp::SdCard::Config config; +config.interface = sdmmc; // or spi +config.mount_point = "/sdcard"; +config.mount_on_initialize = true; // false: probe only, mount() later +config.format_if_mount_failed = false; // never wipe an unknown card by default +config.max_files = 5; +config.allocation_unit_size = 16 * 1024; +``` + +## API + +- `bool initialize(std::error_code&)` — host + card probe (+ mount by default). + `no_such_device` means no card answered: check the card, wiring and pull-ups. +- `bool mount(ec)` / `bool unmount(ec)` — the FAT volume at `mount_point()`. + `mount()` reports `no_such_device` when the card has no FAT filesystem and + `format_if_mount_failed` is off. +- `bool format(ec)` — create a fresh FAT filesystem (erases the card), remounting + afterwards if the volume was mounted. +- `sdmmc_card_t *card()` — the initialized card (stable for the object's life). +- `card_info()` — name, capacity, sector size, bus width / clock, SDHC, MMC. +- `volume_info()` — total / free bytes while mounted. +- `is_initialized()`, `is_mounted()`, `interface()`, `mount_point()`, + `print_info()`, `deinitialize(ec)` (also done by the destructor). + +## Sharing the card with a USB host (MSC) + +Probe without mounting, then hand `card()` to the MSC function, which mounts the +card at its own path while the application owns it and unmounts it while the PC +does: + +```cpp +espp::SdCard::Config config; +config.interface = sdmmc; +config.mount_on_initialize = false; +espp::SdCard sdcard(config); +sdcard.initialize(); + +espp::UsbDevice::MscMedium medium; +medium.type = espp::UsbDevice::MscMedium::Type::SdCard; +medium.sd_card = sdcard.card(); +medium.base_path = "/sdcard"; +``` + +Do not `mount()` here while the MSC function has the card: FatFs and the USB +host would both write the volume. See the `usb_device` component's `msc_example`. + +## Example + +The [example](./example) probes a card over SDMMC or SPI (configured in +menuconfig), records a boot counter, lists the files, and unmounts / remounts the +volume while the card stays initialized. + +## Notes + +- SDMMC needs `SOC_SDMMC_HOST_SUPPORTED` (ESP32, ESP32-S3, ESP32-P4). The ESP32 + routes SDMMC through fixed pins and ignores the pins in `SdmmcConfig`; the S3 + and P4 use the GPIO matrix. +- SDSPI runs the card in 1-bit SPI mode at up to 20 MHz; SDMMC 4-bit at 40 MHz is + several times faster. +- `format_if_mount_failed` and `format()` erase the card. They use FatFs's + `f_mkfs` on the card's own drive. +- One `SdCard` per card; the object is not copyable. diff --git a/components/sdcard/example/CMakeLists.txt b/components/sdcard/example/CMakeLists.txt new file mode 100644 index 0000000000..22c8aa831e --- /dev/null +++ b/components/sdcard/example/CMakeLists.txt @@ -0,0 +1,22 @@ +# The following lines of boilerplate have to be in your project's CMakeLists +# in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.20) + +set(ENV{IDF_COMPONENT_MANAGER} "0") +include($ENV{IDF_PATH}/tools/cmake/project.cmake) + +# add the component directories that we want to use +set(EXTRA_COMPONENT_DIRS + "../../../components/" +) + +set( + COMPONENTS + "main esptool_py base_component format logger sdcard" + CACHE STRING + "List of components to include" + ) + +project(sdcard_example) + +set(CMAKE_CXX_STANDARD 20) diff --git a/components/sdcard/example/README.md b/components/sdcard/example/README.md new file mode 100644 index 0000000000..cf620638c7 --- /dev/null +++ b/components/sdcard/example/README.md @@ -0,0 +1,52 @@ +# SD Card Example + +Brings up a microSD card with `espp::SdCard` over **SDMMC (SDIO)** or **SPI** +and shows the two-step model the component adds on top of ESP-IDF: the card is +probed once, and its FAT volume is mounted and unmounted while the card stays +initialized. It records a boot counter on the card, lists the files, unmounts, +and mounts again. + +## How to use example + +### Hardware Required + +An ESP32-S3 (or ESP32 / ESP32-P4) with a microSD slot. The default pins are the +LilyGo T-Dongle-S3's slot (SDMMC, 4-bit). Change the interface and pins under +`idf.py menuconfig` → *SD Card Example Configuration*; an SDSPI card only needs +MOSI / MISO / SCLK / CS. On the ESP32-P4 leave the LDO channel at 4 (the chip +powers its SD pads from that LDO). + +The card must carry a FAT filesystem unless you enable *Format the card if it +has no FAT filesystem* (which erases it). + +### Build and Flash + +Run `idf.py -p PORT flash monitor` to build, flash and monitor the project. + +(To exit the serial monitor, type ``Ctrl-]``.) + +See the Getting Started Guide for full steps to configure and use ESP-IDF to build projects. + +## Handing the card to a USB host + +The point of the split is that the card can be used by something other than the +firmware's FAT mount. With `espp::UsbDevice`'s MSC function the same card +becomes a USB drive; the device mounts it at its own path while the application +owns it, and unmounts it while the PC has it: + +```cpp +espp::SdCard::Config config; +config.mount_on_initialize = false; // the MSC function mounts / unmounts it +config.interface = sdmmc; // as above +espp::SdCard sdcard(config); +sdcard.initialize(); + +espp::UsbDevice::MscMedium medium; +medium.type = espp::UsbDevice::MscMedium::Type::SdCard; +medium.sd_card = sdcard.card(); +medium.base_path = "/sdcard"; +espp::UsbDevice::MscFunction msc; +msc.media = {medium}; +``` + +See the `usb_device` component's `msc_example` for the rest. diff --git a/components/sdcard/example/main/CMakeLists.txt b/components/sdcard/example/main/CMakeLists.txt new file mode 100644 index 0000000000..f62aafeb1a --- /dev/null +++ b/components/sdcard/example/main/CMakeLists.txt @@ -0,0 +1,5 @@ +idf_component_register( + SRC_DIRS "." + INCLUDE_DIRS "." + REQUIRES sdcard logger +) diff --git a/components/sdcard/example/main/Kconfig.projbuild b/components/sdcard/example/main/Kconfig.projbuild new file mode 100644 index 0000000000..979d056033 --- /dev/null +++ b/components/sdcard/example/main/Kconfig.projbuild @@ -0,0 +1,70 @@ +menu "SD Card Example Configuration" + + choice SDCARD_EXAMPLE_INTERFACE + prompt "Card interface" + default SDCARD_EXAMPLE_INTERFACE_SDMMC + help + How the card is wired: the dedicated SDMMC (SDIO) peripheral, or a + plain SPI bus (SDSPI). The default pins are the LilyGo T-Dongle-S3's + microSD slot (SDMMC, 4-bit); change them for your board. + + config SDCARD_EXAMPLE_INTERFACE_SDMMC + bool "SDMMC (SDIO)" + depends on SOC_SDMMC_HOST_SUPPORTED + config SDCARD_EXAMPLE_INTERFACE_SPI + bool "SPI (SDSPI)" + endchoice + + if SDCARD_EXAMPLE_INTERFACE_SDMMC + config SDCARD_EXAMPLE_SDMMC_BUS_WIDTH + int "Bus width (1 or 4)" + range 1 4 + default 4 + config SDCARD_EXAMPLE_SDMMC_CLK + int "CLK GPIO" + default 12 + config SDCARD_EXAMPLE_SDMMC_CMD + int "CMD GPIO" + default 16 + config SDCARD_EXAMPLE_SDMMC_D0 + int "D0 GPIO" + default 14 + config SDCARD_EXAMPLE_SDMMC_D1 + int "D1 GPIO (4-bit only)" + default 17 + config SDCARD_EXAMPLE_SDMMC_D2 + int "D2 GPIO (4-bit only)" + default 21 + config SDCARD_EXAMPLE_SDMMC_D3 + int "D3 GPIO (4-bit only)" + default 18 + config SDCARD_EXAMPLE_SDMMC_LDO_CHANNEL + int "On-chip LDO channel powering the card (-1 = none)" + default 4 if IDF_TARGET_ESP32P4 + default -1 + help + The ESP32-P4 powers its SD pads from LDO channel 4 (LDO_VO4). + endif + + if SDCARD_EXAMPLE_INTERFACE_SPI + config SDCARD_EXAMPLE_SPI_MOSI + int "MOSI GPIO" + default 16 + config SDCARD_EXAMPLE_SPI_MISO + int "MISO GPIO" + default 14 + config SDCARD_EXAMPLE_SPI_SCLK + int "SCLK GPIO" + default 12 + config SDCARD_EXAMPLE_SPI_CS + int "CS GPIO" + default 18 + endif + + config SDCARD_EXAMPLE_FORMAT_IF_MOUNT_FAILED + bool "Format the card if it has no FAT filesystem" + default n + help + Erases the card. Off by default so an unknown card is never wiped. + +endmenu diff --git a/components/sdcard/example/main/sdcard_example.cpp b/components/sdcard/example/main/sdcard_example.cpp new file mode 100644 index 0000000000..c5dcf55f66 --- /dev/null +++ b/components/sdcard/example/main/sdcard_example.cpp @@ -0,0 +1,122 @@ +// SD card example. +// +// Brings up a microSD card with espp::SdCard over SDMMC (SDIO) or SPI, then +// shows the two-step model the component adds on top of ESP-IDF: the card is +// probed once, and its FAT volume can be mounted and unmounted any number of +// times while the card stays initialized (which is what lets the same card be +// handed to a USB host, see the README). +// +// The card must already carry a FAT filesystem unless +// CONFIG_SDCARD_EXAMPLE_FORMAT_IF_MOUNT_FAILED is enabled. + +#include +#include +#include +#include +#include + +#include "sdkconfig.h" + +#include "logger.hpp" +#include "sdcard.hpp" + +using namespace std::chrono_literals; + +static void list_files(espp::Logger &logger, const std::string &path) { + std::error_code ec; + logger.info("Files in {}:", path); + size_t count = 0; + for (const auto &entry : std::filesystem::directory_iterator(path, ec)) { + std::error_code size_ec; + const bool dir = entry.is_directory(size_ec); + const auto size = dir ? 0 : entry.file_size(size_ec); + logger.info(" {}{} ({} bytes)", entry.path().filename().string(), dir ? "/" : "", size); + if (++count >= 20) { + logger.info(" ..."); + break; + } + } + if (ec) + logger.error("could not list {}: {}", path, ec.message()); +} + +extern "C" void app_main(void) { + espp::Logger logger({.tag = "SD Card", .level = espp::Logger::Verbosity::INFO}); + logger.info("Starting SD card example"); + + //! [sdcard example] + espp::SdCard::Config config; + config.mount_point = "/sdcard"; +#ifdef CONFIG_SDCARD_EXAMPLE_FORMAT_IF_MOUNT_FAILED + config.format_if_mount_failed = true; // erases a card with no FAT filesystem +#endif + config.log_level = espp::Logger::Verbosity::INFO; +#if CONFIG_SDCARD_EXAMPLE_INTERFACE_SDMMC + espp::SdCard::SdmmcConfig sdmmc; + sdmmc.bus_width = CONFIG_SDCARD_EXAMPLE_SDMMC_BUS_WIDTH; + sdmmc.clk = static_cast(CONFIG_SDCARD_EXAMPLE_SDMMC_CLK); + sdmmc.cmd = static_cast(CONFIG_SDCARD_EXAMPLE_SDMMC_CMD); + sdmmc.d0 = static_cast(CONFIG_SDCARD_EXAMPLE_SDMMC_D0); + sdmmc.d1 = static_cast(CONFIG_SDCARD_EXAMPLE_SDMMC_D1); + sdmmc.d2 = static_cast(CONFIG_SDCARD_EXAMPLE_SDMMC_D2); + sdmmc.d3 = static_cast(CONFIG_SDCARD_EXAMPLE_SDMMC_D3); + sdmmc.ldo_channel = CONFIG_SDCARD_EXAMPLE_SDMMC_LDO_CHANNEL; + config.interface = sdmmc; +#else + espp::SdCard::SpiConfig spi; + spi.host = SPI2_HOST; + spi.initialize_bus = true; // nothing else is on this bus + spi.mosi = static_cast(CONFIG_SDCARD_EXAMPLE_SPI_MOSI); + spi.miso = static_cast(CONFIG_SDCARD_EXAMPLE_SPI_MISO); + spi.sclk = static_cast(CONFIG_SDCARD_EXAMPLE_SPI_SCLK); + spi.cs = static_cast(CONFIG_SDCARD_EXAMPLE_SPI_CS); + config.interface = spi; +#endif + + espp::SdCard sdcard(config); + std::error_code ec; + if (!sdcard.initialize(ec)) { // probes the card and mounts it at /sdcard + logger.error("SD card initialization failed: {}", ec.message()); + return; + } + //! [sdcard example] + + if (const auto card = sdcard.card_info()) { + logger.info("Card '{}': {} MiB, {} kHz, {}-bit, {}", card->name, + card->capacity_bytes / (1024 * 1024), card->frequency_khz, card->bus_width, + card->high_capacity ? "SDHC/SDXC" : "SDSC"); + } + if (const auto volume = sdcard.volume_info()) { + logger.info("Volume: {} MiB total, {} MiB free", volume->total_bytes / (1024 * 1024), + volume->free_bytes / (1024 * 1024)); + } + + // Ordinary file I/O while the volume is mounted. + const std::string counter_path = sdcard.mount_point() + "/boots.txt"; + int boots = 0; + if (std::ifstream in(counter_path); in) + in >> boots; + ++boots; + if (std::ofstream out(counter_path, std::ios::trunc); out) + out << boots << "\n"; + else + logger.error("could not write {}", counter_path); + logger.info("boot #{} recorded", boots); + list_files(logger, sdcard.mount_point()); + + // The card stays initialized while its volume is unmounted: this is the window + // in which another owner (e.g. a USB host through espp::UsbDevice's MSC + // function) may use sdcard.card() directly. + if (!sdcard.unmount(ec)) + logger.error("unmount failed: {}", ec.message()); + logger.info("volume unmounted; the card is still initialized: {}", sdcard.is_initialized()); + std::this_thread::sleep_for(1s); + if (!sdcard.mount(ec)) + logger.error("mount failed: {}", ec.message()); + else + logger.info("volume mounted again; boots.txt still says {}", boots); + + logger.info("Done. The card stays mounted at {}.", sdcard.mount_point()); + while (true) + std::this_thread::sleep_for(1s); +} diff --git a/components/sdcard/example/sdkconfig.defaults b/components/sdcard/example/sdkconfig.defaults new file mode 100644 index 0000000000..76173fc82c --- /dev/null +++ b/components/sdcard/example/sdkconfig.defaults @@ -0,0 +1,9 @@ +# Default pins are the LilyGo T-Dongle-S3's microSD slot (SDMMC, 4-bit); the +# card interface and pins are set under "SD Card Example Configuration". +CONFIG_IDF_TARGET="esp32s3" + +CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192 + +# Long file names on the heap, and volume labels +CONFIG_FATFS_LFN_HEAP=y +CONFIG_FATFS_USE_LABEL=y diff --git a/components/sdcard/idf_component.yml b/components/sdcard/idf_component.yml new file mode 100644 index 0000000000..6fa298c1ce --- /dev/null +++ b/components/sdcard/idf_component.yml @@ -0,0 +1,25 @@ +## IDF Component Manager Manifest File +license: "MIT" +description: "SD / microSD card over SDSPI or SDMMC (SDIO) with card initialization and FAT mounting as separate steps, for ESP-IDF" +url: "https://github.com/esp-cpp/espp/tree/main/components/sdcard" +repository: "git://github.com/esp-cpp/espp.git" +maintainers: + - William Emfinger +documentation: "https://esp-cpp.github.io/espp/storage/sdcard.html" +examples: + - path: example +tags: + - cpp + - Component + - SD + - SD-Card + - microSD + - SDMMC + - SDIO + - SDSPI + - FAT + - Storage +dependencies: + idf: + version: '>=5.0' + espp/base_component: '>=1.0' diff --git a/components/sdcard/include/sdcard.hpp b/components/sdcard/include/sdcard.hpp new file mode 100644 index 0000000000..ee72e9d1e9 --- /dev/null +++ b/components/sdcard/include/sdcard.hpp @@ -0,0 +1,285 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#if SOC_SDMMC_HOST_SUPPORTED +#include +#endif + +#include "base_component.hpp" + +namespace espp { + +/** + * @brief SD / microSD card over SDSPI or SDMMC (SDIO), with card + * initialization and FAT mounting as two separate steps. + * + * @details ESP-IDF's convenience functions (`esp_vfs_fat_sdspi_mount()` / + * `esp_vfs_fat_sdmmc_mount()`) initialize the card and mount its FAT volume in + * one call, and own the card for as long as it is mounted. That is fine for a + * board that only ever reads its own card, but not when something else needs the + * raw card: USB mass storage (`espp::UsbDevice`'s MSC function) hands the card to + * a PC, which must not happen while the firmware has the volume mounted. + * + * `SdCard` therefore separates the two: + * + * - `initialize()` brings up the host (an SPI bus device, or an SDMMC slot) and + * probes the card: afterwards `card()` is a valid `sdmmc_card_t` usable for raw + * sector access or for handing to another owner. By default it also mounts. + * - `mount()` / `unmount()` register / unregister the card's FAT volume at + * `Config::mount_point`, so the card can move between the application's VFS and + * another user (USB host) any number of times without re-probing it. + * + * Both interfaces are configured through one `Config` and selected with a + * `std::variant`: + * + * - `SpiConfig`: the card on an SPI bus (any target). The bus may already be + * initialized by the application / BSP (shared with a display, a radio, ...) or + * the component can initialize and later free it. + * - `SdmmcConfig`: the dedicated SDMMC peripheral (ESP32, ESP32-S3, ESP32-P4), 1- + * or 4-bit, with the pins routed through the GPIO matrix on targets that support + * it, and an optional on-chip LDO channel powering the card (ESP32-P4). + * + * \section sdcard_ex1 SdCard Example + * \snippet sdcard_example.cpp sdcard example + */ +class SdCard : public BaseComponent { +public: + /// @brief The card is attached to an SPI bus (SDSPI). Works on every target. + struct SpiConfig { + spi_host_device_t host{SPI2_HOST}; /**< SPI peripheral the card is on. */ + gpio_num_t cs{GPIO_NUM_NC}; /**< Card chip-select pin. */ + /** Initialize the SPI bus (mosi / miso / sclk below) in initialize() and free it + * in deinitialize(). Leave false when the application or BSP already owns + * the bus (e.g. it is shared with a display), in which case only `host` and + * `cs` are used. */ + bool initialize_bus{false}; + gpio_num_t mosi{GPIO_NUM_NC}; /**< Bus MOSI (only with initialize_bus). */ + gpio_num_t miso{GPIO_NUM_NC}; /**< Bus MISO (only with initialize_bus). */ + gpio_num_t sclk{GPIO_NUM_NC}; /**< Bus SCLK (only with initialize_bus). */ + /** Largest transfer the bus will carry, in bytes (only with initialize_bus). + * Multi-sector reads need at least the sector size (512). */ + int max_transfer_size{4092}; + /** SPI clock while talking to the card, in kHz. SDSPI supports 400 kHz up + * to 20 MHz (SDMMC_FREQ_DEFAULT). */ + int frequency_khz{SDMMC_FREQ_DEFAULT}; + gpio_num_t card_detect{GPIO_NUM_NC}; /**< Card-detect input, if wired. */ + gpio_num_t write_protect{GPIO_NUM_NC}; /**< Write-protect input, if wired. */ + }; + + /// @brief The card is on the SDMMC (SDIO) peripheral: ESP32, ESP32-S3, ESP32-P4. + struct SdmmcConfig { + /** SDMMC slot. ESP32: slot 0 (8-bit capable, shares pins with flash on some + * modules) or slot 1 (4-bit, the usual choice); ESP32-S3 / -P4: any slot, + * pins are routed through the GPIO matrix. */ + int slot{1}; + uint8_t bus_width{4}; /**< Data bus width: 1 or 4. */ + /** Pins (targets with SOC_SDMMC_USE_GPIO_MATRIX only, e.g. ESP32-S3 / -P4; + * the ESP32 uses its fixed slot pins and ignores these). GPIO_NUM_NC keeps + * the slot's default pin. d1..d3 are unused with bus_width 1. */ + gpio_num_t clk{GPIO_NUM_NC}; + gpio_num_t cmd{GPIO_NUM_NC}; + gpio_num_t d0{GPIO_NUM_NC}; + gpio_num_t d1{GPIO_NUM_NC}; + gpio_num_t d2{GPIO_NUM_NC}; + gpio_num_t d3{GPIO_NUM_NC}; + /** Bus clock in kHz: SDMMC_FREQ_DEFAULT (20 MHz), SDMMC_FREQ_HIGHSPEED + * (40 MHz), or SDMMC_FREQ_PROBING (400 kHz) for marginal wiring. */ + int frequency_khz{SDMMC_FREQ_HIGHSPEED}; + gpio_num_t card_detect{GPIO_NUM_NC}; /**< Card-detect input, if wired. */ + gpio_num_t write_protect{GPIO_NUM_NC}; /**< Write-protect input, if wired. */ + /** On-chip LDO channel that powers the card's IO rail, or -1 if the card is + * powered externally. The ESP32-P4 feeds the SD pads from LDO_VO4 (channel + * 4): without it the bus floats and the card never answers. */ + int ldo_channel{-1}; + }; + + /// @brief Configuration for the SdCard. + struct Config { + /** Which interface the card is on and how it is wired. */ + std::variant interface { + SpiConfig {} + }; + std::string mount_point{"/sdcard"}; /**< VFS path the FAT volume is mounted at. */ + /** Mount the FAT volume at the end of initialize(). Leave false when the card + * is first going elsewhere (e.g. to a USB host) and call mount() later. */ + bool mount_on_initialize{true}; + /** If the card has no FAT filesystem, create one when mounting (this erases + * whatever is on the card). Off by default: mount() then fails with + * `std::errc::no_such_device` and format() is available. */ + bool format_if_mount_failed{false}; + int max_files{5}; /**< Files the application may keep open at once. */ + /** FAT allocation unit (cluster) size in bytes used when the card is formatted; + * 0 = FatFs picks one from the card size. Larger clusters make big files + * faster and small files wasteful. */ + size_t allocation_unit_size{16 * 1024}; + /** Ask the card for its status before every FAT operation, so a card removed + * while mounted is noticed instead of returning stale data; costs a command + * per operation. */ + bool disk_status_check{false}; + espp::Logger::Verbosity log_level{espp::Logger::Verbosity::WARN}; /**< Logger verbosity. */ + }; + + /// @brief Which interface a configured card uses. + enum class Interface : uint8_t { Spi, Sdmmc }; + + /// @brief What the card reported about itself at initialize(). + struct CardInfo { + std::string name; ///< Product name from the card's CID register. + uint64_t capacity_bytes{0}; ///< Total capacity. + uint32_t sector_size{0}; ///< Bytes per sector (512 for every SD card). + uint32_t sector_count{0}; ///< Number of sectors. + uint32_t frequency_khz{0}; ///< Bus clock actually in use. + uint8_t bus_width{1}; ///< Data lines in use (SDSPI: 1). + bool high_capacity{false}; ///< SDHC / SDXC (block addressing). + bool is_mmc{false}; ///< An (e)MMC device rather than an SD card. + Interface interface { Interface::Spi }; ///< The interface it is on. + }; + + /// @brief Space on the mounted FAT volume. + struct VolumeInfo { + uint64_t total_bytes{0}; ///< Volume size. + uint64_t free_bytes{0}; ///< Unallocated space. + }; + + /** + * @brief Construct the component. Does not touch hardware until initialize(). + * @param config Configuration. + */ + explicit SdCard(const Config &config); + + /// @brief Unmounts the volume (if mounted) and releases the card, host and, + /// when the component initialized it, the SPI bus. + ~SdCard(); + + SdCard(const SdCard &) = delete; + SdCard &operator=(const SdCard &) = delete; + + /** + * @brief Bring up the host (SPI device / SDMMC slot, LDO), probe the card and, + * with Config::mount_on_initialize, mount its FAT volume. + * @param[out] ec Set on failure: invalid configuration (`invalid_argument`), + * the host could not be initialized (`io_error`), no card answered + * (`no_such_device` -- check the card, the wiring and the pull-ups), or a + * mount failure (see mount()). Nothing stays initialized on failure. + * @return true on success. + */ + bool initialize(std::error_code &ec); + + /// @brief Convenience overload of initialize() that ignores errors. + bool initialize(); + + /** + * @brief Mount the card's FAT volume at Config::mount_point. + * @param[out] ec Set on failure: not initialized (`not_connected`), every FatFs + * drive slot in use (`device_or_resource_busy` -- raise + * CONFIG_FATFS_VOLUME_COUNT), no FAT filesystem on the card and + * Config::format_if_mount_failed off (`no_such_device` -- see format()), + * or the mount / VFS registration failed (`io_error`). + * @return true if the volume is mounted (also when it already was). + */ + bool mount(std::error_code &ec); + + /// @brief Convenience overload of mount() that ignores errors. + bool mount(); + + /** + * @brief Unmount the FAT volume, releasing Config::mount_point. Files still + * open there become invalid. The card stays initialized: card() remains + * valid and mount() may be called again. + * @param[out] ec Set on failure (`not_connected` if not initialized). + * @return true if the volume is unmounted (also when it already was). + */ + bool unmount(std::error_code &ec); + + /// @brief Convenience overload of unmount() that ignores errors. + bool unmount(); + + /** + * @brief Create a fresh FAT filesystem on the card (erasing everything on it), + * using Config::allocation_unit_size. The volume is unmounted first if + * it was mounted, and mounted again afterwards. + * @param[out] ec Set on failure (`not_connected` if not initialized, else + * `io_error`). + * @return true if the card was formatted. + */ + bool format(std::error_code &ec); + + /// @brief Convenience overload of format() that ignores errors. + bool format(); + + /** + * @brief Release everything: unmount, detach the card from the host, delete + * the LDO handle and free the SPI bus if the component initialized it. + * The destructor calls this. + * @param[out] ec Set on failure (the object is still deinitialized). + * @return true on success. + */ + bool deinitialize(std::error_code &ec); + + /// @brief Convenience overload of deinitialize() that ignores errors. + bool deinitialize(); + + /// @brief Whether initialize() succeeded (the card is probed and card() is valid). + bool is_initialized() const; + + /// @brief Whether the FAT volume is currently mounted at mount_point(). + bool is_mounted() const; + + /// @brief The interface the card is configured on. + Interface interface() const; + + /// @brief The VFS path the volume is (or would be) mounted at. + const std::string &mount_point() const { return config_.mount_point; } + + /** + * @brief The initialized card, for raw sector access or to hand to another + * owner (e.g. `espp::UsbDevice::MscMedium::sd_card`). Valid from a + * successful initialize() until deinitialize(); the SdCard keeps owning + * it. nullptr when not initialized. + * @note Whoever uses the card directly must do so while the volume is NOT + * mounted here (unmount() first): FatFs and a raw writer must not share + * the card. + */ + sdmmc_card_t *card() const; + + /// @brief What the card reported at initialize(); nullopt if not initialized. + std::optional card_info() const; + + /// @brief Total / free space on the mounted volume; nullopt if not mounted. + std::optional volume_info() const; + + /// @brief Print the card's properties (what `sdmmc_card_print_info()` prints). + /// @param out Stream to print to (default stdout). + void print_info(FILE *out = stdout) const; + +protected: + bool init_host(std::error_code &ec); + void deinit_host(); + bool mount_locked(std::error_code &ec); + bool unmount_locked(std::error_code &ec); + bool format_locked(std::error_code &ec); + + Config config_; + mutable std::mutex mutex_; + bool initialized_{false}; + bool mounted_{false}; + bool bus_initialized_{false}; // we initialized the SPI bus (SpiConfig::initialize_bus) + sdmmc_host_t host_{}; // host with the (SDSPI: device handle) slot filled in + sdmmc_card_t card_{}; // the probed card (owned here) + uint8_t pdrv_{0xFF}; // FatFs drive number while mounted + void *ldo_handle_{nullptr}; // sd_pwr_ctrl_handle_t while an LDO channel is in use +}; + +} // namespace espp diff --git a/components/sdcard/src/sdcard.cpp b/components/sdcard/src/sdcard.cpp new file mode 100644 index 0000000000..e895c7abf1 --- /dev/null +++ b/components/sdcard/src/sdcard.cpp @@ -0,0 +1,519 @@ +#include "sdcard.hpp" + +#include + +#include +#include +#include // SD_OCR_SDHC_CAP +#include + +// FatFs drive plumbing: the same calls ESP-IDF's esp_vfs_fat_sd*_mount() makes +// internally, used here so the card can be probed once and mounted / unmounted +// any number of times (IDF's helpers only offer both steps together before v6.1). +#include +#include +#include + +// The on-chip LDO power control (ESP32-P4 SD pads) is only compiled by ESP-IDF on +// targets with general-purpose LDOs; the header exists everywhere. +#if defined(SOC_GP_LDO_SUPPORTED) && SOC_GP_LDO_SUPPORTED && \ + __has_include() +#include +#define ESPP_SDCARD_HAS_LDO_PWR_CTRL 1 +#else +#define ESPP_SDCARD_HAS_LDO_PWR_CTRL 0 +#endif + +namespace espp { + +namespace { +constexpr uint8_t kNoDrive = 0xFF; + +std::string fat_drive_string(uint8_t pdrv) { return std::to_string(pdrv) + ":"; } + +// Release the host the way IDF's helpers do: a host that takes its handle in +// deinit_p() (SDSPI device, and SDMMC slots on newer IDF) gets it. +void call_host_deinit(const sdmmc_host_t &host) { + if (host.flags & SDMMC_HOST_FLAG_DEINIT_ARG) { + if (host.deinit_p) + host.deinit_p(host.slot); + } else if (host.deinit) { + host.deinit(); + } +} +} // namespace + +SdCard::SdCard(const Config &config) + : BaseComponent("SdCard", config.log_level) + , config_(config) {} + +SdCard::~SdCard() { + std::error_code ec; + deinitialize(ec); +} + +bool SdCard::initialize() { + std::error_code ec; + return initialize(ec); +} + +bool SdCard::initialize(std::error_code &ec) { + ec.clear(); + std::lock_guard lock(mutex_); + if (initialized_) { + logger_.warn("Already initialized"); + return true; + } + if (config_.mount_point.size() < 2 || config_.mount_point.front() != '/') { + logger_.error("mount_point '{}' must be an absolute VFS path like '/sdcard'", + config_.mount_point); + ec = std::make_error_code(std::errc::invalid_argument); + return false; + } + if (!init_host(ec)) + return false; + + // Probe the card. sdmmc_card_init() runs the SD / MMC identification sequence + // and fills card_ (CID / CSD / OCR, bus width, real frequency). + logger_.info("Probing the card"); + std::memset(&card_, 0, sizeof(card_)); + const esp_err_t err = sdmmc_card_init(&host_, &card_); + if (err != ESP_OK) { + logger_.error("No card answered ({}): check that a card is inserted, the wiring, and the " + "pull-ups on CMD / D0-D3", + esp_err_to_name(err)); + deinit_host(); + ec = std::make_error_code(std::errc::no_such_device); + return false; + } + initialized_ = true; + logger_.info("Card '{}' ready: {} MiB, {} kHz, {}-bit", card_.cid.name, + static_cast(card_.csd.capacity) * card_.csd.sector_size / (1024 * 1024), + card_.real_freq_khz, 1u << card_.log_bus_width); + + if (config_.mount_on_initialize && !mount_locked(ec)) { + // leave nothing half-done: the caller sees a clean "not initialized" + initialized_ = false; + deinit_host(); + return false; + } + return true; +} + +bool SdCard::init_host(std::error_code &ec) { + if (const auto *spi = std::get_if(&config_.interface)) { + if (spi->cs == GPIO_NUM_NC) { + logger_.error("SpiConfig::cs is required"); + ec = std::make_error_code(std::errc::invalid_argument); + return false; + } + if (spi->initialize_bus) { + if (spi->mosi == GPIO_NUM_NC || spi->miso == GPIO_NUM_NC || spi->sclk == GPIO_NUM_NC) { + logger_.error("SpiConfig::initialize_bus needs mosi, miso and sclk"); + ec = std::make_error_code(std::errc::invalid_argument); + return false; + } + spi_bus_config_t bus{}; + bus.mosi_io_num = spi->mosi; + bus.miso_io_num = spi->miso; + bus.sclk_io_num = spi->sclk; + bus.quadwp_io_num = GPIO_NUM_NC; + bus.quadhd_io_num = GPIO_NUM_NC; + bus.max_transfer_sz = spi->max_transfer_size; + const esp_err_t err = spi_bus_initialize(spi->host, &bus, SDSPI_DEFAULT_DMA); + if (err != ESP_OK) { + logger_.error("spi_bus_initialize failed: {}", esp_err_to_name(err)); + ec = std::make_error_code(std::errc::io_error); + return false; + } + bus_initialized_ = true; + } + host_ = SDSPI_HOST_DEFAULT(); + host_.slot = spi->host; + host_.max_freq_khz = spi->frequency_khz; + sdspi_device_config_t device = SDSPI_DEVICE_CONFIG_DEFAULT(); + device.host_id = spi->host; + device.gpio_cs = spi->cs; + device.gpio_cd = spi->card_detect; + device.gpio_wp = spi->write_protect; + // host_.init() is sdspi_host_init(); the device handle it returns REPLACES + // the slot in the host struct (that is how the SDSPI host addresses the card). + esp_err_t err = host_.init ? host_.init() : ESP_OK; + sdspi_dev_handle_t handle = -1; + if (err == ESP_OK) + err = sdspi_host_init_device(&device, &handle); + if (err != ESP_OK) { + logger_.error("Could not attach the card to SPI host {} (cs {}): {}", + static_cast(spi->host), static_cast(spi->cs), esp_err_to_name(err)); + if (bus_initialized_) { + spi_bus_free(spi->host); + bus_initialized_ = false; + } + ec = std::make_error_code(std::errc::io_error); + return false; + } + host_.slot = handle; + logger_.debug("SDSPI device on host {} cs {} at {} kHz", static_cast(spi->host), + static_cast(spi->cs), spi->frequency_khz); + return true; + } + + const auto &sdmmc = std::get(config_.interface); +#if !SOC_SDMMC_HOST_SUPPORTED + (void)sdmmc; + logger_.error("This target has no SDMMC host; use SpiConfig"); + ec = std::make_error_code(std::errc::function_not_supported); + return false; +#else + if (sdmmc.bus_width != 1 && sdmmc.bus_width != 4) { + logger_.error("SdmmcConfig::bus_width must be 1 or 4, got {}", sdmmc.bus_width); + ec = std::make_error_code(std::errc::invalid_argument); + return false; + } + host_ = SDMMC_HOST_DEFAULT(); + host_.slot = sdmmc.slot; + host_.max_freq_khz = sdmmc.frequency_khz; + if (sdmmc.ldo_channel >= 0) { +#if ESPP_SDCARD_HAS_LDO_PWR_CTRL + sd_pwr_ctrl_ldo_config_t ldo{}; + ldo.ldo_chan_id = sdmmc.ldo_channel; + sd_pwr_ctrl_handle_t handle = nullptr; + const esp_err_t err = sd_pwr_ctrl_new_on_chip_ldo(&ldo, &handle); + if (err != ESP_OK) { + logger_.error("Could not power the card from LDO channel {}: {}", sdmmc.ldo_channel, + esp_err_to_name(err)); + ec = std::make_error_code(std::errc::io_error); + return false; + } + ldo_handle_ = handle; + host_.pwr_ctrl_handle = handle; +#else + logger_.error("SdmmcConfig::ldo_channel: this target has no on-chip LDO for the SD pads"); + ec = std::make_error_code(std::errc::function_not_supported); + return false; +#endif + } + sdmmc_slot_config_t slot = SDMMC_SLOT_CONFIG_DEFAULT(); + slot.width = sdmmc.bus_width; + slot.gpio_cd = sdmmc.card_detect; + slot.gpio_wp = sdmmc.write_protect; +#if SOC_SDMMC_USE_GPIO_MATRIX + if (sdmmc.clk != GPIO_NUM_NC) + slot.clk = sdmmc.clk; + if (sdmmc.cmd != GPIO_NUM_NC) + slot.cmd = sdmmc.cmd; + if (sdmmc.d0 != GPIO_NUM_NC) + slot.d0 = sdmmc.d0; + if (sdmmc.d1 != GPIO_NUM_NC) + slot.d1 = sdmmc.d1; + if (sdmmc.d2 != GPIO_NUM_NC) + slot.d2 = sdmmc.d2; + if (sdmmc.d3 != GPIO_NUM_NC) + slot.d3 = sdmmc.d3; +#else + if (sdmmc.clk != GPIO_NUM_NC || sdmmc.cmd != GPIO_NUM_NC || sdmmc.d0 != GPIO_NUM_NC) + logger_.warn("This target routes SDMMC through fixed pins; the configured pins are ignored"); +#endif + esp_err_t err = host_.init ? host_.init() : ESP_OK; // sdmmc_host_init() + if (err == ESP_OK) + err = sdmmc_host_init_slot(sdmmc.slot, &slot); + if (err != ESP_OK) { + logger_.error("Could not initialize SDMMC slot {}: {}", sdmmc.slot, esp_err_to_name(err)); + if (host_.deinit && err != ESP_ERR_INVALID_STATE) + host_.deinit(); // the slot init failed: release the host we just brought up +#if ESPP_SDCARD_HAS_LDO_PWR_CTRL + if (ldo_handle_) { + sd_pwr_ctrl_del_on_chip_ldo(static_cast(ldo_handle_)); + ldo_handle_ = nullptr; + } +#endif + ec = std::make_error_code(std::errc::io_error); + return false; + } + logger_.debug("SDMMC slot {} {}-bit at {} kHz", sdmmc.slot, sdmmc.bus_width, sdmmc.frequency_khz); + return true; +#endif // SOC_SDMMC_HOST_SUPPORTED +} + +void SdCard::deinit_host() { + call_host_deinit(host_); +#if ESPP_SDCARD_HAS_LDO_PWR_CTRL + if (ldo_handle_) { + sd_pwr_ctrl_del_on_chip_ldo(static_cast(ldo_handle_)); + ldo_handle_ = nullptr; + } +#endif + if (bus_initialized_) { + spi_bus_free(std::get(config_.interface).host); + bus_initialized_ = false; + } + host_ = sdmmc_host_t{}; +} + +bool SdCard::mount() { + std::error_code ec; + return mount(ec); +} + +bool SdCard::mount(std::error_code &ec) { + ec.clear(); + std::lock_guard lock(mutex_); + if (!initialized_) { + ec = std::make_error_code(std::errc::not_connected); + return false; + } + return mount_locked(ec); +} + +bool SdCard::mount_locked(std::error_code &ec) { + if (mounted_) + return true; + uint8_t pdrv = kNoDrive; + if (ff_diskio_get_drive(&pdrv) != ESP_OK || pdrv == kNoDrive) { + logger_.error("Every FatFs drive slot is in use (raise CONFIG_FATFS_VOLUME_COUNT)"); + ec = std::make_error_code(std::errc::device_or_resource_busy); + return false; + } + ff_diskio_register_sdmmc(pdrv, &card_); + ff_sdmmc_set_disk_status_check(pdrv, config_.disk_status_check); + const std::string drive = fat_drive_string(pdrv); + + FATFS *fs = nullptr; + esp_err_t err; + { +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) + esp_vfs_fat_conf_t conf{}; + conf.base_path = config_.mount_point.c_str(); + conf.fat_drive = drive.c_str(); + conf.max_files = static_cast(config_.max_files); +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) + err = esp_vfs_fat_register(&conf, &fs); +#else + err = esp_vfs_fat_register_cfg(&conf, &fs); +#endif +#else + err = esp_vfs_fat_register(config_.mount_point.c_str(), drive.c_str(), config_.max_files, &fs); +#endif + } + if (err != ESP_OK) { + logger_.error("Could not register '{}' with the VFS: {}", config_.mount_point, + esp_err_to_name(err)); + ff_diskio_unregister(pdrv); + ec = std::make_error_code(std::errc::io_error); + return false; + } + + FRESULT res = f_mount(fs, drive.c_str(), 1); + if (res == FR_NO_FILESYSTEM || res == FR_INT_ERR) { + if (!config_.format_if_mount_failed) { + logger_.error("No FAT filesystem on the card (format() it, or set " + "format_if_mount_failed)"); + esp_vfs_fat_unregister_path(config_.mount_point.c_str()); + ff_diskio_unregister(pdrv); + ec = std::make_error_code(std::errc::no_such_device); + return false; + } + logger_.warn("No FAT filesystem on the card; formatting it"); + pdrv_ = pdrv; + if (!format_locked(ec)) { + pdrv_ = kNoDrive; + esp_vfs_fat_unregister_path(config_.mount_point.c_str()); + ff_diskio_unregister(pdrv); + return false; + } + pdrv_ = kNoDrive; + res = f_mount(fs, drive.c_str(), 1); + } + if (res != FR_OK) { + logger_.error("Mounting the card failed (FatFs result {})", static_cast(res)); + esp_vfs_fat_unregister_path(config_.mount_point.c_str()); + ff_diskio_unregister(pdrv); + ec = std::make_error_code(std::errc::io_error); + return false; + } + pdrv_ = pdrv; + mounted_ = true; + logger_.info("Mounted at '{}'", config_.mount_point); + return true; +} + +bool SdCard::unmount() { + std::error_code ec; + return unmount(ec); +} + +bool SdCard::unmount(std::error_code &ec) { + ec.clear(); + std::lock_guard lock(mutex_); + if (!initialized_) { + ec = std::make_error_code(std::errc::not_connected); + return false; + } + return unmount_locked(ec); +} + +bool SdCard::unmount_locked(std::error_code &ec) { + if (!mounted_) + return true; + const std::string drive = fat_drive_string(pdrv_); + f_mount(nullptr, drive.c_str(), 0); + ff_diskio_unregister(pdrv_); + const esp_err_t err = esp_vfs_fat_unregister_path(config_.mount_point.c_str()); + pdrv_ = kNoDrive; + mounted_ = false; + if (err != ESP_OK) { + logger_.warn("Unregistering '{}' from the VFS failed: {}", config_.mount_point, + esp_err_to_name(err)); + ec = std::make_error_code(std::errc::io_error); + return false; + } + logger_.info("Unmounted '{}'", config_.mount_point); + return true; +} + +bool SdCard::format() { + std::error_code ec; + return format(ec); +} + +bool SdCard::format(std::error_code &ec) { + ec.clear(); + std::lock_guard lock(mutex_); + if (!initialized_) { + ec = std::make_error_code(std::errc::not_connected); + return false; + } + const bool was_mounted = mounted_; + if (was_mounted) { + // keep the drive registered (pdrv_) but drop the FatFs mount for f_mkfs + const std::string drive = fat_drive_string(pdrv_); + f_mount(nullptr, drive.c_str(), 0); + } else { + uint8_t pdrv = kNoDrive; + if (ff_diskio_get_drive(&pdrv) != ESP_OK || pdrv == kNoDrive) { + logger_.error("Every FatFs drive slot is in use (raise CONFIG_FATFS_VOLUME_COUNT)"); + ec = std::make_error_code(std::errc::device_or_resource_busy); + return false; + } + ff_diskio_register_sdmmc(pdrv, &card_); + pdrv_ = pdrv; + } + const bool ok = format_locked(ec); + if (was_mounted) { + // mount again on the still-registered drive; a failure here leaves the + // volume unmounted (reported through ec) + mounted_ = false; + const uint8_t pdrv = pdrv_; + pdrv_ = kNoDrive; + esp_vfs_fat_unregister_path(config_.mount_point.c_str()); + ff_diskio_unregister(pdrv); + std::error_code mount_ec; + if (!mount_locked(mount_ec) && !ec) + ec = mount_ec; + } else { + ff_diskio_unregister(pdrv_); + pdrv_ = kNoDrive; + } + return ok && !ec; +} + +bool SdCard::format_locked(std::error_code &ec) { + // pdrv_ must be registered (not necessarily mounted) when this runs + const std::string drive = fat_drive_string(pdrv_); + constexpr size_t kWorkBufferSize = 4096; + void *work = ff_memalloc(kWorkBufferSize); + if (!work) { + ec = std::make_error_code(std::errc::not_enough_memory); + return false; + } + // FM_ANY: FatFs picks FAT12/16/32 (or exFAT if enabled) from the card size and + // creates an MBR partition (no FM_SFD), like ESP-IDF's own SD formatting. + MKFS_PARM opt{}; + opt.fmt = FM_ANY; + opt.au_size = config_.allocation_unit_size; + logger_.info("Formatting the card (allocation unit {} bytes)", config_.allocation_unit_size); + const FRESULT res = f_mkfs(drive.c_str(), &opt, work, kWorkBufferSize); + ff_memfree(work); + if (res != FR_OK) { + logger_.error("Formatting failed (FatFs result {})", static_cast(res)); + ec = std::make_error_code(std::errc::io_error); + return false; + } + return true; +} + +bool SdCard::deinitialize() { + std::error_code ec; + return deinitialize(ec); +} + +bool SdCard::deinitialize(std::error_code &ec) { + ec.clear(); + std::lock_guard lock(mutex_); + if (!initialized_) + return true; + std::error_code unmount_ec; + unmount_locked(unmount_ec); + deinit_host(); + initialized_ = false; + std::memset(&card_, 0, sizeof(card_)); + if (unmount_ec) + ec = unmount_ec; + logger_.info("Deinitialized"); + return !ec; +} + +bool SdCard::is_initialized() const { + std::lock_guard lock(mutex_); + return initialized_; +} + +bool SdCard::is_mounted() const { + std::lock_guard lock(mutex_); + return mounted_; +} + +SdCard::Interface SdCard::interface() const { + return std::holds_alternative(config_.interface) ? Interface::Spi : Interface::Sdmmc; +} + +sdmmc_card_t *SdCard::card() const { + std::lock_guard lock(mutex_); + // the card lives in this object; the pointer is stable for the object's life + return initialized_ ? const_cast(&card_) : nullptr; +} + +std::optional SdCard::card_info() const { + std::lock_guard lock(mutex_); + if (!initialized_) + return std::nullopt; + CardInfo info; + info.name.assign(card_.cid.name, strnlen(card_.cid.name, sizeof(card_.cid.name))); + info.sector_size = static_cast(card_.csd.sector_size); + info.sector_count = static_cast(card_.csd.capacity); + info.capacity_bytes = static_cast(info.sector_count) * info.sector_size; + info.frequency_khz = static_cast(card_.real_freq_khz); + info.bus_width = static_cast(1u << card_.log_bus_width); + info.high_capacity = (card_.ocr & SD_OCR_SDHC_CAP) != 0; + info.is_mmc = card_.is_mmc != 0; + info.interface = interface(); + return info; +} + +std::optional SdCard::volume_info() const { + std::lock_guard lock(mutex_); + if (!mounted_) + return std::nullopt; + VolumeInfo info; + if (esp_vfs_fat_info(config_.mount_point.c_str(), &info.total_bytes, &info.free_bytes) != ESP_OK) + return std::nullopt; + return info; +} + +void SdCard::print_info(FILE *out) const { + std::lock_guard lock(mutex_); + if (initialized_) + sdmmc_card_print_info(out, &card_); +} + +} // namespace espp diff --git a/doc/Doxyfile b/doc/Doxyfile index f6cd973ead..00285d056e 100755 --- a/doc/Doxyfile +++ b/doc/Doxyfile @@ -173,6 +173,7 @@ EXAMPLE_PATH = \ $(PROJECT_PATH)/components/rmt/example/main/rmt_example.cpp \ $(PROJECT_PATH)/components/rtps/example/main/rtps_example.cpp \ $(PROJECT_PATH)/components/rtsp/example/main/rtsp_example.cpp \ + $(PROJECT_PATH)/components/sdcard/example/main/sdcard_example.cpp \ $(PROJECT_PATH)/components/ping/example/main/ping_example.cpp \ $(PROJECT_PATH)/components/runqueue/example/main/runqueue_example.cpp \ $(PROJECT_PATH)/components/rx8130ce/example/main/rx8130ce_example.cpp \ @@ -442,6 +443,7 @@ INPUT = \ $(PROJECT_PATH)/components/rtsp/include/rtsp_client.hpp \ $(PROJECT_PATH)/components/rtsp/include/rtsp_server.hpp \ $(PROJECT_PATH)/components/rtsp/include/rtsp_session.hpp \ + $(PROJECT_PATH)/components/sdcard/include/sdcard.hpp \ $(PROJECT_PATH)/components/runqueue/include/runqueue.hpp \ $(PROJECT_PATH)/components/rx8130ce/include/rx8130ce.hpp \ $(PROJECT_PATH)/components/serialization/include/serialization.hpp \ diff --git a/doc/en/storage/index.rst b/doc/en/storage/index.rst index 5e186758b7..63829759d0 100644 --- a/doc/en/storage/index.rst +++ b/doc/en/storage/index.rst @@ -9,3 +9,4 @@ storage. file_system nvs + sdcard diff --git a/doc/en/storage/sdcard.rst b/doc/en/storage/sdcard.rst new file mode 100644 index 0000000000..7ffccd33ac --- /dev/null +++ b/doc/en/storage/sdcard.rst @@ -0,0 +1,52 @@ +SD Card +******* + +The ``espp::SdCard`` component brings up an SD / microSD card over **SDSPI** (any +target) or the **SDMMC (SDIO)** peripheral (ESP32, ESP32-S3, ESP32-P4), and keeps +card initialization and FAT mounting as two separate steps. + +ESP-IDF's convenience functions (``esp_vfs_fat_sdspi_mount()`` / +``esp_vfs_fat_sdmmc_mount()``) probe the card and mount its FAT volume in one +call, and own the card for as long as it is mounted. ``SdCard`` instead probes the +card in ``initialize()`` and mounts / unmounts the volume with ``mount()`` / +``unmount()``, so the same card can be handed to another user -- a USB host +through ``espp::UsbDevice``'s MSC function -- and taken back without re-probing it. + +Interface configuration +----------------------- + +``Config::interface`` is a ``std::variant`` of: + +- ``SpiConfig``: the SPI host and chip-select pin; optionally the bus pins, when + the component should initialize (and later free) the bus rather than share one + the application or BSP already owns. +- ``SdmmcConfig``: the slot, 1- or 4-bit width, the pins (routed through the + GPIO matrix on targets that support it), the bus clock, and an optional on-chip + LDO channel that powers the card (the ESP32-P4 feeds its SD pads from LDO + channel 4). + +Both share the mount settings: ``mount_point``, ``mount_on_initialize``, +``format_if_mount_failed`` (off by default; ``format()`` is explicit), +``max_files``, ``allocation_unit_size`` and ``disk_status_check``. + +Handing the card to a USB host +------------------------------ + +Initialize with ``mount_on_initialize = false`` (or ``unmount()`` first) and pass +``card()`` as ``espp::UsbDevice::MscMedium::sd_card``; the MSC function then mounts +the card at its own path while the application owns it and unmounts it while the +PC has it. See the ``usb_device`` component's ``msc_example`` and the SD card +example README. + +.. ------------------------------- Example ------------------------------------- + +.. toctree:: + + sdcard_example + +.. ---------------------------- API Reference ---------------------------------- + +API Reference +------------- + +.. include-build-file:: inc/sdcard.inc diff --git a/doc/en/storage/sdcard_example.md b/doc/en/storage/sdcard_example.md new file mode 100644 index 0000000000..594322a32d --- /dev/null +++ b/doc/en/storage/sdcard_example.md @@ -0,0 +1,2 @@ +```{include} ../../../components/sdcard/example/README.md +``` From 348136488fec573b555f4328089d787583fdfa7c Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Thu, 17 Sep 2026 20:06:25 -0500 Subject: [PATCH 2/9] fix(sdcard): valid registry tags, upload entry, SDMMC host cleanup, MSC 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 Claude-Session: https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU --- .github/workflows/upload_components.yml | 1 + components/sdcard/idf_component.yml | 1 - components/sdcard/include/sdcard.hpp | 22 ++++++++++----------- components/sdcard/src/sdcard.cpp | 12 ++++++----- components/usb_device/README.md | 3 ++- components/usb_device/msc_example/README.md | 18 +++++++++++++---- 6 files changed, 34 insertions(+), 23 deletions(-) diff --git a/.github/workflows/upload_components.yml b/.github/workflows/upload_components.yml index 0b910a6076..7f028af3e7 100755 --- a/.github/workflows/upload_components.yml +++ b/.github/workflows/upload_components.yml @@ -156,6 +156,7 @@ jobs: components/rtsp components/runqueue components/rx8130ce + components/sdcard components/seeed-studio-round-display components/serialization components/smartpanlee-sc01-plus diff --git a/components/sdcard/idf_component.yml b/components/sdcard/idf_component.yml index 6fa298c1ce..28bfd9d888 100644 --- a/components/sdcard/idf_component.yml +++ b/components/sdcard/idf_component.yml @@ -11,7 +11,6 @@ examples: tags: - cpp - Component - - SD - SD-Card - microSD - SDMMC diff --git a/components/sdcard/include/sdcard.hpp b/components/sdcard/include/sdcard.hpp index ee72e9d1e9..232b1e8a4c 100644 --- a/components/sdcard/include/sdcard.hpp +++ b/components/sdcard/include/sdcard.hpp @@ -108,9 +108,7 @@ class SdCard : public BaseComponent { /// @brief Configuration for the SdCard. struct Config { /** Which interface the card is on and how it is wired. */ - std::variant interface { - SpiConfig {} - }; + std::variant interface = SpiConfig{}; std::string mount_point{"/sdcard"}; /**< VFS path the FAT volume is mounted at. */ /** Mount the FAT volume at the end of initialize(). Leave false when the card * is first going elsewhere (e.g. to a USB host) and call mount() later. */ @@ -136,15 +134,15 @@ class SdCard : public BaseComponent { /// @brief What the card reported about itself at initialize(). struct CardInfo { - std::string name; ///< Product name from the card's CID register. - uint64_t capacity_bytes{0}; ///< Total capacity. - uint32_t sector_size{0}; ///< Bytes per sector (512 for every SD card). - uint32_t sector_count{0}; ///< Number of sectors. - uint32_t frequency_khz{0}; ///< Bus clock actually in use. - uint8_t bus_width{1}; ///< Data lines in use (SDSPI: 1). - bool high_capacity{false}; ///< SDHC / SDXC (block addressing). - bool is_mmc{false}; ///< An (e)MMC device rather than an SD card. - Interface interface { Interface::Spi }; ///< The interface it is on. + std::string name; ///< Product name from the card's CID register. + uint64_t capacity_bytes{0}; ///< Total capacity. + uint32_t sector_size{0}; ///< Bytes per sector (512 for every SD card). + uint32_t sector_count{0}; ///< Number of sectors. + uint32_t frequency_khz{0}; ///< Bus clock actually in use. + uint8_t bus_width{1}; ///< Data lines in use (SDSPI: 1). + bool high_capacity{false}; ///< SDHC / SDXC (block addressing). + bool is_mmc{false}; ///< An (e)MMC device rather than an SD card. + Interface interface = Interface::Spi; ///< The interface it is on. }; /// @brief Space on the mounted FAT volume. diff --git a/components/sdcard/src/sdcard.cpp b/components/sdcard/src/sdcard.cpp index e895c7abf1..4b9cfacd31 100644 --- a/components/sdcard/src/sdcard.cpp +++ b/components/sdcard/src/sdcard.cpp @@ -215,12 +215,13 @@ bool SdCard::init_host(std::error_code &ec) { logger_.warn("This target routes SDMMC through fixed pins; the configured pins are ignored"); #endif esp_err_t err = host_.init ? host_.init() : ESP_OK; // sdmmc_host_init() - if (err == ESP_OK) + const bool host_inited = err == ESP_OK; + if (host_inited) err = sdmmc_host_init_slot(sdmmc.slot, &slot); if (err != ESP_OK) { logger_.error("Could not initialize SDMMC slot {}: {}", sdmmc.slot, esp_err_to_name(err)); - if (host_.deinit && err != ESP_ERR_INVALID_STATE) - host_.deinit(); // the slot init failed: release the host we just brought up + if (host_inited) + call_host_deinit(host_); // the slot init failed: release the host we just brought up #if ESPP_SDCARD_HAS_LDO_PWR_CTRL if (ldo_handle_) { sd_pwr_ctrl_del_on_chip_ldo(static_cast(ldo_handle_)); @@ -281,12 +282,13 @@ bool SdCard::mount_locked(std::error_code &ec) { FATFS *fs = nullptr; esp_err_t err; { -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) +// Plain-macro version tests (cppcheck cannot evaluate ESP_IDF_VERSION_VAL()). +#if ESP_IDF_VERSION_MAJOR > 5 || (ESP_IDF_VERSION_MAJOR == 5 && ESP_IDF_VERSION_MINOR >= 3) esp_vfs_fat_conf_t conf{}; conf.base_path = config_.mount_point.c_str(); conf.fat_drive = drive.c_str(); conf.max_files = static_cast(config_.max_files); -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) +#if ESP_IDF_VERSION_MAJOR >= 6 err = esp_vfs_fat_register(&conf, &fs); #else err = esp_vfs_fat_register_cfg(&conf, &fs); diff --git a/components/usb_device/README.md b/components/usb_device/README.md index 43ff5c4252..034633ed54 100644 --- a/components/usb_device/README.md +++ b/components/usb_device/README.md @@ -284,7 +284,8 @@ hand-overs, so act on it from your own task. ```cpp espp::UsbDevice::MscMedium card; card.type = espp::UsbDevice::MscMedium::Type::SdCard; -card.sd_card = sd_card; // an initialized sdmmc_card_t* (SDMMC or SDSPI host) +card.sd_card = sdcard.card(); // an initialized, unmounted sdmmc_card_t* (SDMMC or SDSPI host), + // e.g. from espp::SdCard with mount_on_initialize = false card.base_path = "/sdcard"; // do NOT also esp_vfs_fat_*_mount() the card yourself espp::UsbDevice::MscMedium flash; diff --git a/components/usb_device/msc_example/README.md b/components/usb_device/msc_example/README.md index 6067e79a78..a4bf56c932 100644 --- a/components/usb_device/msc_example/README.md +++ b/components/usb_device/msc_example/README.md @@ -92,18 +92,28 @@ idf.py erase-flash flash # or: esptool.py erase_region 0x110000 0x100000 ## Using an SD card instead -Initialize the card as usual (SDMMC or SDSPI host) but do **not** mount it with -`esp_vfs_fat_*_mount()` — the MSC function mounts it at `base_path` itself — -then pass the card pointer: +Initialize the card (SDMMC or SDSPI host) but do **not** mount it with +`esp_vfs_fat_*_mount()` — the MSC function mounts it at `base_path` itself. +`espp::SdCard` (the `sdcard` component) keeps those two steps apart, and every +espp BSP with a microSD slot exposes its card through `sdcard()`: ```cpp +espp::SdCard::Config sd_config; +sd_config.interface = espp::SdCard::SdmmcConfig{/* pins */}; +sd_config.mount_on_initialize = false; // probe only; the MSC function mounts it +espp::SdCard sdcard(sd_config); +sdcard.initialize(); + espp::UsbDevice::MscMedium card; card.type = espp::UsbDevice::MscMedium::Type::SdCard; -card.sd_card = sd_card; // sdmmc_card_t* from sdmmc_card_init() +card.sd_card = sdcard.card(); card.base_path = "/sdcard"; msc.media = {card}; // or {card, flash} for two drives ``` +With a BSP, call `initialize_sdcard(...)`, then `sdcard_component()->unmount()` +before handing `sdcard()` to the MSC function. + SD card media need a target with an SDMMC host peripheral (ESP32-S3 / -P4), even when the card is wired to SPI. From 1e64b28a98e03b3c2b3aedbe97490e9f4332347e Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Thu, 17 Sep 2026 20:10:42 -0500 Subject: [PATCH 3/9] feat(bsp): use espp::SdCard in every BSP with a microSD slot 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. 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 Claude-Session: https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU --- components/esp32-p4-eth/CMakeLists.txt | 1 + .../esp32-p4-eth/example/CMakeLists.txt | 1 + components/esp32-p4-eth/idf_component.yml | 1 + .../esp32-p4-eth/include/esp32-p4-eth.hpp | 12 +- components/esp32-p4-eth/src/sdcard.cpp | 93 +++++--------- .../esp32-p4-function-ev-board/CMakeLists.txt | 1 + .../idf_component.yml | 1 + .../include/esp32-p4-function-ev-board.hpp | 12 +- .../esp32-p4-function-ev-board/src/sdcard.cpp | 93 +++++--------- .../esp32-p4-module-dev-kit/CMakeLists.txt | 1 + .../example/CMakeLists.txt | 1 + .../esp32-p4-module-dev-kit/idf_component.yml | 1 + .../include/esp32-p4-module-dev-kit.hpp | 16 +-- .../esp32-p4-module-dev-kit/src/sdcard.cpp | 95 ++++++-------- components/esp32-p4-nano/CMakeLists.txt | 1 + .../esp32-p4-nano/example/CMakeLists.txt | 1 + components/esp32-p4-nano/idf_component.yml | 1 + .../esp32-p4-nano/include/esp32-p4-nano.hpp | 12 +- components/esp32-p4-nano/src/sdcard.cpp | 93 +++++--------- .../esp32-p4-wifi6-dev-kit/CMakeLists.txt | 1 + .../example/CMakeLists.txt | 1 + .../esp32-p4-wifi6-dev-kit/idf_component.yml | 1 + .../include/esp32-p4-wifi6-dev-kit.hpp | 16 +-- .../esp32-p4-wifi6-dev-kit/src/sdcard.cpp | 93 +++++--------- components/lilygo-t5-47/CMakeLists.txt | 2 +- .../lilygo-t5-47/example/CMakeLists.txt | 1 + components/lilygo-t5-47/idf_component.yml | 1 + .../lilygo-t5-47/include/lilygo-t5-47.hpp | 12 +- components/lilygo-t5-47/src/sdcard.cpp | 49 +++----- components/m5stack-cardputer/CMakeLists.txt | 2 +- .../m5stack-cardputer/idf_component.yml | 1 + .../include/m5stack-cardputer.hpp | 13 +- components/m5stack-cardputer/src/sdcard.cpp | 52 +++----- components/m5stack-tab5/CMakeLists.txt | 2 +- .../m5stack-tab5/example/CMakeLists.txt | 1 + components/m5stack-tab5/idf_component.yml | 1 + .../m5stack-tab5/include/m5stack-tab5.hpp | 12 +- components/m5stack-tab5/src/sdcard.cpp | 118 +++++------------- .../smartpanlee-sc01-plus/CMakeLists.txt | 2 +- .../smartpanlee-sc01-plus/idf_component.yml | 1 + .../include/smartpanlee-sc01-plus.hpp | 13 +- .../src/smartpanlee-sc01-plus.cpp | 74 +++++------ components/t-deck/CMakeLists.txt | 2 +- components/t-deck/idf_component.yml | 1 + components/t-deck/include/t-deck.hpp | 11 +- components/t-deck/src/sdcard.cpp | 70 +++-------- components/t-dongle-s3/CMakeLists.txt | 2 +- components/t-dongle-s3/idf_component.yml | 1 + .../t-dongle-s3/include/t-dongle-s3.hpp | 11 +- components/t-dongle-s3/src/sdcard.cpp | 74 ++++------- components/ws-s3-geek/CMakeLists.txt | 2 +- components/ws-s3-geek/idf_component.yml | 1 + components/ws-s3-geek/include/ws-s3-geek.hpp | 13 +- components/ws-s3-geek/src/sdcard.cpp | 107 ++++------------ components/ws-s3-lcd-1-47/CMakeLists.txt | 2 +- components/ws-s3-lcd-1-47/idf_component.yml | 1 + .../ws-s3-lcd-1-47/include/ws-s3-lcd-1-47.hpp | 13 +- components/ws-s3-lcd-1-47/src/sdcard.cpp | 74 ++++------- components/xiao-esp32s3-sense/CMakeLists.txt | 2 +- .../xiao-esp32s3-sense/example/CMakeLists.txt | 1 + .../xiao-esp32s3-sense/idf_component.yml | 1 + .../include/xiao-esp32s3-sense.hpp | 12 +- .../src/xiao-esp32s3-sense.cpp | 65 ++++------ 63 files changed, 549 insertions(+), 823 deletions(-) mode change 100755 => 100644 components/t-deck/include/t-deck.hpp diff --git a/components/esp32-p4-eth/CMakeLists.txt b/components/esp32-p4-eth/CMakeLists.txt index 49b2ba6448..06ef5e3d7b 100644 --- a/components/esp32-p4-eth/CMakeLists.txt +++ b/components/esp32-p4-eth/CMakeLists.txt @@ -7,6 +7,7 @@ idf_component_register( "esp_netif" "fatfs" "esp_driver_sdmmc" + "sdcard" "sdmmc" "codec" "i2c" diff --git a/components/esp32-p4-eth/example/CMakeLists.txt b/components/esp32-p4-eth/example/CMakeLists.txt index 15a42f5a43..530287ee79 100644 --- a/components/esp32-p4-eth/example/CMakeLists.txt +++ b/components/esp32-p4-eth/example/CMakeLists.txt @@ -30,6 +30,7 @@ set(EXTRA_COMPONENT_DIRS "../../../components/led" "../../../components/logger" "../../../components/lvgl" + "../../../components/sdcard" "../../../components/spi" "../../../components/task" "../../../components/touch" diff --git a/components/esp32-p4-eth/idf_component.yml b/components/esp32-p4-eth/idf_component.yml index fa58a765ae..6d28af5ce5 100644 --- a/components/esp32-p4-eth/idf_component.yml +++ b/components/esp32-p4-eth/idf_component.yml @@ -31,6 +31,7 @@ dependencies: espp/gt911: ">=1.0" espp/input_drivers: ">=1.0" espp/interrupt: ">=1.0" + espp/sdcard: ">=1.0" # MIPI-CSI camera pipeline: esp_video provides the V4L2 capture framework # (CSI + ISP) and esp_cam_sensor provides the OV5647 sensor driver. espressif/esp_video: ">=2.0,<2.4" # 2.4.0 fails to compile against the CI IDF (ISP_LL_EVENT_ERROR_MASK undeclared) diff --git a/components/esp32-p4-eth/include/esp32-p4-eth.hpp b/components/esp32-p4-eth/include/esp32-p4-eth.hpp index b606fa80a7..345a592148 100644 --- a/components/esp32-p4-eth/include/esp32-p4-eth.hpp +++ b/components/esp32-p4-eth/include/esp32-p4-eth.hpp @@ -33,6 +33,7 @@ #include "ili9881.hpp" #include "interrupt.hpp" #include "jd9365.hpp" +#include "sdcard.hpp" #include "task.hpp" #include "touchpad_input.hpp" @@ -460,7 +461,13 @@ class Esp32P4Eth : public BaseComponent { bool is_sd_card_available() const { return sd_card_initialized_; } /// \return The SDMMC card handle, or nullptr if not initialized. - sdmmc_card_t *sdcard() const { return sdcard_; } + sdmmc_card_t *sdcard() const { return sdcard_ ? sdcard_->card() : nullptr; } + + /// Get the SD card component: mount() / unmount() (e.g. to hand the card to a + /// USB host with the usb_device MSC function), format(), card_info(), + /// volume_info(), ... + /// \return The component, or nullptr until initialize_sdcard() succeeded + espp::SdCard *sdcard_component() const { return sdcard_.get(); } /// Get total/free space of the mounted card. /// \param size_mb Optional out: total size in MB. @@ -631,8 +638,7 @@ class Esp32P4Eth : public BaseComponent { static constexpr gpio_num_t sd_d3_io = GPIO_NUM_42; std::atomic sd_card_initialized_{false}; - sdmmc_card_t *sdcard_{nullptr}; - void *sd_pwr_ctrl_handle_{nullptr}; + std::unique_ptr sdcard_; ///////////////////////////////////////////////////////////////////////////// // Interrupts (used by the optional interrupt-driven touch path) diff --git a/components/esp32-p4-eth/src/sdcard.cpp b/components/esp32-p4-eth/src/sdcard.cpp index 2603d710ca..a60b4ddaef 100644 --- a/components/esp32-p4-eth/src/sdcard.cpp +++ b/components/esp32-p4-eth/src/sdcard.cpp @@ -1,9 +1,5 @@ #include "esp32-p4-eth.hpp" -#include -#include -#include - namespace espp { bool Esp32P4Eth::initialize_sdcard(const SdCardConfig &config) { @@ -13,75 +9,54 @@ bool Esp32P4Eth::initialize_sdcard(const SdCardConfig &config) { } logger_.info("Initializing SD card (4-bit SDMMC)"); - - esp_vfs_fat_sdmmc_mount_config_t mount_config{}; - mount_config.format_if_mount_failed = config.format_if_mount_failed; - mount_config.max_files = config.max_files; - mount_config.allocation_unit_size = config.allocation_unit_size; - - sdmmc_host_t host = SDMMC_HOST_DEFAULT(); - host.max_freq_khz = SDMMC_FREQ_HIGHSPEED; - host.slot = SDMMC_HOST_SLOT_0; - - // The ESP32-P4 powers the SD card via an internal LDO (LDO_VO4). Configure the - // power control handle so the host can enable that rail. - sd_pwr_ctrl_ldo_config_t ldo_config{}; - ldo_config.ldo_chan_id = sd_ldo_channel; - sd_pwr_ctrl_handle_t pwr_ctrl_handle = nullptr; - esp_err_t ret = sd_pwr_ctrl_new_on_chip_ldo(&ldo_config, &pwr_ctrl_handle); - if (ret != ESP_OK) { - logger_.error("Failed to create SD power control driver: {}", esp_err_to_name(ret)); - return false; - } - host.pwr_ctrl_handle = pwr_ctrl_handle; - sd_pwr_ctrl_handle_ = pwr_ctrl_handle; - - sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT(); - slot_config.width = 4; - slot_config.clk = sd_clk_io; - slot_config.cmd = sd_cmd_io; - slot_config.d0 = sd_d0_io; - slot_config.d1 = sd_d1_io; - slot_config.d2 = sd_d2_io; - slot_config.d3 = sd_d3_io; - - logger_.debug("Mounting filesystem"); - ret = esp_vfs_fat_sdmmc_mount(mount_point, &host, &slot_config, &mount_config, &sdcard_); - - if (ret != ESP_OK) { - if (ret == ESP_FAIL) { - logger_.error("Failed to mount filesystem."); - } else { - logger_.warn("Failed to initialize the card ({}). " - "Make sure an SD card is inserted.", - esp_err_to_name(ret)); - } - sd_pwr_ctrl_del_on_chip_ldo(pwr_ctrl_handle); - sd_pwr_ctrl_handle_ = nullptr; + espp::SdCard::SdmmcConfig sdmmc; + sdmmc.slot = SDMMC_HOST_SLOT_0; + sdmmc.bus_width = 4; + sdmmc.clk = sd_clk_io; + sdmmc.cmd = sd_cmd_io; + sdmmc.d0 = sd_d0_io; + sdmmc.d1 = sd_d1_io; + sdmmc.d2 = sd_d2_io; + sdmmc.d3 = sd_d3_io; + sdmmc.frequency_khz = SDMMC_FREQ_HIGHSPEED; // 40 MHz + // The ESP32-P4 powers the SD card via an internal LDO (LDO_VO4): without it + // the bus floats and card init fails. + sdmmc.ldo_channel = sd_ldo_channel; + sdcard_ = std::make_unique(espp::SdCard::Config{ + .interface = sdmmc, + .mount_point = mount_point, + .format_if_mount_failed = config.format_if_mount_failed, + .max_files = config.max_files, + .allocation_unit_size = config.allocation_unit_size, + .log_level = get_log_level(), + }); + std::error_code ec; + if (!sdcard_->initialize(ec)) { + logger_.error("Failed to initialize the SD card: {}. Make sure an SD card is inserted.", + ec.message()); + sdcard_.reset(); return false; } - - logger_.info("Filesystem mounted"); - sdmmc_card_print_info(stdout, sdcard_); + logger_.info("Filesystem mounted at {}", mount_point); + sdcard_->print_info(stdout); sd_card_initialized_ = true; return true; } bool Esp32P4Eth::get_sd_card_info(uint32_t *size_mb, uint32_t *free_mb) const { - if (!sd_card_initialized_) { + if (!sd_card_initialized_ || !sdcard_) { return false; } - uint64_t total_bytes = 0, free_bytes = 0; - esp_err_t ret = esp_vfs_fat_info(mount_point, &total_bytes, &free_bytes); - if (ret != ESP_OK) { - logger_.error("Failed to get SD card information ({})", esp_err_to_name(ret)); + const auto volume = sdcard_->volume_info(); + if (!volume) { + logger_.error("Failed to get SD card information (volume not mounted)"); return false; } if (size_mb) { - *size_mb = total_bytes / (1024 * 1024); + *size_mb = volume->total_bytes / (1024 * 1024); } if (free_mb) { - *free_mb = free_bytes / (1024 * 1024); + *free_mb = volume->free_bytes / (1024 * 1024); } return true; } diff --git a/components/esp32-p4-function-ev-board/CMakeLists.txt b/components/esp32-p4-function-ev-board/CMakeLists.txt index 938d0a2677..542aa6ede2 100644 --- a/components/esp32-p4-function-ev-board/CMakeLists.txt +++ b/components/esp32-p4-function-ev-board/CMakeLists.txt @@ -19,5 +19,6 @@ idf_component_register( "esp_netif" "fatfs" "esp_driver_sdmmc" + "sdcard" "sdmmc" ) diff --git a/components/esp32-p4-function-ev-board/idf_component.yml b/components/esp32-p4-function-ev-board/idf_component.yml index d942987dce..222d511e4a 100644 --- a/components/esp32-p4-function-ev-board/idf_component.yml +++ b/components/esp32-p4-function-ev-board/idf_component.yml @@ -26,6 +26,7 @@ dependencies: espp/i2c: ">=1.0" espp/input_drivers: ">=1.0" espp/interrupt: ">=1.0" + espp/sdcard: ">=1.0" espp/led: ">=1.0" espp/task: ">=1.0" targets: diff --git a/components/esp32-p4-function-ev-board/include/esp32-p4-function-ev-board.hpp b/components/esp32-p4-function-ev-board/include/esp32-p4-function-ev-board.hpp index e1d67521ae..937ef04190 100644 --- a/components/esp32-p4-function-ev-board/include/esp32-p4-function-ev-board.hpp +++ b/components/esp32-p4-function-ev-board/include/esp32-p4-function-ev-board.hpp @@ -38,6 +38,7 @@ #include "ili9881.hpp" #include "interrupt.hpp" #include "led.hpp" +#include "sdcard.hpp" #include "task.hpp" #include "touchpad_input.hpp" @@ -349,7 +350,13 @@ class Esp32P4FunctionEvBoard : public BaseComponent { /// Get the uSD card handle /// \return A pointer to the uSD card, or nullptr if not initialized - sdmmc_card_t *sdcard() const { return sdcard_; } + sdmmc_card_t *sdcard() const { return sdcard_ ? sdcard_->card() : nullptr; } + + /// Get the SD card component: mount() / unmount() (e.g. to hand the card to a + /// USB host with the usb_device MSC function), format(), card_info(), + /// volume_info(), ... + /// \return The component, or nullptr until initialize_sdcard() succeeded + espp::SdCard *sdcard_component() const { return sdcard_.get(); } /// Get SD card info /// \param size_mb Pointer to store size in MB @@ -600,8 +607,7 @@ class Esp32P4FunctionEvBoard : public BaseComponent { // uSD card std::atomic sd_card_initialized_{false}; - sdmmc_card_t *sdcard_{nullptr}; - void *sd_pwr_ctrl_handle_{nullptr}; + std::unique_ptr sdcard_; #if CONFIG_ESP_P4_EV_BOARD_ETHERNET // The board's RMII Ethernet is driven by the reusable espp::Ethernet component diff --git a/components/esp32-p4-function-ev-board/src/sdcard.cpp b/components/esp32-p4-function-ev-board/src/sdcard.cpp index 9e405212b0..422973bf48 100644 --- a/components/esp32-p4-function-ev-board/src/sdcard.cpp +++ b/components/esp32-p4-function-ev-board/src/sdcard.cpp @@ -1,9 +1,5 @@ #include "esp32-p4-function-ev-board.hpp" -#include -#include -#include - namespace espp { bool Esp32P4FunctionEvBoard::initialize_sdcard(const SdCardConfig &config) { @@ -13,75 +9,54 @@ bool Esp32P4FunctionEvBoard::initialize_sdcard(const SdCardConfig &config) { } logger_.info("Initializing SD card (4-bit SDMMC)"); - - esp_vfs_fat_sdmmc_mount_config_t mount_config{}; - mount_config.format_if_mount_failed = config.format_if_mount_failed; - mount_config.max_files = config.max_files; - mount_config.allocation_unit_size = config.allocation_unit_size; - - sdmmc_host_t host = SDMMC_HOST_DEFAULT(); - host.max_freq_khz = SDMMC_FREQ_HIGHSPEED; - host.slot = SDMMC_HOST_SLOT_0; - - // The ESP32-P4 powers the SD card via an internal LDO (LDO_VO4). Configure the - // power control handle so the host can enable that rail. - sd_pwr_ctrl_ldo_config_t ldo_config{}; - ldo_config.ldo_chan_id = sd_ldo_channel; - sd_pwr_ctrl_handle_t pwr_ctrl_handle = nullptr; - esp_err_t ret = sd_pwr_ctrl_new_on_chip_ldo(&ldo_config, &pwr_ctrl_handle); - if (ret != ESP_OK) { - logger_.error("Failed to create SD power control driver: {}", esp_err_to_name(ret)); - return false; - } - host.pwr_ctrl_handle = pwr_ctrl_handle; - sd_pwr_ctrl_handle_ = pwr_ctrl_handle; - - sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT(); - slot_config.width = 4; - slot_config.clk = sd_clk_io; - slot_config.cmd = sd_cmd_io; - slot_config.d0 = sd_d0_io; - slot_config.d1 = sd_d1_io; - slot_config.d2 = sd_d2_io; - slot_config.d3 = sd_d3_io; - - logger_.debug("Mounting filesystem"); - ret = esp_vfs_fat_sdmmc_mount(mount_point, &host, &slot_config, &mount_config, &sdcard_); - - if (ret != ESP_OK) { - if (ret == ESP_FAIL) { - logger_.error("Failed to mount filesystem."); - } else { - logger_.warn("Failed to initialize the card ({}). " - "Make sure an SD card is inserted.", - esp_err_to_name(ret)); - } - sd_pwr_ctrl_del_on_chip_ldo(pwr_ctrl_handle); - sd_pwr_ctrl_handle_ = nullptr; + espp::SdCard::SdmmcConfig sdmmc; + sdmmc.slot = SDMMC_HOST_SLOT_0; + sdmmc.bus_width = 4; + sdmmc.clk = sd_clk_io; + sdmmc.cmd = sd_cmd_io; + sdmmc.d0 = sd_d0_io; + sdmmc.d1 = sd_d1_io; + sdmmc.d2 = sd_d2_io; + sdmmc.d3 = sd_d3_io; + sdmmc.frequency_khz = SDMMC_FREQ_HIGHSPEED; // 40 MHz + // The ESP32-P4 powers the SD card via an internal LDO (LDO_VO4): without it + // the bus floats and card init fails. + sdmmc.ldo_channel = sd_ldo_channel; + sdcard_ = std::make_unique(espp::SdCard::Config{ + .interface = sdmmc, + .mount_point = mount_point, + .format_if_mount_failed = config.format_if_mount_failed, + .max_files = config.max_files, + .allocation_unit_size = config.allocation_unit_size, + .log_level = get_log_level(), + }); + std::error_code ec; + if (!sdcard_->initialize(ec)) { + logger_.error("Failed to initialize the SD card: {}. Make sure an SD card is inserted.", + ec.message()); + sdcard_.reset(); return false; } - - logger_.info("Filesystem mounted"); - sdmmc_card_print_info(stdout, sdcard_); + logger_.info("Filesystem mounted at {}", mount_point); + sdcard_->print_info(stdout); sd_card_initialized_ = true; return true; } bool Esp32P4FunctionEvBoard::get_sd_card_info(uint32_t *size_mb, uint32_t *free_mb) const { - if (!sd_card_initialized_) { + if (!sd_card_initialized_ || !sdcard_) { return false; } - uint64_t total_bytes = 0, free_bytes = 0; - esp_err_t ret = esp_vfs_fat_info(mount_point, &total_bytes, &free_bytes); - if (ret != ESP_OK) { - logger_.error("Failed to get SD card information ({})", esp_err_to_name(ret)); + const auto volume = sdcard_->volume_info(); + if (!volume) { + logger_.error("Failed to get SD card information (volume not mounted)"); return false; } if (size_mb) { - *size_mb = total_bytes / (1024 * 1024); + *size_mb = volume->total_bytes / (1024 * 1024); } if (free_mb) { - *free_mb = free_bytes / (1024 * 1024); + *free_mb = volume->free_bytes / (1024 * 1024); } return true; } diff --git a/components/esp32-p4-module-dev-kit/CMakeLists.txt b/components/esp32-p4-module-dev-kit/CMakeLists.txt index 8d01b2563c..2c7caabd5e 100644 --- a/components/esp32-p4-module-dev-kit/CMakeLists.txt +++ b/components/esp32-p4-module-dev-kit/CMakeLists.txt @@ -7,6 +7,7 @@ idf_component_register( "esp_netif" "fatfs" "esp_driver_sdmmc" + "sdcard" "sdmmc" "codec" "i2c" diff --git a/components/esp32-p4-module-dev-kit/example/CMakeLists.txt b/components/esp32-p4-module-dev-kit/example/CMakeLists.txt index 7a8a44847a..1f7de46526 100644 --- a/components/esp32-p4-module-dev-kit/example/CMakeLists.txt +++ b/components/esp32-p4-module-dev-kit/example/CMakeLists.txt @@ -35,6 +35,7 @@ set(EXTRA_COMPONENT_DIRS "../../../components/led" "../../../components/logger" "../../../components/lvgl" + "../../../components/sdcard" "../../../components/spi" "../../../components/task" "../../../components/touch" diff --git a/components/esp32-p4-module-dev-kit/idf_component.yml b/components/esp32-p4-module-dev-kit/idf_component.yml index 8d9c9ffb4c..299fe05261 100644 --- a/components/esp32-p4-module-dev-kit/idf_component.yml +++ b/components/esp32-p4-module-dev-kit/idf_component.yml @@ -33,6 +33,7 @@ dependencies: espp/gt911: ">=1.0" espp/input_drivers: ">=1.0" espp/interrupt: ">=1.0" + espp/sdcard: ">=1.0" # MIPI-CSI camera pipeline: esp_video provides the V4L2 capture framework # (CSI + ISP) and esp_cam_sensor provides the OV5647 sensor driver. espressif/esp_video: ">=2.0,<2.4" # 2.4.0 fails to compile against the CI IDF (ISP_LL_EVENT_ERROR_MASK undeclared) diff --git a/components/esp32-p4-module-dev-kit/include/esp32-p4-module-dev-kit.hpp b/components/esp32-p4-module-dev-kit/include/esp32-p4-module-dev-kit.hpp index cb1271f0d1..d5681a6884 100644 --- a/components/esp32-p4-module-dev-kit/include/esp32-p4-module-dev-kit.hpp +++ b/components/esp32-p4-module-dev-kit/include/esp32-p4-module-dev-kit.hpp @@ -14,7 +14,6 @@ #include #include #include -#include #include #include @@ -34,6 +33,7 @@ #include "ili9881.hpp" #include "interrupt.hpp" #include "jd9365.hpp" +#include "sdcard.hpp" #include "task.hpp" #include "touchpad_input.hpp" @@ -485,7 +485,13 @@ class Esp32P4ModuleDevKit : public BaseComponent { bool is_sd_card_available() const { return sd_card_initialized_; } /// \return The SDMMC card handle, or nullptr if not initialized. - sdmmc_card_t *sdcard() const { return sdcard_; } + sdmmc_card_t *sdcard() const { return sdcard_ ? sdcard_->card() : nullptr; } + + /// Get the SD card component: mount() / unmount() (e.g. to hand the card to a + /// USB host with the usb_device MSC function), format(), card_info(), + /// volume_info(), ... + /// \return The component, or nullptr until initialize_sdcard() succeeded + espp::SdCard *sdcard_component() const { return sdcard_.get(); } /// Get total/free space of the mounted card. /// \param size_mb Optional out: total size in MB. @@ -675,11 +681,7 @@ class Esp32P4ModuleDevKit : public BaseComponent { static constexpr gpio_num_t sd_d3_io = GPIO_NUM_42; std::atomic sd_card_initialized_{false}; - sdmmc_card_t *sdcard_{nullptr}; - // SD power-control driver (on-chip LDO). Owned by this class: created in - // initialize_sdcard() and deleted there (sd_pwr_ctrl_del_on_chip_ldo) if the - // mount fails; a successful mount keeps it alive for the life of the card. - sd_pwr_ctrl_handle_t sd_pwr_ctrl_handle_{nullptr}; + std::unique_ptr sdcard_; ///////////////////////////////////////////////////////////////////////////// // Interrupts (used by the optional interrupt-driven touch path) diff --git a/components/esp32-p4-module-dev-kit/src/sdcard.cpp b/components/esp32-p4-module-dev-kit/src/sdcard.cpp index 5db24970c9..7b1d0e973d 100644 --- a/components/esp32-p4-module-dev-kit/src/sdcard.cpp +++ b/components/esp32-p4-module-dev-kit/src/sdcard.cpp @@ -1,87 +1,64 @@ #include "esp32-p4-module-dev-kit.hpp" -#include -#include -#include - namespace espp { bool Esp32P4ModuleDevKit::initialize_sdcard(const SdCardConfig &config) { if (sdcard_) { + // Idempotent, matching the other initialize_* methods: calling again is + // harmless, so warn and report success. logger_.warn("SD card already initialized"); return true; } logger_.info("Initializing SD card (4-bit SDMMC)"); - - esp_vfs_fat_sdmmc_mount_config_t mount_config{}; - mount_config.format_if_mount_failed = config.format_if_mount_failed; - mount_config.max_files = config.max_files; - mount_config.allocation_unit_size = config.allocation_unit_size; - - sdmmc_host_t host = SDMMC_HOST_DEFAULT(); - host.max_freq_khz = SDMMC_FREQ_HIGHSPEED; - host.slot = SDMMC_HOST_SLOT_0; - - // The ESP32-P4 powers the SD card via an internal LDO (LDO_VO4). Configure the - // power control handle so the host can enable that rail. - sd_pwr_ctrl_ldo_config_t ldo_config{}; - ldo_config.ldo_chan_id = sd_ldo_channel; - sd_pwr_ctrl_handle_t pwr_ctrl_handle = nullptr; - esp_err_t ret = sd_pwr_ctrl_new_on_chip_ldo(&ldo_config, &pwr_ctrl_handle); - if (ret != ESP_OK) { - logger_.error("Failed to create SD power control driver: {}", esp_err_to_name(ret)); - return false; - } - host.pwr_ctrl_handle = pwr_ctrl_handle; - sd_pwr_ctrl_handle_ = pwr_ctrl_handle; - - sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT(); - slot_config.width = 4; - slot_config.clk = sd_clk_io; - slot_config.cmd = sd_cmd_io; - slot_config.d0 = sd_d0_io; - slot_config.d1 = sd_d1_io; - slot_config.d2 = sd_d2_io; - slot_config.d3 = sd_d3_io; - - logger_.debug("Mounting filesystem"); - ret = esp_vfs_fat_sdmmc_mount(mount_point, &host, &slot_config, &mount_config, &sdcard_); - - if (ret != ESP_OK) { - if (ret == ESP_FAIL) { - logger_.error("Failed to mount filesystem."); - } else { - logger_.warn("Failed to initialize the card ({}). " - "Make sure an SD card is inserted.", - esp_err_to_name(ret)); - } - sd_pwr_ctrl_del_on_chip_ldo(pwr_ctrl_handle); - sd_pwr_ctrl_handle_ = nullptr; + espp::SdCard::SdmmcConfig sdmmc; + sdmmc.slot = SDMMC_HOST_SLOT_0; + sdmmc.bus_width = 4; + sdmmc.clk = sd_clk_io; + sdmmc.cmd = sd_cmd_io; + sdmmc.d0 = sd_d0_io; + sdmmc.d1 = sd_d1_io; + sdmmc.d2 = sd_d2_io; + sdmmc.d3 = sd_d3_io; + sdmmc.frequency_khz = SDMMC_FREQ_HIGHSPEED; // 40 MHz + // The ESP32-P4 powers the SD card via an internal LDO (LDO_VO4): without it + // the bus floats and card init fails. + sdmmc.ldo_channel = sd_ldo_channel; + sdcard_ = std::make_unique(espp::SdCard::Config{ + .interface = sdmmc, + .mount_point = mount_point, + .format_if_mount_failed = config.format_if_mount_failed, + .max_files = config.max_files, + .allocation_unit_size = config.allocation_unit_size, + .log_level = get_log_level(), + }); + std::error_code ec; + if (!sdcard_->initialize(ec)) { + logger_.error("Failed to initialize the SD card: {}. Make sure an SD card is inserted.", + ec.message()); + sdcard_.reset(); return false; } - - logger_.info("Filesystem mounted"); - sdmmc_card_print_info(stdout, sdcard_); + logger_.info("Filesystem mounted at {}", mount_point); + sdcard_->print_info(stdout); sd_card_initialized_ = true; return true; } bool Esp32P4ModuleDevKit::get_sd_card_info(uint32_t *size_mb, uint32_t *free_mb) const { - if (!sd_card_initialized_) { + if (!sd_card_initialized_ || !sdcard_) { return false; } - uint64_t total_bytes = 0, free_bytes = 0; - esp_err_t ret = esp_vfs_fat_info(mount_point, &total_bytes, &free_bytes); - if (ret != ESP_OK) { - logger_.error("Failed to get SD card information ({})", esp_err_to_name(ret)); + const auto volume = sdcard_->volume_info(); + if (!volume) { + logger_.error("Failed to get SD card information (volume not mounted)"); return false; } if (size_mb) { - *size_mb = total_bytes / (1024 * 1024); + *size_mb = volume->total_bytes / (1024 * 1024); } if (free_mb) { - *free_mb = free_bytes / (1024 * 1024); + *free_mb = volume->free_bytes / (1024 * 1024); } return true; } diff --git a/components/esp32-p4-nano/CMakeLists.txt b/components/esp32-p4-nano/CMakeLists.txt index 49b2ba6448..06ef5e3d7b 100644 --- a/components/esp32-p4-nano/CMakeLists.txt +++ b/components/esp32-p4-nano/CMakeLists.txt @@ -7,6 +7,7 @@ idf_component_register( "esp_netif" "fatfs" "esp_driver_sdmmc" + "sdcard" "sdmmc" "codec" "i2c" diff --git a/components/esp32-p4-nano/example/CMakeLists.txt b/components/esp32-p4-nano/example/CMakeLists.txt index 145de0658e..1edab382dd 100644 --- a/components/esp32-p4-nano/example/CMakeLists.txt +++ b/components/esp32-p4-nano/example/CMakeLists.txt @@ -30,6 +30,7 @@ set(EXTRA_COMPONENT_DIRS "../../../components/led" "../../../components/logger" "../../../components/lvgl" + "../../../components/sdcard" "../../../components/spi" "../../../components/task" "../../../components/touch" diff --git a/components/esp32-p4-nano/idf_component.yml b/components/esp32-p4-nano/idf_component.yml index e288661b16..50fc8d6e61 100644 --- a/components/esp32-p4-nano/idf_component.yml +++ b/components/esp32-p4-nano/idf_component.yml @@ -31,6 +31,7 @@ dependencies: espp/gt911: ">=1.0" espp/input_drivers: ">=1.0" espp/interrupt: ">=1.0" + espp/sdcard: ">=1.0" # MIPI-CSI camera pipeline: esp_video provides the V4L2 capture framework # (CSI + ISP) and esp_cam_sensor provides the OV5647 sensor driver. espressif/esp_video: ">=2.0,<2.4" # 2.4.0 fails to compile against the CI IDF (ISP_LL_EVENT_ERROR_MASK undeclared) diff --git a/components/esp32-p4-nano/include/esp32-p4-nano.hpp b/components/esp32-p4-nano/include/esp32-p4-nano.hpp index ca03c084ff..4683b2fc76 100644 --- a/components/esp32-p4-nano/include/esp32-p4-nano.hpp +++ b/components/esp32-p4-nano/include/esp32-p4-nano.hpp @@ -33,6 +33,7 @@ #include "ili9881.hpp" #include "interrupt.hpp" #include "jd9365.hpp" +#include "sdcard.hpp" #include "task.hpp" #include "touchpad_input.hpp" @@ -461,7 +462,13 @@ class Esp32P4Nano : public BaseComponent { bool is_sd_card_available() const { return sd_card_initialized_; } /// \return The SDMMC card handle, or nullptr if not initialized. - sdmmc_card_t *sdcard() const { return sdcard_; } + sdmmc_card_t *sdcard() const { return sdcard_ ? sdcard_->card() : nullptr; } + + /// Get the SD card component: mount() / unmount() (e.g. to hand the card to a + /// USB host with the usb_device MSC function), format(), card_info(), + /// volume_info(), ... + /// \return The component, or nullptr until initialize_sdcard() succeeded + espp::SdCard *sdcard_component() const { return sdcard_.get(); } /// Get total/free space of the mounted card. /// \param size_mb Optional out: total size in MB. @@ -632,8 +639,7 @@ class Esp32P4Nano : public BaseComponent { static constexpr gpio_num_t sd_d3_io = GPIO_NUM_42; std::atomic sd_card_initialized_{false}; - sdmmc_card_t *sdcard_{nullptr}; - void *sd_pwr_ctrl_handle_{nullptr}; + std::unique_ptr sdcard_; ///////////////////////////////////////////////////////////////////////////// // Interrupts (used by the optional interrupt-driven touch path) diff --git a/components/esp32-p4-nano/src/sdcard.cpp b/components/esp32-p4-nano/src/sdcard.cpp index b20e77d36b..efaad7125a 100644 --- a/components/esp32-p4-nano/src/sdcard.cpp +++ b/components/esp32-p4-nano/src/sdcard.cpp @@ -1,9 +1,5 @@ #include "esp32-p4-nano.hpp" -#include -#include -#include - namespace espp { bool Esp32P4Nano::initialize_sdcard(const SdCardConfig &config) { @@ -13,75 +9,54 @@ bool Esp32P4Nano::initialize_sdcard(const SdCardConfig &config) { } logger_.info("Initializing SD card (4-bit SDMMC)"); - - esp_vfs_fat_sdmmc_mount_config_t mount_config{}; - mount_config.format_if_mount_failed = config.format_if_mount_failed; - mount_config.max_files = config.max_files; - mount_config.allocation_unit_size = config.allocation_unit_size; - - sdmmc_host_t host = SDMMC_HOST_DEFAULT(); - host.max_freq_khz = SDMMC_FREQ_HIGHSPEED; - host.slot = SDMMC_HOST_SLOT_0; - - // The ESP32-P4 powers the SD card via an internal LDO (LDO_VO4). Configure the - // power control handle so the host can enable that rail. - sd_pwr_ctrl_ldo_config_t ldo_config{}; - ldo_config.ldo_chan_id = sd_ldo_channel; - sd_pwr_ctrl_handle_t pwr_ctrl_handle = nullptr; - esp_err_t ret = sd_pwr_ctrl_new_on_chip_ldo(&ldo_config, &pwr_ctrl_handle); - if (ret != ESP_OK) { - logger_.error("Failed to create SD power control driver: {}", esp_err_to_name(ret)); - return false; - } - host.pwr_ctrl_handle = pwr_ctrl_handle; - sd_pwr_ctrl_handle_ = pwr_ctrl_handle; - - sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT(); - slot_config.width = 4; - slot_config.clk = sd_clk_io; - slot_config.cmd = sd_cmd_io; - slot_config.d0 = sd_d0_io; - slot_config.d1 = sd_d1_io; - slot_config.d2 = sd_d2_io; - slot_config.d3 = sd_d3_io; - - logger_.debug("Mounting filesystem"); - ret = esp_vfs_fat_sdmmc_mount(mount_point, &host, &slot_config, &mount_config, &sdcard_); - - if (ret != ESP_OK) { - if (ret == ESP_FAIL) { - logger_.error("Failed to mount filesystem."); - } else { - logger_.warn("Failed to initialize the card ({}). " - "Make sure an SD card is inserted.", - esp_err_to_name(ret)); - } - sd_pwr_ctrl_del_on_chip_ldo(pwr_ctrl_handle); - sd_pwr_ctrl_handle_ = nullptr; + espp::SdCard::SdmmcConfig sdmmc; + sdmmc.slot = SDMMC_HOST_SLOT_0; + sdmmc.bus_width = 4; + sdmmc.clk = sd_clk_io; + sdmmc.cmd = sd_cmd_io; + sdmmc.d0 = sd_d0_io; + sdmmc.d1 = sd_d1_io; + sdmmc.d2 = sd_d2_io; + sdmmc.d3 = sd_d3_io; + sdmmc.frequency_khz = SDMMC_FREQ_HIGHSPEED; // 40 MHz + // The ESP32-P4 powers the SD card via an internal LDO (LDO_VO4): without it + // the bus floats and card init fails. + sdmmc.ldo_channel = sd_ldo_channel; + sdcard_ = std::make_unique(espp::SdCard::Config{ + .interface = sdmmc, + .mount_point = mount_point, + .format_if_mount_failed = config.format_if_mount_failed, + .max_files = config.max_files, + .allocation_unit_size = config.allocation_unit_size, + .log_level = get_log_level(), + }); + std::error_code ec; + if (!sdcard_->initialize(ec)) { + logger_.error("Failed to initialize the SD card: {}. Make sure an SD card is inserted.", + ec.message()); + sdcard_.reset(); return false; } - - logger_.info("Filesystem mounted"); - sdmmc_card_print_info(stdout, sdcard_); + logger_.info("Filesystem mounted at {}", mount_point); + sdcard_->print_info(stdout); sd_card_initialized_ = true; return true; } bool Esp32P4Nano::get_sd_card_info(uint32_t *size_mb, uint32_t *free_mb) const { - if (!sd_card_initialized_) { + if (!sd_card_initialized_ || !sdcard_) { return false; } - uint64_t total_bytes = 0, free_bytes = 0; - esp_err_t ret = esp_vfs_fat_info(mount_point, &total_bytes, &free_bytes); - if (ret != ESP_OK) { - logger_.error("Failed to get SD card information ({})", esp_err_to_name(ret)); + const auto volume = sdcard_->volume_info(); + if (!volume) { + logger_.error("Failed to get SD card information (volume not mounted)"); return false; } if (size_mb) { - *size_mb = total_bytes / (1024 * 1024); + *size_mb = volume->total_bytes / (1024 * 1024); } if (free_mb) { - *free_mb = free_bytes / (1024 * 1024); + *free_mb = volume->free_bytes / (1024 * 1024); } return true; } diff --git a/components/esp32-p4-wifi6-dev-kit/CMakeLists.txt b/components/esp32-p4-wifi6-dev-kit/CMakeLists.txt index 49b2ba6448..06ef5e3d7b 100644 --- a/components/esp32-p4-wifi6-dev-kit/CMakeLists.txt +++ b/components/esp32-p4-wifi6-dev-kit/CMakeLists.txt @@ -7,6 +7,7 @@ idf_component_register( "esp_netif" "fatfs" "esp_driver_sdmmc" + "sdcard" "sdmmc" "codec" "i2c" diff --git a/components/esp32-p4-wifi6-dev-kit/example/CMakeLists.txt b/components/esp32-p4-wifi6-dev-kit/example/CMakeLists.txt index d8c002c430..a47a3a75a0 100644 --- a/components/esp32-p4-wifi6-dev-kit/example/CMakeLists.txt +++ b/components/esp32-p4-wifi6-dev-kit/example/CMakeLists.txt @@ -34,6 +34,7 @@ set(EXTRA_COMPONENT_DIRS "../../../components/led" "../../../components/logger" "../../../components/lvgl" + "../../../components/sdcard" "../../../components/spi" "../../../components/task" "../../../components/touch" diff --git a/components/esp32-p4-wifi6-dev-kit/idf_component.yml b/components/esp32-p4-wifi6-dev-kit/idf_component.yml index c7984e052a..0482402f04 100644 --- a/components/esp32-p4-wifi6-dev-kit/idf_component.yml +++ b/components/esp32-p4-wifi6-dev-kit/idf_component.yml @@ -35,6 +35,7 @@ dependencies: espp/gt911: ">=1.0" espp/input_drivers: ">=1.0" espp/interrupt: ">=1.0" + espp/sdcard: ">=1.0" # MIPI-CSI camera pipeline: esp_video provides the V4L2 capture framework # (CSI + ISP) and esp_cam_sensor provides the OV5647 sensor driver. espressif/esp_video: ">=2.0,<2.4" # 2.4.0 fails to compile against the CI IDF (ISP_LL_EVENT_ERROR_MASK undeclared) diff --git a/components/esp32-p4-wifi6-dev-kit/include/esp32-p4-wifi6-dev-kit.hpp b/components/esp32-p4-wifi6-dev-kit/include/esp32-p4-wifi6-dev-kit.hpp index da0a6c3bb4..d1492297a8 100644 --- a/components/esp32-p4-wifi6-dev-kit/include/esp32-p4-wifi6-dev-kit.hpp +++ b/components/esp32-p4-wifi6-dev-kit/include/esp32-p4-wifi6-dev-kit.hpp @@ -14,7 +14,6 @@ #include #include #include -#include #include #include @@ -34,6 +33,7 @@ #include "ili9881.hpp" #include "interrupt.hpp" #include "jd9365.hpp" +#include "sdcard.hpp" #include "task.hpp" #include "touchpad_input.hpp" @@ -529,7 +529,13 @@ class Esp32P4Wifi6DevKit : public BaseComponent { bool is_sd_card_available() const { return sd_card_initialized_; } /// \return The SDMMC card handle, or nullptr if not initialized. - sdmmc_card_t *sdcard() const { return sdcard_; } + sdmmc_card_t *sdcard() const { return sdcard_ ? sdcard_->card() : nullptr; } + + /// Get the SD card component: mount() / unmount() (e.g. to hand the card to a + /// USB host with the usb_device MSC function), format(), card_info(), + /// volume_info(), ... + /// \return The component, or nullptr until initialize_sdcard() succeeded + espp::SdCard *sdcard_component() const { return sdcard_.get(); } /// Get total/free space of the mounted card. /// \param size_mb Optional out: total size in MB. @@ -718,11 +724,7 @@ class Esp32P4Wifi6DevKit : public BaseComponent { static constexpr gpio_num_t sd_d3_io = GPIO_NUM_42; std::atomic sd_card_initialized_{false}; - sdmmc_card_t *sdcard_{nullptr}; - // SD power-control driver (on-chip LDO). Owned by this class: created in - // initialize_sdcard() and deleted there (sd_pwr_ctrl_del_on_chip_ldo) if the - // mount fails; a successful mount keeps it alive for the life of the card. - sd_pwr_ctrl_handle_t sd_pwr_ctrl_handle_{nullptr}; + std::unique_ptr sdcard_; ///////////////////////////////////////////////////////////////////////////// // Interrupts (used by the optional interrupt-driven touch path) diff --git a/components/esp32-p4-wifi6-dev-kit/src/sdcard.cpp b/components/esp32-p4-wifi6-dev-kit/src/sdcard.cpp index f413dde0b2..6ea23dea68 100644 --- a/components/esp32-p4-wifi6-dev-kit/src/sdcard.cpp +++ b/components/esp32-p4-wifi6-dev-kit/src/sdcard.cpp @@ -1,9 +1,5 @@ #include "esp32-p4-wifi6-dev-kit.hpp" -#include -#include -#include - namespace espp { bool Esp32P4Wifi6DevKit::initialize_sdcard(const SdCardConfig &config) { @@ -15,75 +11,54 @@ bool Esp32P4Wifi6DevKit::initialize_sdcard(const SdCardConfig &config) { } logger_.info("Initializing SD card (4-bit SDMMC)"); - - esp_vfs_fat_sdmmc_mount_config_t mount_config{}; - mount_config.format_if_mount_failed = config.format_if_mount_failed; - mount_config.max_files = config.max_files; - mount_config.allocation_unit_size = config.allocation_unit_size; - - sdmmc_host_t host = SDMMC_HOST_DEFAULT(); - host.max_freq_khz = SDMMC_FREQ_HIGHSPEED; - host.slot = SDMMC_HOST_SLOT_0; - - // The ESP32-P4 powers the SD card via an internal LDO (LDO_VO4). Configure the - // power control handle so the host can enable that rail. - sd_pwr_ctrl_ldo_config_t ldo_config{}; - ldo_config.ldo_chan_id = sd_ldo_channel; - sd_pwr_ctrl_handle_t pwr_ctrl_handle = nullptr; - esp_err_t ret = sd_pwr_ctrl_new_on_chip_ldo(&ldo_config, &pwr_ctrl_handle); - if (ret != ESP_OK) { - logger_.error("Failed to create SD power control driver: {}", esp_err_to_name(ret)); - return false; - } - host.pwr_ctrl_handle = pwr_ctrl_handle; - sd_pwr_ctrl_handle_ = pwr_ctrl_handle; - - sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT(); - slot_config.width = 4; - slot_config.clk = sd_clk_io; - slot_config.cmd = sd_cmd_io; - slot_config.d0 = sd_d0_io; - slot_config.d1 = sd_d1_io; - slot_config.d2 = sd_d2_io; - slot_config.d3 = sd_d3_io; - - logger_.debug("Mounting filesystem"); - ret = esp_vfs_fat_sdmmc_mount(mount_point, &host, &slot_config, &mount_config, &sdcard_); - - if (ret != ESP_OK) { - if (ret == ESP_FAIL) { - logger_.error("Failed to mount filesystem."); - } else { - logger_.warn("Failed to initialize the card ({}). " - "Make sure an SD card is inserted.", - esp_err_to_name(ret)); - } - sd_pwr_ctrl_del_on_chip_ldo(pwr_ctrl_handle); - sd_pwr_ctrl_handle_ = nullptr; + espp::SdCard::SdmmcConfig sdmmc; + sdmmc.slot = SDMMC_HOST_SLOT_0; + sdmmc.bus_width = 4; + sdmmc.clk = sd_clk_io; + sdmmc.cmd = sd_cmd_io; + sdmmc.d0 = sd_d0_io; + sdmmc.d1 = sd_d1_io; + sdmmc.d2 = sd_d2_io; + sdmmc.d3 = sd_d3_io; + sdmmc.frequency_khz = SDMMC_FREQ_HIGHSPEED; // 40 MHz + // The ESP32-P4 powers the SD card via an internal LDO (LDO_VO4): without it + // the bus floats and card init fails. + sdmmc.ldo_channel = sd_ldo_channel; + sdcard_ = std::make_unique(espp::SdCard::Config{ + .interface = sdmmc, + .mount_point = mount_point, + .format_if_mount_failed = config.format_if_mount_failed, + .max_files = config.max_files, + .allocation_unit_size = config.allocation_unit_size, + .log_level = get_log_level(), + }); + std::error_code ec; + if (!sdcard_->initialize(ec)) { + logger_.error("Failed to initialize the SD card: {}. Make sure an SD card is inserted.", + ec.message()); + sdcard_.reset(); return false; } - - logger_.info("Filesystem mounted"); - sdmmc_card_print_info(stdout, sdcard_); + logger_.info("Filesystem mounted at {}", mount_point); + sdcard_->print_info(stdout); sd_card_initialized_ = true; return true; } bool Esp32P4Wifi6DevKit::get_sd_card_info(uint32_t *size_mb, uint32_t *free_mb) const { - if (!sd_card_initialized_) { + if (!sd_card_initialized_ || !sdcard_) { return false; } - uint64_t total_bytes = 0, free_bytes = 0; - esp_err_t ret = esp_vfs_fat_info(mount_point, &total_bytes, &free_bytes); - if (ret != ESP_OK) { - logger_.error("Failed to get SD card information ({})", esp_err_to_name(ret)); + const auto volume = sdcard_->volume_info(); + if (!volume) { + logger_.error("Failed to get SD card information (volume not mounted)"); return false; } if (size_mb) { - *size_mb = total_bytes / (1024 * 1024); + *size_mb = volume->total_bytes / (1024 * 1024); } if (free_mb) { - *free_mb = free_bytes / (1024 * 1024); + *free_mb = volume->free_bytes / (1024 * 1024); } return true; } diff --git a/components/lilygo-t5-47/CMakeLists.txt b/components/lilygo-t5-47/CMakeLists.txt index 93e646569e..3970178b08 100644 --- a/components/lilygo-t5-47/CMakeLists.txt +++ b/components/lilygo-t5-47/CMakeLists.txt @@ -1,6 +1,6 @@ idf_component_register( INCLUDE_DIRS "include" SRC_DIRS "src" - REQUIRES driver esp_driver_gpio esp_driver_spi esp_driver_i2c fatfs base_component task interrupt i2c spi gt911 bm8563 bq27220 pca9535 sx126x input_drivers lvgl epdiy + REQUIRES driver esp_driver_gpio esp_driver_spi esp_driver_i2c fatfs base_component task interrupt i2c sdcard spi gt911 bm8563 bq27220 pca9535 sx126x input_drivers lvgl epdiy REQUIRED_IDF_TARGETS "esp32s3" ) diff --git a/components/lilygo-t5-47/example/CMakeLists.txt b/components/lilygo-t5-47/example/CMakeLists.txt index 6f8fe49b38..270fc2f273 100644 --- a/components/lilygo-t5-47/example/CMakeLists.txt +++ b/components/lilygo-t5-47/example/CMakeLists.txt @@ -22,6 +22,7 @@ set(EXTRA_COMPONENT_DIRS "../../../components/interrupt" "../../../components/logger" "../../../components/lvgl" + "../../../components/sdcard" "../../../components/spi" "../../../components/task" "../../../components/touch" diff --git a/components/lilygo-t5-47/idf_component.yml b/components/lilygo-t5-47/idf_component.yml index 2af55e148e..0bcb771b22 100644 --- a/components/lilygo-t5-47/idf_component.yml +++ b/components/lilygo-t5-47/idf_component.yml @@ -22,6 +22,7 @@ dependencies: espp/task: ">=1.0" espp/interrupt: ">=1.0" espp/i2c: ">=1.0" + espp/sdcard: ">=1.0" espp/spi: ">=1.0" espp/gt911: ">=1.0" espp/bm8563: ">=1.0" diff --git a/components/lilygo-t5-47/include/lilygo-t5-47.hpp b/components/lilygo-t5-47/include/lilygo-t5-47.hpp index bfcda824c3..6c2362abe5 100644 --- a/components/lilygo-t5-47/include/lilygo-t5-47.hpp +++ b/components/lilygo-t5-47/include/lilygo-t5-47.hpp @@ -8,7 +8,6 @@ #include #include -#include #include #include "base_component.hpp" @@ -18,6 +17,7 @@ #include "i2c.hpp" #include "interrupt.hpp" #include "pca9535.hpp" +#include "sdcard.hpp" #include "spi.hpp" #include "sx126x.hpp" #include "task.hpp" @@ -342,7 +342,13 @@ class LilyGoT547 : public BaseComponent { /// Get the mounted microSD card. /// \return Pointer to the sdmmc_card_t, or nullptr if not initialized - sdmmc_card_t *sdcard() const { return sdcard_; } + sdmmc_card_t *sdcard() const { return sdcard_ ? sdcard_->card() : nullptr; } + + /// Get the SD card component: mount() / unmount() (e.g. to hand the card to a + /// USB host with the usb_device MSC function), format(), card_info(), + /// volume_info(), ... + /// \return The component, or nullptr until initialize_sdcard() succeeded + espp::SdCard *sdcard_component() const { return sdcard_.get(); } ///////////////////////////////////////////////////////////////////////////// // LoRa Radio (SX1262) @@ -491,7 +497,7 @@ class LilyGoT547 : public BaseComponent { static constexpr int SPI_MAX_TRANSFER_BYTES = 4092; static constexpr int spi_queue_size = 6; std::unique_ptr spi_{nullptr}; - sdmmc_card_t *sdcard_{nullptr}; + std::unique_ptr sdcard_; // LoRa radio (SX1262) on the shared SPI bus. Pins from the T5 4.7" ePaper S3 // PRO factory firmware. diff --git a/components/lilygo-t5-47/src/sdcard.cpp b/components/lilygo-t5-47/src/sdcard.cpp index 431c35a07d..55501c6318 100644 --- a/components/lilygo-t5-47/src/sdcard.cpp +++ b/components/lilygo-t5-47/src/sdcard.cpp @@ -1,7 +1,5 @@ #include "lilygo-t5-47.hpp" -#include - using namespace espp; ///////////////////////////////////////////////////////////////////////////// @@ -49,38 +47,25 @@ bool LilyGoT547::initialize_sdcard(const LilyGoT547::SdCardConfig &config) { } logger_.info("Initializing microSD card (CS={})", static_cast(sdcard_cs)); - - esp_vfs_fat_sdmmc_mount_config_t mount_config; - memset(&mount_config, 0, sizeof(mount_config)); - mount_config.format_if_mount_failed = config.format_if_mount_failed; - mount_config.max_files = config.max_files; - mount_config.allocation_unit_size = config.allocation_unit_size; - - // The SPI bus is already initialized (init_spi_bus above), so use the host on - // our spi_num and only attach the card's chip-select on this slot. - sdmmc_host_t host = SDSPI_HOST_DEFAULT(); - host.slot = spi_num; - - sdspi_device_config_t slot_config = SDSPI_DEVICE_CONFIG_DEFAULT(); - slot_config.gpio_cs = sdcard_cs; - slot_config.host_id = static_cast(host.slot); - - logger_.debug("Mounting filesystem at {}", mount_point); - esp_err_t ret = - esp_vfs_fat_sdspi_mount(mount_point, &host, &slot_config, &mount_config, &sdcard_); - if (ret != ESP_OK) { - if (ret == ESP_FAIL) { - logger_.error("Failed to mount filesystem (set format_if_mount_failed to format the card)"); - } else { - logger_.error("Failed to initialize the microSD card ({}). Make sure the card is inserted " - "and the lines have pull-ups.", - esp_err_to_name(ret)); - } - sdcard_ = nullptr; + espp::SdCard::SpiConfig spi; + spi.host = spi_num; + spi.cs = sdcard_cs; + spi.initialize_bus = false; // the BSP owns the (shared) bus + sdcard_ = std::make_unique(espp::SdCard::Config{ + .interface = spi, + .mount_point = mount_point, + .format_if_mount_failed = config.format_if_mount_failed, + .max_files = config.max_files, + .allocation_unit_size = config.allocation_unit_size, + .log_level = get_log_level(), + }); + std::error_code ec; + if (!sdcard_->initialize(ec)) { + logger_.error("Failed to initialize the microSD card: {}", ec.message()); + sdcard_.reset(); return false; } - logger_.info("microSD card mounted at {}", mount_point); - sdmmc_card_print_info(stdout, sdcard_); + sdcard_->print_info(stdout); return true; } diff --git a/components/m5stack-cardputer/CMakeLists.txt b/components/m5stack-cardputer/CMakeLists.txt index 1cd2f3acd2..1a087c6c32 100755 --- a/components/m5stack-cardputer/CMakeLists.txt +++ b/components/m5stack-cardputer/CMakeLists.txt @@ -2,6 +2,6 @@ idf_component_register( INCLUDE_DIRS "include" SRC_DIRS "src" - REQUIRES driver esp_adc esp_driver_i2s fatfs base_component adc bmi270 display display_drivers gps i2c interrupt led math neopixel spi sx126x task + REQUIRES driver esp_adc esp_driver_i2s fatfs base_component adc bmi270 display display_drivers gps i2c interrupt led math neopixel sdcard spi sx126x task REQUIRED_IDF_TARGETS "esp32s3" ) diff --git a/components/m5stack-cardputer/idf_component.yml b/components/m5stack-cardputer/idf_component.yml index 3038df3025..c229aa6fb7 100755 --- a/components/m5stack-cardputer/idf_component.yml +++ b/components/m5stack-cardputer/idf_component.yml @@ -28,6 +28,7 @@ dependencies: espp/led: '>=1.0' espp/math: '>=1.0' espp/neopixel: '>=1.0' + espp/sdcard: '>=1.0' espp/spi: '>=1.0' espp/sx126x: '>=1.0' espp/task: '>=1.0' diff --git a/components/m5stack-cardputer/include/m5stack-cardputer.hpp b/components/m5stack-cardputer/include/m5stack-cardputer.hpp index 5df2948701..94489535b4 100755 --- a/components/m5stack-cardputer/include/m5stack-cardputer.hpp +++ b/components/m5stack-cardputer/include/m5stack-cardputer.hpp @@ -11,13 +11,11 @@ #include #include -#include #include #include #include #include -#include #include #include #include @@ -34,6 +32,7 @@ #include "led.hpp" #include "neopixel.hpp" #include "oneshot_adc.hpp" +#include "sdcard.hpp" #include "spi.hpp" #include "st7789.hpp" #include "sx126x.hpp" @@ -503,7 +502,13 @@ class M5StackCardputer : public BaseComponent { /// \return A pointer to the uSD card /// \note The uSD card is only available if it was successfully initialized /// and the mount point is valid - sdmmc_card_t *sdcard() const { return sdcard_; } + sdmmc_card_t *sdcard() const { return sdcard_ ? sdcard_->card() : nullptr; } + + /// Get the SD card component: mount() / unmount() (e.g. to hand the card to a + /// USB host with the usb_device MSC function), format(), card_info(), + /// volume_info(), ... + /// \return The component, or nullptr until initialize_sdcard() succeeded + espp::SdCard *sdcard_component() const { return sdcard_.get(); } ///////////////////////////////////////////////////////////////////////////// // RGB LED @@ -902,7 +907,7 @@ class M5StackCardputer : public BaseComponent { .log_level = espp::Logger::Verbosity::WARN}}; // sdcard - sdmmc_card_t *sdcard_{nullptr}; + std::unique_ptr sdcard_; // whether the (shared) uSD / LoRa Cap SPI bus has been initialized bool expansion_spi_bus_initialized_{false}; diff --git a/components/m5stack-cardputer/src/sdcard.cpp b/components/m5stack-cardputer/src/sdcard.cpp index 0377f795c9..20d72fcc2a 100755 --- a/components/m5stack-cardputer/src/sdcard.cpp +++ b/components/m5stack-cardputer/src/sdcard.cpp @@ -1,7 +1,5 @@ #include "m5stack-cardputer.hpp" -#include - using namespace espp; //////////////////////// @@ -23,44 +21,32 @@ bool M5StackCardputer::initialize_sdcard(const SdCardConfig &config) { return false; } - sdmmc_host_t host = SDSPI_HOST_DEFAULT(); - host.slot = sdcard_spi_num; - - sdspi_device_config_t slot_config = SDSPI_DEVICE_CONFIG_DEFAULT(); - slot_config.gpio_cs = sdcard_cs; - slot_config.host_id = static_cast(host.slot); - - esp_vfs_fat_sdmmc_mount_config_t mount_config; - memset(&mount_config, 0, sizeof(mount_config)); - mount_config.format_if_mount_failed = config.format_if_mount_failed; - mount_config.max_files = config.max_files; - mount_config.allocation_unit_size = config.allocation_unit_size; - - logger_.debug("Mounting filesystem"); - auto ret = esp_vfs_fat_sdspi_mount(mount_point, &host, &slot_config, &mount_config, &sdcard_); - - if (ret != ESP_OK) { - if (ret == ESP_FAIL) { - logger_.error("Failed to mount filesystem. If you want the card to be formatted, set the " - "format_if_mount_failed field in the SdCardConfig."); - } else { - logger_.error("Failed to initialize the card ({}). Make sure SD card lines have pull-up " - "resistors in place.", - esp_err_to_name(ret)); - } + espp::SdCard::SpiConfig spi; + spi.host = sdcard_spi_num; + spi.cs = sdcard_cs; + spi.initialize_bus = false; // the BSP owns the (shared) expansion bus + sdcard_ = std::make_unique(espp::SdCard::Config{ + .interface = spi, + .mount_point = mount_point, + .format_if_mount_failed = config.format_if_mount_failed, + .max_files = config.max_files, + .allocation_unit_size = config.allocation_unit_size, + .log_level = get_log_level(), + }); + std::error_code ec; + if (!sdcard_->initialize(ec)) { + logger_.error("Failed to initialize the SD card: {}", ec.message()); + // release the card (and its SPI device) before touching the bus + sdcard_.reset(); // only free the bus if the LoRa radio isn't using it if (!lora_) { spi_bus_free(sdcard_spi_num); expansion_spi_bus_initialized_ = false; } - sdcard_ = nullptr; return false; } - logger_.info("Filesystem mounted"); - - // Card has been initialized, print its properties - sdmmc_card_print_info(stdout, sdcard_); - + logger_.info("Filesystem mounted at {}", mount_point); + sdcard_->print_info(stdout); return true; } diff --git a/components/m5stack-tab5/CMakeLists.txt b/components/m5stack-tab5/CMakeLists.txt index 7d91d55ad7..0fb7ba7075 100755 --- a/components/m5stack-tab5/CMakeLists.txt +++ b/components/m5stack-tab5/CMakeLists.txt @@ -1,6 +1,6 @@ idf_component_register( INCLUDE_DIRS "include" SRC_DIRS "src" - REQUIRES driver esp_driver_i2s esp_driver_ppa esp_driver_sdmmc esp_driver_spi esp_lcd esp_video esp_cam_sensor fatfs base_component bmi270 codec display display_drivers gt911 i2c ina226 input_drivers interrupt pi4ioe5v rx8130ce st7123touch task touch + REQUIRES driver esp_driver_i2s esp_driver_ppa esp_driver_sdmmc esp_driver_spi esp_lcd esp_video esp_cam_sensor fatfs base_component bmi270 codec display display_drivers gt911 i2c ina226 input_drivers interrupt pi4ioe5v rx8130ce sdcard st7123touch task touch REQUIRED_IDF_TARGETS "esp32p4" ) diff --git a/components/m5stack-tab5/example/CMakeLists.txt b/components/m5stack-tab5/example/CMakeLists.txt index 7ac93c829a..408fc6bd6f 100644 --- a/components/m5stack-tab5/example/CMakeLists.txt +++ b/components/m5stack-tab5/example/CMakeLists.txt @@ -38,6 +38,7 @@ set(EXTRA_COMPONENT_DIRS "../../../components/lvgl" "../../../components/math" "../../../components/rx8130ce" + "../../../components/sdcard" "../../../components/pi4ioe5v" "../../../components/spi" "../../../components/st7123touch" diff --git a/components/m5stack-tab5/idf_component.yml b/components/m5stack-tab5/idf_component.yml index 25208b1dfe..8cc3854660 100644 --- a/components/m5stack-tab5/idf_component.yml +++ b/components/m5stack-tab5/idf_component.yml @@ -28,6 +28,7 @@ dependencies: espp/interrupt: ">=1.0" espp/pi4ioe5v: ">=1.0" espp/rx8130ce: ">=1.0" + espp/sdcard: ">=1.0" espp/st7123touch: ">=1.0" espp/task: ">=1.0" # MIPI-CSI camera pipeline: esp_video provides the V4L2 capture framework diff --git a/components/m5stack-tab5/include/m5stack-tab5.hpp b/components/m5stack-tab5/include/m5stack-tab5.hpp index c2acfca4a7..f18165f182 100644 --- a/components/m5stack-tab5/include/m5stack-tab5.hpp +++ b/components/m5stack-tab5/include/m5stack-tab5.hpp @@ -40,6 +40,7 @@ #include "led.hpp" #include "pi4ioe5v.hpp" #include "rx8130ce.hpp" +#include "sdcard.hpp" #include "st7121.hpp" #include "st7123.hpp" #include "st7123touch.hpp" @@ -610,7 +611,13 @@ class M5StackTab5 : public BaseComponent { /// \return A pointer to the uSD card /// \note The uSD card is only available if it was successfully initialized /// and the mount point is valid - sdmmc_card_t *sdcard() const { return sdcard_; } + sdmmc_card_t *sdcard() const { return sdcard_ ? sdcard_->card() : nullptr; } + + /// Get the SD card component: mount() / unmount() (e.g. to hand the card to a + /// USB host with the usb_device MSC function), format(), card_info(), + /// volume_info(), ... + /// \return The component, or nullptr until initialize_sdcard() succeeded + espp::SdCard *sdcard_component() const { return sdcard_.get(); } /// Get SD card info /// \param size_mb Pointer to store size in MB @@ -929,8 +936,7 @@ class M5StackTab5 : public BaseComponent { std::atomic sd_card_initialized_{false}; // uSD Card - sdmmc_card_t *sdcard_{nullptr}; - void *sd_pwr_ctrl_handle_{nullptr}; // sd_pwr_ctrl_handle_t (on-chip LDO) + std::unique_ptr sdcard_; // RTC std::atomic rtc_initialized_{false}; diff --git a/components/m5stack-tab5/src/sdcard.cpp b/components/m5stack-tab5/src/sdcard.cpp index aaa23c22a7..d42d0f335d 100644 --- a/components/m5stack-tab5/src/sdcard.cpp +++ b/components/m5stack-tab5/src/sdcard.cpp @@ -1,9 +1,5 @@ #include "m5stack-tab5.hpp" -#include -#include -#include - namespace espp { ///////////////////////////////////////////////////////////////////////////// @@ -17,104 +13,56 @@ bool M5StackTab5::initialize_sdcard(const M5StackTab5::SdCardConfig &config) { } logger_.info("Initializing SD card"); - - esp_err_t ret; - // Options for mounting the filesystem. If format_if_mount_failed is set to - // true, SD card will be partitioned and formatted in case when mounting - // fails. - esp_vfs_fat_sdmmc_mount_config_t mount_config; - memset(&mount_config, 0, sizeof(mount_config)); - mount_config.format_if_mount_failed = config.format_if_mount_failed; - mount_config.max_files = config.max_files; - mount_config.allocation_unit_size = config.allocation_unit_size; - - // Use settings defined above to initialize SD card and mount FAT filesystem. - // Note: esp_vfs_fat_sdmmc/sdspi_mount is all-in-one convenience functions. - // Please check its source code and implement error recovery when developing - // production applications. - logger_.debug("Using SDMMC peripheral"); - - // By default, SD card frequency is initialized to SDMMC_FREQ_DEFAULT (20MHz) - // For setting a specific frequency, use host.max_freq_khz (range 400kHz - 20MHz for SDSPI) - sdmmc_host_t host = SDMMC_HOST_DEFAULT(); - host.max_freq_khz = SDMMC_FREQ_HIGHSPEED; // 40MHz - host.slot = SDMMC_HOST_SLOT_0; - - // The ESP32-P4 powers the SD card's IO pads from its internal LDO (LDO_VO4). - // Without a power control handle that rail stays off, the bus floats and - // card init fails (timeouts, or errors on the first data transfer). Same as - // M5Stack's own Tab5 BSP. - if (sd_pwr_ctrl_handle_ == nullptr) { - sd_pwr_ctrl_ldo_config_t ldo_config{}; - ldo_config.ldo_chan_id = sd_ldo_channel; - sd_pwr_ctrl_handle_t pwr_ctrl_handle = nullptr; - ret = sd_pwr_ctrl_new_on_chip_ldo(&ldo_config, &pwr_ctrl_handle); - if (ret != ESP_OK) { - logger_.error("Failed to create the SD power control driver: {}", esp_err_to_name(ret)); - return false; - } - sd_pwr_ctrl_handle_ = pwr_ctrl_handle; - } - host.pwr_ctrl_handle = static_cast(sd_pwr_ctrl_handle_); - - // This initializes the slot without card detect (CD) and write protect (WP) signals. - // Modify slot_config.gpio_cd and slot_config.gpio_wp if your board has these signals. - sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT(); - slot_config.clk = sd_clk_io; - slot_config.cmd = sd_cmd_io; - slot_config.d0 = sd_dat0_io; - slot_config.d1 = sd_dat1_io; - slot_config.d2 = sd_dat2_io; - slot_config.d3 = sd_dat3_io; - slot_config.width = 4; - - logger_.debug("Mounting filesystem"); - ret = esp_vfs_fat_sdmmc_mount(mount_point, &host, &slot_config, &mount_config, &sdcard_); - - if (ret != ESP_OK) { - if (ret == ESP_FAIL) { - logger_.error("Failed to mount filesystem. "); - } else { - logger_.warn("Failed to initialize the card ({}). " - "Make sure SD card is present and lines have pull-up resistors in place.", - esp_err_to_name(ret)); - } - sd_pwr_ctrl_del_on_chip_ldo(static_cast(sd_pwr_ctrl_handle_)); - sd_pwr_ctrl_handle_ = nullptr; + espp::SdCard::SdmmcConfig sdmmc; + sdmmc.slot = SDMMC_HOST_SLOT_0; + sdmmc.bus_width = 4; + sdmmc.clk = sd_clk_io; + sdmmc.cmd = sd_cmd_io; + sdmmc.d0 = sd_dat0_io; + sdmmc.d1 = sd_dat1_io; + sdmmc.d2 = sd_dat2_io; + sdmmc.d3 = sd_dat3_io; + sdmmc.frequency_khz = SDMMC_FREQ_HIGHSPEED; // 40 MHz + // The ESP32-P4 powers the SD card's IO pads from its internal LDO (LDO_VO4): + // without it the bus floats and card init fails. Same as M5Stack's own BSP. + sdmmc.ldo_channel = sd_ldo_channel; + sdcard_ = std::make_unique(espp::SdCard::Config{ + .interface = sdmmc, + .mount_point = mount_point, + .format_if_mount_failed = config.format_if_mount_failed, + .max_files = config.max_files, + .allocation_unit_size = config.allocation_unit_size, + .log_level = get_log_level(), + }); + std::error_code ec; + if (!sdcard_->initialize(ec)) { + logger_.error("Failed to initialize the SD card: {}", ec.message()); + sdcard_.reset(); return false; } - - logger_.info("Filesystem mounted"); - - // Card has been initialized, print its properties - sdmmc_card_print_info(stdout, sdcard_); - + logger_.info("Filesystem mounted at {}", mount_point); + sdcard_->print_info(stdout); sd_card_initialized_ = true; - return true; } bool M5StackTab5::is_sd_card_available() const { return sd_card_initialized_; } bool M5StackTab5::get_sd_card_info(uint32_t *size_mb, uint32_t *free_mb) const { - if (!sd_card_initialized_) { + if (!sd_card_initialized_ || !sdcard_) { return false; } - - uint64_t total_bytes = 0, free_bytes = 0; - esp_err_t ret = esp_vfs_fat_info(mount_point, &total_bytes, &free_bytes); - if (ret != ESP_OK) { - logger_.error("Failed to get SD card information ({})", esp_err_to_name(ret)); + const auto volume = sdcard_->volume_info(); + if (!volume) { + logger_.error("Failed to get SD card information (volume not mounted)"); return false; } - if (size_mb) { - *size_mb = total_bytes / (1024 * 1024); + *size_mb = volume->total_bytes / (1024 * 1024); } if (free_mb) { - *free_mb = free_bytes / (1024 * 1024); + *free_mb = volume->free_bytes / (1024 * 1024); } - return true; } diff --git a/components/smartpanlee-sc01-plus/CMakeLists.txt b/components/smartpanlee-sc01-plus/CMakeLists.txt index 5dc307f54f..82d9ac7596 100755 --- a/components/smartpanlee-sc01-plus/CMakeLists.txt +++ b/components/smartpanlee-sc01-plus/CMakeLists.txt @@ -1,6 +1,6 @@ idf_component_register( INCLUDE_DIRS "include" SRC_DIRS "src" - REQUIRES driver esp_driver_i2s esp_lcd fatfs sdmmc base_component display display_drivers ft5x06 i2c input_drivers interrupt led task + REQUIRES driver esp_driver_i2s esp_lcd fatfs sdmmc base_component display display_drivers ft5x06 i2c input_drivers interrupt led sdcard task REQUIRED_IDF_TARGETS "esp32s3" ) diff --git a/components/smartpanlee-sc01-plus/idf_component.yml b/components/smartpanlee-sc01-plus/idf_component.yml index 68893ffa93..f75d6f0834 100755 --- a/components/smartpanlee-sc01-plus/idf_component.yml +++ b/components/smartpanlee-sc01-plus/idf_component.yml @@ -29,6 +29,7 @@ dependencies: espp/input_drivers: ">=1.0" espp/interrupt: ">=1.0" espp/led: ">=1.0" + espp/sdcard: ">=1.0" espp/task: ">=1.0" targets: - esp32s3 diff --git a/components/smartpanlee-sc01-plus/include/smartpanlee-sc01-plus.hpp b/components/smartpanlee-sc01-plus/include/smartpanlee-sc01-plus.hpp index 98fd5f7060..1240d448b0 100755 --- a/components/smartpanlee-sc01-plus/include/smartpanlee-sc01-plus.hpp +++ b/components/smartpanlee-sc01-plus/include/smartpanlee-sc01-plus.hpp @@ -23,6 +23,7 @@ #include "i2c.hpp" #include "interrupt.hpp" #include "led.hpp" +#include "sdcard.hpp" #include "st7796.hpp" #include "task.hpp" #include "touchpad_input.hpp" @@ -268,6 +269,16 @@ class SmartPanleeSc01Plus : public BaseComponent { /// \param config Mount configuration. /// \return True if the card was mounted successfully, false otherwise. bool initialize_sdcard(const SdCardConfig &config); + /// Get the mounted microSD card. + /// \return Pointer to the sdmmc_card_t, or nullptr if not initialized + sdmmc_card_t *sdcard() const { return sdcard_ ? sdcard_->card() : nullptr; } + + /// Get the SD card component: mount() / unmount() (e.g. to hand the card to a + /// USB host with the usb_device MSC function), format(), card_info(), + /// volume_info(), ... + /// \return The component, or nullptr until initialize_sdcard() succeeded + espp::SdCard *sdcard_component() const { return sdcard_.get(); } + /// Check whether the microSD card is currently mounted. /// \return True if the card is mounted and available. bool is_sd_card_available() const; @@ -384,6 +395,6 @@ class SmartPanleeSc01Plus : public BaseComponent { std::vector audio_tx_buffer_; bool sd_card_initialized_{false}; - sdmmc_card_t *sdcard_{nullptr}; + std::unique_ptr sdcard_; }; } // namespace espp diff --git a/components/smartpanlee-sc01-plus/src/smartpanlee-sc01-plus.cpp b/components/smartpanlee-sc01-plus/src/smartpanlee-sc01-plus.cpp index 92148b8c26..d961d7482e 100755 --- a/components/smartpanlee-sc01-plus/src/smartpanlee-sc01-plus.cpp +++ b/components/smartpanlee-sc01-plus/src/smartpanlee-sc01-plus.cpp @@ -14,12 +14,9 @@ #include #include -#include #include #include #include -#include -#include namespace espp { @@ -438,37 +435,34 @@ bool SmartPanleeSc01Plus::initialize_sdcard(const SmartPanleeSc01Plus::SdCardCon return false; } - esp_vfs_fat_sdmmc_mount_config_t mount_config = {}; - mount_config.format_if_mount_failed = config.format_if_mount_failed; - mount_config.max_files = config.max_files; - mount_config.allocation_unit_size = config.allocation_unit_size; - - sdmmc_host_t host = SDSPI_HOST_DEFAULT(); - host.slot = SPI2_HOST; - host.max_freq_khz = SDMMC_FREQ_HIGHSPEED; - - spi_bus_config_t bus_config = {}; - bus_config.mosi_io_num = sd_card_pins().mosi; - bus_config.miso_io_num = sd_card_pins().miso; - bus_config.sclk_io_num = sd_card_pins().clk; - bus_config.quadwp_io_num = GPIO_NUM_NC; - bus_config.quadhd_io_num = GPIO_NUM_NC; - bus_config.max_transfer_sz = 4 * 1024; - auto ret = spi_bus_initialize((spi_host_device_t)host.slot, &bus_config, SDSPI_DEFAULT_DMA); - if (ret != ESP_OK && ret != ESP_ERR_INVALID_STATE) { - logger_.error("Failed to initialize SD SPI bus ({})", esp_err_to_name(ret)); - return false; - } - - sdspi_device_config_t slot_config = SDSPI_DEVICE_CONFIG_DEFAULT(); - slot_config.gpio_cs = sd_card_pins().cs; - slot_config.host_id = (spi_host_device_t)host.slot; - - ret = esp_vfs_fat_sdspi_mount(mount_point, &host, &slot_config, &mount_config, &sdcard_); - if (ret != ESP_OK) { - logger_.error("Failed to initialize the card ({})", esp_err_to_name(ret)); + logger_.info("Initializing SD card"); + // The card is on its own SPI bus (SPI2): the component initializes the bus + // and frees it again when the card is released. + const auto pins = sd_card_pins(); + espp::SdCard::SpiConfig spi; + spi.host = SPI2_HOST; + spi.cs = pins.cs; + spi.initialize_bus = true; + spi.mosi = pins.mosi; + spi.miso = pins.miso; + spi.sclk = pins.clk; + spi.max_transfer_size = 4 * 1024; + spi.frequency_khz = SDMMC_FREQ_HIGHSPEED; + sdcard_ = std::make_unique(espp::SdCard::Config{ + .interface = spi, + .mount_point = mount_point, + .format_if_mount_failed = config.format_if_mount_failed, + .max_files = config.max_files, + .allocation_unit_size = config.allocation_unit_size, + .log_level = get_log_level(), + }); + std::error_code ec; + if (!sdcard_->initialize(ec)) { + logger_.error("Failed to initialize the SD card: {}", ec.message()); + sdcard_.reset(); return false; } + logger_.info("Filesystem mounted at {}", mount_point); sd_card_initialized_ = true; return true; @@ -479,23 +473,19 @@ bool SmartPanleeSc01Plus::initialize_sdcard() { return initialize_sdcard(SdCardC bool SmartPanleeSc01Plus::is_sd_card_available() const { return sd_card_initialized_; } bool SmartPanleeSc01Plus::get_sd_card_info(uint32_t *size_mb, uint32_t *free_mb) const { - if (!sd_card_initialized_) { + if (!sd_card_initialized_ || !sdcard_) { return false; } - - uint64_t total_bytes = 0; - uint64_t free_bytes = 0; - auto ret = esp_vfs_fat_info(mount_point, &total_bytes, &free_bytes); - if (ret != ESP_OK) { - logger_.error("Failed to get SD card information ({})", esp_err_to_name(ret)); + const auto volume = sdcard_->volume_info(); + if (!volume) { + logger_.error("Failed to get SD card information (volume not mounted)"); return false; } - if (size_mb) { - *size_mb = total_bytes / (1024 * 1024); + *size_mb = volume->total_bytes / (1024 * 1024); } if (free_mb) { - *free_mb = free_bytes / (1024 * 1024); + *free_mb = volume->free_bytes / (1024 * 1024); } return true; } diff --git a/components/t-deck/CMakeLists.txt b/components/t-deck/CMakeLists.txt index 8899f3c016..459213640d 100755 --- a/components/t-deck/CMakeLists.txt +++ b/components/t-deck/CMakeLists.txt @@ -2,6 +2,6 @@ idf_component_register( INCLUDE_DIRS "include" SRC_DIRS "src" - REQUIRES driver esp_driver_i2s esp_driver_spi base_component codec display display_drivers fatfs gps i2c input_drivers interrupt gt911 spi sx126x task t_keyboard + REQUIRES driver esp_driver_i2s esp_driver_spi base_component codec display display_drivers fatfs gps i2c input_drivers interrupt gt911 sdcard spi sx126x task t_keyboard REQUIRED_IDF_TARGETS "esp32s3" ) diff --git a/components/t-deck/idf_component.yml b/components/t-deck/idf_component.yml index f6df0d19db..f4094ee9a1 100755 --- a/components/t-deck/idf_component.yml +++ b/components/t-deck/idf_component.yml @@ -25,6 +25,7 @@ dependencies: espp/i2c: '>=1.0' espp/input_drivers: '>=1.0' espp/interrupt: '>=1.0' + espp/sdcard: '>=1.0' espp/spi: '>=1.0' espp/sx126x: '>=1.0' espp/task: '>=1.0' diff --git a/components/t-deck/include/t-deck.hpp b/components/t-deck/include/t-deck.hpp old mode 100755 new mode 100644 index 1f04a03c38..fee5e91940 --- a/components/t-deck/include/t-deck.hpp +++ b/components/t-deck/include/t-deck.hpp @@ -28,6 +28,7 @@ #include "interrupt.hpp" #include "led.hpp" #include "pointer_input.hpp" +#include "sdcard.hpp" #include "spi.hpp" #include "st7789.hpp" #include "sx126x.hpp" @@ -148,7 +149,13 @@ class TDeck : public BaseComponent { /// \return A pointer to the uSD card /// \note The uSD card is only available if it was successfully initialized /// and the mount point is valid - sdmmc_card_t *sdcard() const { return sdcard_; } + sdmmc_card_t *sdcard() const { return sdcard_ ? sdcard_->card() : nullptr; } + + /// Get the SD card component: mount() / unmount() (e.g. to hand the card to a + /// USB host with the usb_device MSC function), format(), card_info(), + /// volume_info(), ... + /// \return The component, or nullptr until initialize_sdcard() succeeded + espp::SdCard *sdcard_component() const { return sdcard_.get(); } ///////////////////////////////////////////////////////////////////////////// // LoRa Radio (SX1262, HPD16A module) @@ -714,7 +721,7 @@ class TDeck : public BaseComponent { .scl_pullup_en = GPIO_PULLUP_ENABLE}}; // sdcard - sdmmc_card_t *sdcard_{nullptr}; + std::unique_ptr sdcard_; espp::Interrupt::PinConfig touch_interrupt_pin_{ .gpio_num = touch_interrupt, diff --git a/components/t-deck/src/sdcard.cpp b/components/t-deck/src/sdcard.cpp index 08eee96332..1a89d2a245 100644 --- a/components/t-deck/src/sdcard.cpp +++ b/components/t-deck/src/sdcard.cpp @@ -12,64 +12,32 @@ bool TDeck::initialize_sdcard(const TDeck::SdCardConfig &config) { return false; } - // ensure that the SPI bus is initialized + // ensure that the SPI bus is initialized (shared with the display and the radio) if (!init_spi_bus()) { logger_.error("Failed to initialize SPI bus."); return false; } logger_.info("Initializing SD card"); - - esp_err_t ret; - // Options for mounting the filesystem. If format_if_mount_failed is set to - // true, SD card will be partitioned and formatted in case when mounting - // fails. - esp_vfs_fat_sdmmc_mount_config_t mount_config; - memset(&mount_config, 0, sizeof(mount_config)); - mount_config.format_if_mount_failed = config.format_if_mount_failed; - mount_config.max_files = config.max_files; - mount_config.allocation_unit_size = config.allocation_unit_size; - - // Use settings defined above to initialize SD card and mount FAT filesystem. - // Note: esp_vfs_fat_sdmmc/sdspi_mount is all-in-one convenience functions. - // Please check its source code and implement error recovery when developing - // production applications. - logger_.debug("Using SPI peripheral"); - - // By default, SD card frequency is initialized to SDMMC_FREQ_DEFAULT (20MHz) - // For setting a specific frequency, use host.max_freq_khz (range 400kHz - 20MHz for SDSPI) - // Example: for fixed frequency of 10MHz, use host.max_freq_khz = 10000; - sdmmc_host_t host = SDSPI_HOST_DEFAULT(); - host.slot = spi_num; - // host.max_freq_khz = 20 * 1000; - - // This initializes the slot without card detect (CD) and write protect (WP) signals. - // Modify slot_config.gpio_cd and slot_config.gpio_wp if your board has these signals. - spi_host_device_t host_id = (spi_host_device_t)host.slot; - sdspi_device_config_t slot_config = SDSPI_DEVICE_CONFIG_DEFAULT(); - slot_config.gpio_cs = sdcard_cs; - slot_config.host_id = host_id; - - logger_.debug("Mounting filesystem"); - ret = esp_vfs_fat_sdspi_mount(mount_point, &host, &slot_config, &mount_config, &sdcard_); - - if (ret != ESP_OK) { - if (ret == ESP_FAIL) { - logger_.error("Failed to mount filesystem."); - return false; - } else { - logger_.error("Failed to initialize the card ({}). " - "Make sure SD card lines have pull-up resistors in place.", - esp_err_to_name(ret)); - return false; - } + espp::SdCard::SpiConfig spi; + spi.host = spi_num; + spi.cs = sdcard_cs; + spi.initialize_bus = false; // the BSP owns the (shared) bus + sdcard_ = std::make_unique(espp::SdCard::Config{ + .interface = spi, + .mount_point = mount_point, + .format_if_mount_failed = config.format_if_mount_failed, + .max_files = config.max_files, + .allocation_unit_size = config.allocation_unit_size, + .log_level = get_log_level(), + }); + std::error_code ec; + if (!sdcard_->initialize(ec)) { + logger_.error("Failed to initialize the SD card: {}", ec.message()); + sdcard_.reset(); return false; } - - logger_.info("Filesystem mounted"); - - // Card has been initialized, print its properties - sdmmc_card_print_info(stdout, sdcard_); - + logger_.info("Filesystem mounted at {}", mount_point); + sdcard_->print_info(stdout); return true; } diff --git a/components/t-dongle-s3/CMakeLists.txt b/components/t-dongle-s3/CMakeLists.txt index db14222ada..b4adae0c7f 100644 --- a/components/t-dongle-s3/CMakeLists.txt +++ b/components/t-dongle-s3/CMakeLists.txt @@ -2,6 +2,6 @@ idf_component_register( INCLUDE_DIRS "include" SRC_DIRS "src" - REQUIRES driver fatfs base_component display display_drivers i2c interrupt led_strip spi task + REQUIRES driver fatfs base_component display display_drivers i2c interrupt led_strip sdcard spi task REQUIRED_IDF_TARGETS "esp32s3" ) diff --git a/components/t-dongle-s3/idf_component.yml b/components/t-dongle-s3/idf_component.yml index e17a06037c..c19ae88d7a 100644 --- a/components/t-dongle-s3/idf_component.yml +++ b/components/t-dongle-s3/idf_component.yml @@ -22,6 +22,7 @@ dependencies: espp/display_drivers: '>=1.0' espp/i2c: '>=1.0' espp/interrupt: '>=1.0' + espp/sdcard: '>=1.0' espp/led_strip: '>=1.0' espp/spi: '>=1.0' espp/task: '>=1.0' diff --git a/components/t-dongle-s3/include/t-dongle-s3.hpp b/components/t-dongle-s3/include/t-dongle-s3.hpp index 2355a9c6cc..bc7977960f 100644 --- a/components/t-dongle-s3/include/t-dongle-s3.hpp +++ b/components/t-dongle-s3/include/t-dongle-s3.hpp @@ -19,6 +19,7 @@ #include "interrupt.hpp" #include "led.hpp" #include "led_strip.hpp" +#include "sdcard.hpp" #include "spi.hpp" #include "st7789.hpp" @@ -231,7 +232,13 @@ class TDongleS3 : public BaseComponent { /// \return A pointer to the uSD card /// \note The uSD card is only available if it was successfully initialized /// and the mount point is valid - sdmmc_card_t *sdcard() const { return sdcard_; } + sdmmc_card_t *sdcard() const { return sdcard_ ? sdcard_->card() : nullptr; } + + /// Get the SD card component: mount() / unmount() (e.g. to hand the card to a + /// USB host with the usb_device MSC function), format(), card_info(), + /// volume_info(), ... + /// \return The component, or nullptr until initialize_sdcard() succeeded + espp::SdCard *sdcard_component() const { return sdcard_.get(); } protected: TDongleS3(); @@ -281,7 +288,7 @@ class TDongleS3 : public BaseComponent { static constexpr gpio_num_t sdcard_cmd = GPIO_NUM_16; // sdcard - sdmmc_card_t *sdcard_{nullptr}; + std::unique_ptr sdcard_; // Interrupts espp::Interrupt::PinConfig button_interrupt_pin_{ diff --git a/components/t-dongle-s3/src/sdcard.cpp b/components/t-dongle-s3/src/sdcard.cpp index e7bead9a44..7854c09575 100644 --- a/components/t-dongle-s3/src/sdcard.cpp +++ b/components/t-dongle-s3/src/sdcard.cpp @@ -13,58 +13,30 @@ bool TDongleS3::initialize_sdcard(const TDongleS3::SdCardConfig &config) { } logger_.info("Initializing SD card"); - - esp_err_t ret; - // Options for mounting the filesystem. If format_if_mount_failed is set to - // true, SD card will be partitioned and formatted in case when mounting - // fails. - esp_vfs_fat_sdmmc_mount_config_t mount_config; - memset(&mount_config, 0, sizeof(mount_config)); - mount_config.format_if_mount_failed = config.format_if_mount_failed; - mount_config.max_files = config.max_files; - mount_config.allocation_unit_size = config.allocation_unit_size; - - // Use settings defined above to initialize SD card and mount FAT filesystem. - // Note: esp_vfs_fat_sdmmc/sdspi_mount is all-in-one convenience functions. - // Please check its source code and implement error recovery when developing - // production applications. - logger_.debug("Using SDMMC peripheral"); - - // By default, SD card frequency is initialized to SDMMC_FREQ_DEFAULT (20MHz) - // For setting a specific frequency, use host.max_freq_khz (range 400kHz - 20MHz for SDSPI) - sdmmc_host_t host = SDMMC_HOST_DEFAULT(); - host.max_freq_khz = SDMMC_FREQ_HIGHSPEED; // 40MHz - - // This initializes the slot without card detect (CD) and write protect (WP) signals. - // Modify slot_config.gpio_cd and slot_config.gpio_wp if your board has these signals. - sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT(); - slot_config.clk = sdcard_clk; - slot_config.cmd = sdcard_cmd; - slot_config.d0 = sdcard_d0; - slot_config.d1 = sdcard_d1; - slot_config.d2 = sdcard_d2; - slot_config.d3 = sdcard_d3; - - logger_.debug("Mounting filesystem"); - ret = esp_vfs_fat_sdmmc_mount(mount_point, &host, &slot_config, &mount_config, &sdcard_); - - if (ret != ESP_OK) { - if (ret == ESP_FAIL) { - logger_.error("Failed to mount filesystem. "); - return false; - } else { - logger_.error("Failed to initialize the card ({}). " - "Make sure SD card lines have pull-up resistors in place.", - esp_err_to_name(ret)); - return false; - } + espp::SdCard::SdmmcConfig sdmmc; + sdmmc.bus_width = 4; + sdmmc.clk = sdcard_clk; + sdmmc.cmd = sdcard_cmd; + sdmmc.d0 = sdcard_d0; + sdmmc.d1 = sdcard_d1; + sdmmc.d2 = sdcard_d2; + sdmmc.d3 = sdcard_d3; + sdmmc.frequency_khz = SDMMC_FREQ_HIGHSPEED; // 40 MHz + sdcard_ = std::make_unique(espp::SdCard::Config{ + .interface = sdmmc, + .mount_point = mount_point, + .format_if_mount_failed = config.format_if_mount_failed, + .max_files = config.max_files, + .allocation_unit_size = config.allocation_unit_size, + .log_level = get_log_level(), + }); + std::error_code ec; + if (!sdcard_->initialize(ec)) { + logger_.error("Failed to initialize the SD card: {}", ec.message()); + sdcard_.reset(); return false; } - - logger_.info("Filesystem mounted"); - - // Card has been initialized, print its properties - sdmmc_card_print_info(stdout, sdcard_); - + logger_.info("Filesystem mounted at {}", mount_point); + sdcard_->print_info(stdout); return true; } diff --git a/components/ws-s3-geek/CMakeLists.txt b/components/ws-s3-geek/CMakeLists.txt index 395b2df185..95d9764daf 100644 --- a/components/ws-s3-geek/CMakeLists.txt +++ b/components/ws-s3-geek/CMakeLists.txt @@ -2,6 +2,6 @@ idf_component_register( INCLUDE_DIRS "include" SRC_DIRS "src" - REQUIRES driver fatfs base_component display display_drivers i2c interrupt spi task + REQUIRES driver fatfs base_component display display_drivers i2c interrupt sdcard spi task REQUIRED_IDF_TARGETS "esp32s3" ) diff --git a/components/ws-s3-geek/idf_component.yml b/components/ws-s3-geek/idf_component.yml index f30e569b77..a2928b7172 100644 --- a/components/ws-s3-geek/idf_component.yml +++ b/components/ws-s3-geek/idf_component.yml @@ -23,6 +23,7 @@ dependencies: espp/display_drivers: '>=1.0' espp/i2c: '>=1.0' espp/interrupt: '>=1.0' + espp/sdcard: '>=1.0' espp/spi: '>=1.0' espp/task: '>=1.0' targets: diff --git a/components/ws-s3-geek/include/ws-s3-geek.hpp b/components/ws-s3-geek/include/ws-s3-geek.hpp index 06a49918c3..70022fb6ee 100644 --- a/components/ws-s3-geek/include/ws-s3-geek.hpp +++ b/components/ws-s3-geek/include/ws-s3-geek.hpp @@ -6,11 +6,9 @@ #include #include -#include #include #include -#include #include #include #include @@ -18,6 +16,7 @@ #include "base_component.hpp" #include "interrupt.hpp" #include "led.hpp" +#include "sdcard.hpp" #include "spi.hpp" #include "st7789.hpp" @@ -187,7 +186,13 @@ class WsS3Geek : public BaseComponent { /// \return A pointer to the uSD card /// \note The uSD card is only available if it was successfully initialized /// and the mount point is valid - sdmmc_card_t *sdcard() const { return sdcard_; } + sdmmc_card_t *sdcard() const { return sdcard_ ? sdcard_->card() : nullptr; } + + /// Get the SD card component: mount() / unmount() (e.g. to hand the card to a + /// USB host with the usb_device MSC function), format(), card_info(), + /// volume_info(), ... + /// \return The component, or nullptr until initialize_sdcard() succeeded + espp::SdCard *sdcard_component() const { return sdcard_.get(); } protected: WsS3Geek(); @@ -234,7 +239,7 @@ class WsS3Geek : public BaseComponent { static constexpr gpio_num_t sdcard_mosi = GPIO_NUM_35; // same as cmd // sdcard - sdmmc_card_t *sdcard_{nullptr}; + std::unique_ptr sdcard_; // Interrupts espp::Interrupt::PinConfig button_interrupt_pin_{ diff --git a/components/ws-s3-geek/src/sdcard.cpp b/components/ws-s3-geek/src/sdcard.cpp index 5e08929345..9e3e9fdc69 100644 --- a/components/ws-s3-geek/src/sdcard.cpp +++ b/components/ws-s3-geek/src/sdcard.cpp @@ -13,90 +13,31 @@ bool WsS3Geek::initialize_sdcard(const WsS3Geek::SdCardConfig &config) { } logger_.info("Initializing SD card"); - - esp_err_t ret; - // Options for mounting the filesystem. If format_if_mount_failed is set to - // true, SD card will be partitioned and formatted in case when mounting - // fails. - esp_vfs_fat_sdmmc_mount_config_t mount_config; - memset(&mount_config, 0, sizeof(mount_config)); - mount_config.format_if_mount_failed = config.format_if_mount_failed; - mount_config.max_files = config.max_files; - mount_config.allocation_unit_size = config.allocation_unit_size; - - spi_bus_config_t bus_cfg; - memset(&bus_cfg, 0, sizeof(bus_cfg)); - bus_cfg.mosi_io_num = sdcard_mosi; - bus_cfg.miso_io_num = sdcard_miso; - bus_cfg.sclk_io_num = sdcard_clk; - bus_cfg.quadwp_io_num = -1; - bus_cfg.quadhd_io_num = -1; - bus_cfg.max_transfer_sz = SPI_MAX_TRANSFER_BYTES; - ret = spi_bus_initialize(sdcard_spi_num, &bus_cfg, - SDSPI_DEFAULT_DMA); // SPI_DMA_CH_AUTO); // SDSPI_DEFAULT_DMA); - if (ret != ESP_OK) { - logger_.error("Failed to initialize bus."); + // The card is driven in SPI mode on its own bus (SPI3): the component + // initializes the bus and frees it again when the card is released. + espp::SdCard::SpiConfig spi; + spi.host = sdcard_spi_num; + spi.cs = sdcard_cs; + spi.initialize_bus = true; + spi.mosi = sdcard_mosi; + spi.miso = sdcard_miso; + spi.sclk = sdcard_clk; + spi.max_transfer_size = SPI_MAX_TRANSFER_BYTES; + sdcard_ = std::make_unique(espp::SdCard::Config{ + .interface = spi, + .mount_point = mount_point, + .format_if_mount_failed = config.format_if_mount_failed, + .max_files = config.max_files, + .allocation_unit_size = config.allocation_unit_size, + .log_level = get_log_level(), + }); + std::error_code ec; + if (!sdcard_->initialize(ec)) { + logger_.error("Failed to initialize the SD card: {}", ec.message()); + sdcard_.reset(); return false; } - - // By default, SD card frequency is initialized to SDMMC_FREQ_DEFAULT (20MHz) - // For setting a specific frequency, use host.max_freq_khz (range 400kHz - 20MHz for SDSPI) - // Example: for fixed frequency of 10MHz, use host.max_freq_khz = 10000; - sdmmc_host_t host = SDSPI_HOST_DEFAULT(); - host.slot = sdcard_spi_num; // Use SPI1 host - // host.max_freq_khz = 20 * 1000; - - // This initializes the slot without card detect (CD) and write protect (WP) signals. - // Modify slot_config.gpio_cd and slot_config.gpio_wp if your board has these signals. - spi_host_device_t host_id = (spi_host_device_t)host.slot; - sdspi_device_config_t slot_config = SDSPI_DEVICE_CONFIG_DEFAULT(); - slot_config.gpio_cs = sdcard_cs; - slot_config.host_id = host_id; - - logger_.debug("Mounting filesystem"); - ret = esp_vfs_fat_sdspi_mount(mount_point, &host, &slot_config, &mount_config, &sdcard_); - - // // Use settings defined above to initialize SD card and mount FAT filesystem. - // // Note: esp_vfs_fat_sdmmc/sdspi_mount is all-in-one convenience functions. - // // Please check its source code and implement error recovery when developing - // // production applications. - // logger_.debug("Using SDMMC peripheral"); - - // // By default, SD card frequency is initialized to SDMMC_FREQ_DEFAULT (20MHz) - // // For setting a specific frequency, use host.max_freq_khz (range 400kHz - 20MHz for SDSPI) - // sdmmc_host_t host = SDMMC_HOST_DEFAULT(); - // host.max_freq_khz = SDMMC_FREQ_DEFAULT; - - // // This initializes the slot without card detect (CD) and write protect (WP) signals. - // // Modify slot_config.gpio_cd and slot_config.gpio_wp if your board has these signals. - // sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT(); - // slot_config.clk = sdcard_clk; - // slot_config.cmd = sdcard_cmd; - // slot_config.d0 = sdcard_d0; - // slot_config.d1 = sdcard_d1; - // slot_config.d2 = sdcard_d2; - // slot_config.d3 = sdcard_d3; - - // logger_.debug("Mounting filesystem"); - // ret = esp_vfs_fat_sdmmc_mount(mount_point, &host, &slot_config, &mount_config, &sdcard_); - - if (ret != ESP_OK) { - if (ret == ESP_FAIL) { - logger_.error("Failed to mount filesystem. "); - return false; - } else { - logger_.error("Failed to initialize the card ({}). " - "Make sure SD card lines have pull-up resistors in place.", - esp_err_to_name(ret)); - return false; - } - return false; - } - - logger_.info("Filesystem mounted"); - - // Card has been initialized, print its properties - sdmmc_card_print_info(stdout, sdcard_); - + logger_.info("Filesystem mounted at {}", mount_point); + sdcard_->print_info(stdout); return true; } diff --git a/components/ws-s3-lcd-1-47/CMakeLists.txt b/components/ws-s3-lcd-1-47/CMakeLists.txt index 3d6ce662fd..bd6e432bd4 100644 --- a/components/ws-s3-lcd-1-47/CMakeLists.txt +++ b/components/ws-s3-lcd-1-47/CMakeLists.txt @@ -2,6 +2,6 @@ idf_component_register( INCLUDE_DIRS "include" SRC_DIRS "src" - REQUIRES driver fatfs base_component display display_drivers i2c interrupt spi task neopixel + REQUIRES driver fatfs base_component display display_drivers i2c interrupt sdcard spi task neopixel REQUIRED_IDF_TARGETS "esp32s3" ) diff --git a/components/ws-s3-lcd-1-47/idf_component.yml b/components/ws-s3-lcd-1-47/idf_component.yml index f7ff6474cf..aa16ee637b 100644 --- a/components/ws-s3-lcd-1-47/idf_component.yml +++ b/components/ws-s3-lcd-1-47/idf_component.yml @@ -23,6 +23,7 @@ dependencies: espp/display_drivers: '>=1.0' espp/i2c: '>=1.0' espp/interrupt: '>=1.0' + espp/sdcard: '>=1.0' espp/spi: '>=1.0' espp/task: '>=1.0' targets: diff --git a/components/ws-s3-lcd-1-47/include/ws-s3-lcd-1-47.hpp b/components/ws-s3-lcd-1-47/include/ws-s3-lcd-1-47.hpp index 1f28c5f25a..123c5e3266 100644 --- a/components/ws-s3-lcd-1-47/include/ws-s3-lcd-1-47.hpp +++ b/components/ws-s3-lcd-1-47/include/ws-s3-lcd-1-47.hpp @@ -6,11 +6,9 @@ #include #include -#include #include #include -#include #include #include #include @@ -19,6 +17,7 @@ #include "interrupt.hpp" #include "led.hpp" #include "neopixel.hpp" +#include "sdcard.hpp" #include "spi.hpp" #include "st7789.hpp" @@ -203,7 +202,13 @@ class WsS3Lcd147 : public BaseComponent { /// \return A pointer to the uSD card /// \note The uSD card is only available if it was successfully initialized /// and the mount point is valid - sdmmc_card_t *sdcard() const { return sdcard_; } + sdmmc_card_t *sdcard() const { return sdcard_ ? sdcard_->card() : nullptr; } + + /// Get the SD card component: mount() / unmount() (e.g. to hand the card to a + /// USB host with the usb_device MSC function), format(), card_info(), + /// volume_info(), ... + /// \return The component, or nullptr until initialize_sdcard() succeeded + espp::SdCard *sdcard_component() const { return sdcard_.get(); } ///////////////////////////////////////////////////////////////////////////// // LED @@ -270,7 +275,7 @@ class WsS3Lcd147 : public BaseComponent { static constexpr gpio_num_t rgb_led_io = GPIO_NUM_38; // sdcard - sdmmc_card_t *sdcard_{nullptr}; + std::unique_ptr sdcard_; std::shared_ptr led_{nullptr}; // Interrupts diff --git a/components/ws-s3-lcd-1-47/src/sdcard.cpp b/components/ws-s3-lcd-1-47/src/sdcard.cpp index f90a6f8355..7ff8bedf6a 100644 --- a/components/ws-s3-lcd-1-47/src/sdcard.cpp +++ b/components/ws-s3-lcd-1-47/src/sdcard.cpp @@ -13,58 +13,30 @@ bool WsS3Lcd147::initialize_sdcard(const WsS3Lcd147::SdCardConfig &config) { } logger_.info("Initializing SD card"); - - esp_err_t ret; - // Options for mounting the filesystem. If format_if_mount_failed is set to - // true, SD card will be partitioned and formatted in case when mounting - // fails. - esp_vfs_fat_sdmmc_mount_config_t mount_config; - memset(&mount_config, 0, sizeof(mount_config)); - mount_config.format_if_mount_failed = config.format_if_mount_failed; - mount_config.max_files = config.max_files; - mount_config.allocation_unit_size = config.allocation_unit_size; - - // Use settings defined above to initialize SD card and mount FAT filesystem. - // Note: esp_vfs_fat_sdmmc/sdspi_mount is all-in-one convenience functions. - // Please check its source code and implement error recovery when developing - // production applications. - logger_.debug("Using SDMMC peripheral"); - - // By default, SD card frequency is initialized to SDMMC_FREQ_DEFAULT (20MHz) - // For setting a specific frequency, use host.max_freq_khz (range 400kHz - 20MHz for SDSPI) - sdmmc_host_t host = SDMMC_HOST_DEFAULT(); - host.max_freq_khz = SDMMC_FREQ_HIGHSPEED; // 40MHz - - // This initializes the slot without card detect (CD) and write protect (WP) signals. - // Modify slot_config.gpio_cd and slot_config.gpio_wp if your board has these signals. - sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT(); - slot_config.clk = sdcard_clk; - slot_config.cmd = sdcard_cmd; - slot_config.d0 = sdcard_d0; - slot_config.d1 = sdcard_d1; - slot_config.d2 = sdcard_d2; - slot_config.d3 = sdcard_d3; - // card detect is not connected - slot_config.width = 4; - - logger_.debug("Mounting filesystem"); - ret = esp_vfs_fat_sdmmc_mount(mount_point, &host, &slot_config, &mount_config, &sdcard_); - - if (ret != ESP_OK) { - if (ret == ESP_FAIL) { - logger_.error("Failed to mount filesystem. "); - } else { - logger_.error("Failed to initialize the card ({}). " - "Make sure SD card lines have pull-up resistors in place.", - esp_err_to_name(ret)); - } + espp::SdCard::SdmmcConfig sdmmc; + sdmmc.bus_width = 4; + sdmmc.clk = sdcard_clk; + sdmmc.cmd = sdcard_cmd; + sdmmc.d0 = sdcard_d0; + sdmmc.d1 = sdcard_d1; + sdmmc.d2 = sdcard_d2; + sdmmc.d3 = sdcard_d3; + sdmmc.frequency_khz = SDMMC_FREQ_HIGHSPEED; // 40 MHz + sdcard_ = std::make_unique(espp::SdCard::Config{ + .interface = sdmmc, + .mount_point = mount_point, + .format_if_mount_failed = config.format_if_mount_failed, + .max_files = config.max_files, + .allocation_unit_size = config.allocation_unit_size, + .log_level = get_log_level(), + }); + std::error_code ec; + if (!sdcard_->initialize(ec)) { + logger_.error("Failed to initialize the SD card: {}", ec.message()); + sdcard_.reset(); return false; } - - logger_.info("Filesystem mounted"); - - // Card has been initialized, print its properties - sdmmc_card_print_info(stdout, sdcard_); - + logger_.info("Filesystem mounted at {}", mount_point); + sdcard_->print_info(stdout); return true; } diff --git a/components/xiao-esp32s3-sense/CMakeLists.txt b/components/xiao-esp32s3-sense/CMakeLists.txt index 009de75d7f..618f79855e 100644 --- a/components/xiao-esp32s3-sense/CMakeLists.txt +++ b/components/xiao-esp32s3-sense/CMakeLists.txt @@ -1,6 +1,6 @@ idf_component_register( INCLUDE_DIRS "include" SRC_DIRS "src" - REQUIRES base_component driver esp_driver_gpio esp_driver_i2s fatfs led math task + REQUIRES base_component driver esp_driver_gpio esp_driver_i2s fatfs led math sdcard task REQUIRED_IDF_TARGETS "esp32s3" ) diff --git a/components/xiao-esp32s3-sense/example/CMakeLists.txt b/components/xiao-esp32s3-sense/example/CMakeLists.txt index fd0422ea25..e2df283228 100644 --- a/components/xiao-esp32s3-sense/example/CMakeLists.txt +++ b/components/xiao-esp32s3-sense/example/CMakeLists.txt @@ -12,6 +12,7 @@ set(EXTRA_COMPONENT_DIRS "../../../components/logger" "../../../components/monitor" "../../../components/rtsp" + "../../../components/sdcard" "../../../components/socket" "../../../components/task" "../../../components/thread_pool" diff --git a/components/xiao-esp32s3-sense/idf_component.yml b/components/xiao-esp32s3-sense/idf_component.yml index 7e2fc1cb1f..64c3c7ad24 100644 --- a/components/xiao-esp32s3-sense/idf_component.yml +++ b/components/xiao-esp32s3-sense/idf_component.yml @@ -24,6 +24,7 @@ dependencies: espp/base_component: '>=1.0' espp/led: '>=1.0' espp/math: '>=1.0' + espp/sdcard: '>=1.0' espp/task: '>=1.0' targets: - esp32s3 diff --git a/components/xiao-esp32s3-sense/include/xiao-esp32s3-sense.hpp b/components/xiao-esp32s3-sense/include/xiao-esp32s3-sense.hpp index 3d2d3bd4c2..7a85330669 100644 --- a/components/xiao-esp32s3-sense/include/xiao-esp32s3-sense.hpp +++ b/components/xiao-esp32s3-sense/include/xiao-esp32s3-sense.hpp @@ -6,7 +6,6 @@ #include #include -#include #include #include @@ -15,6 +14,7 @@ #include "base_component.hpp" #include "gaussian.hpp" #include "led.hpp" +#include "sdcard.hpp" #include "task.hpp" namespace espp { @@ -121,7 +121,13 @@ class XiaoEsp32S3Sense : public BaseComponent { /// Get the mounted microSD card handle. /// \return Pointer to the mounted card, or nullptr if the card has not been /// initialized successfully. - sdmmc_card_t *sdcard() const { return sdcard_; } + sdmmc_card_t *sdcard() const { return sdcard_ ? sdcard_->card() : nullptr; } + + /// Get the SD card component: mount() / unmount() (e.g. to hand the card to a + /// USB host with the usb_device MSC function), format(), card_info(), + /// volume_info(), ... + /// \return The component, or nullptr until initialize_sdcard() succeeded + espp::SdCard *sdcard_component() const { return sdcard_.get(); } ///////////////////////////////////////////////////////////////////////////// // LED @@ -322,7 +328,7 @@ class XiaoEsp32S3Sense : public BaseComponent { .timer = LEDC_TIMER_1, .output_invert = true, }}; - sdmmc_card_t *sdcard_{nullptr}; + std::unique_ptr sdcard_; std::shared_ptr led_; std::unique_ptr led_task_; std::atomic breathing_period_{3.5f}; diff --git a/components/xiao-esp32s3-sense/src/xiao-esp32s3-sense.cpp b/components/xiao-esp32s3-sense/src/xiao-esp32s3-sense.cpp index 4e77356b5a..cc1e8f3cb1 100644 --- a/components/xiao-esp32s3-sense/src/xiao-esp32s3-sense.cpp +++ b/components/xiao-esp32s3-sense/src/xiao-esp32s3-sense.cpp @@ -2,8 +2,6 @@ #include -#include - using namespace espp; XiaoEsp32S3Sense::XiaoEsp32S3Sense() @@ -55,49 +53,32 @@ bool XiaoEsp32S3Sense::initialize_sdcard(const XiaoEsp32S3Sense::SdCardConfig &c } logger_.info("Initializing microSD card"); - - esp_vfs_fat_sdmmc_mount_config_t mount_config{}; - mount_config.format_if_mount_failed = config.format_if_mount_failed; - mount_config.max_files = config.max_files; - mount_config.allocation_unit_size = config.allocation_unit_size; - - spi_bus_config_t bus_config{}; - bus_config.mosi_io_num = sd_card_mosi_pin(); - bus_config.miso_io_num = sd_card_miso_pin(); - bus_config.sclk_io_num = sd_card_clk_pin(); - bus_config.quadwp_io_num = -1; - bus_config.quadhd_io_num = -1; - bus_config.max_transfer_sz = sd_card_spi_max_transfer_bytes_; - - esp_err_t ret = spi_bus_initialize(sd_card_spi_num_, &bus_config, SDSPI_DEFAULT_DMA); - if (ret != ESP_OK) { - logger_.error("Failed to initialize microSD SPI bus: {}", esp_err_to_name(ret)); - return false; - } - - sdmmc_host_t host = SDSPI_HOST_DEFAULT(); - host.slot = sd_card_spi_num_; - - sdspi_device_config_t slot_config = SDSPI_DEVICE_CONFIG_DEFAULT(); - slot_config.gpio_cs = sd_card_cs_pin(); - slot_config.host_id = sd_card_spi_num_; - - ret = esp_vfs_fat_sdspi_mount(mount_point, &host, &slot_config, &mount_config, &sdcard_); - if (ret != ESP_OK) { - if (ret == ESP_FAIL) { - logger_.error("Failed to mount microSD filesystem"); - } else { - logger_.error("Failed to initialize microSD card ({}). Make sure the card is inserted and " - "the bus has pull-ups.", - esp_err_to_name(ret)); - } - spi_bus_free(sd_card_spi_num_); - sdcard_ = nullptr; + // The card is on its own SPI bus: the component initializes the bus and + // frees it again when the card is released (or initialization fails). + espp::SdCard::SpiConfig spi; + spi.host = sd_card_spi_num_; + spi.cs = sd_card_cs_pin(); + spi.initialize_bus = true; + spi.mosi = sd_card_mosi_pin(); + spi.miso = sd_card_miso_pin(); + spi.sclk = sd_card_clk_pin(); + spi.max_transfer_size = sd_card_spi_max_transfer_bytes_; + sdcard_ = std::make_unique(espp::SdCard::Config{ + .interface = spi, + .mount_point = mount_point, + .format_if_mount_failed = config.format_if_mount_failed, + .max_files = config.max_files, + .allocation_unit_size = config.allocation_unit_size, + .log_level = get_log_level(), + }); + std::error_code ec; + if (!sdcard_->initialize(ec)) { + logger_.error("Failed to initialize the microSD card: {}", ec.message()); + sdcard_.reset(); return false; } - logger_.info("microSD filesystem mounted at {}", mount_point); - sdmmc_card_print_info(stdout, sdcard_); + sdcard_->print_info(stdout); return true; } From b33a93de85256da131fcca6a45d1e3a3e29bbe32 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Fri, 18 Sep 2026 07:59:35 -0500 Subject: [PATCH 4/9] fix(sdcard,bsp): review follow-ups on #800 - 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 Claude-Session: https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU --- .github/workflows/build.yml | 4 ++-- .github/workflows/upload_components.yml | 7 ++++++- components/esp32-p4-eth/include/esp32-p4-eth.hpp | 6 +++--- components/esp32-p4-eth/src/sdcard.cpp | 3 +-- .../include/esp32-p4-function-ev-board.hpp | 8 ++++---- components/esp32-p4-function-ev-board/src/sdcard.cpp | 3 +-- .../include/esp32-p4-module-dev-kit.hpp | 6 +++--- components/esp32-p4-module-dev-kit/src/sdcard.cpp | 3 +-- components/esp32-p4-nano/include/esp32-p4-nano.hpp | 6 +++--- components/esp32-p4-nano/src/sdcard.cpp | 3 +-- .../include/esp32-p4-wifi6-dev-kit.hpp | 6 +++--- components/esp32-p4-wifi6-dev-kit/src/sdcard.cpp | 3 +-- components/m5stack-tab5/include/m5stack-tab5.hpp | 6 +++--- components/m5stack-tab5/src/sdcard.cpp | 5 ++--- components/sdcard/example/main/Kconfig.projbuild | 12 ++++++++---- components/sdcard/example/main/sdcard_example.cpp | 6 +++++- .../include/smartpanlee-sc01-plus.hpp | 4 ++-- .../src/smartpanlee-sc01-plus.cpp | 5 ++--- doc/Doxyfile | 4 ++-- 19 files changed, 53 insertions(+), 47 deletions(-) mode change 100755 => 100644 components/smartpanlee-sc01-plus/src/smartpanlee-sc01-plus.cpp diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 32dd267dfd..46b2a5563e 100755 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -278,12 +278,12 @@ jobs: target: esp32 - path: 'components/rtsp/example' target: esp32 - - path: 'components/sdcard/example' - target: esp32s3 - path: 'components/runqueue/example' target: esp32 - path: 'components/rx8130ce/example' target: esp32s3 + - path: 'components/sdcard/example' + target: esp32s3 - path: 'components/seeed-studio-round-display/example' target: esp32s3 - path: 'components/serialization/example' diff --git a/.github/workflows/upload_components.yml b/.github/workflows/upload_components.yml index 7f028af3e7..4e2c23d66e 100755 --- a/.github/workflows/upload_components.yml +++ b/.github/workflows/upload_components.yml @@ -41,6 +41,11 @@ jobs: # ahead of its first-time dependents coredump / dispatcher / ota, # so it is uploaded to the registry before they try to resolve it. # + # Note: sdcard is intentionally listed (out of alphabetical order) ahead + # of the BSPs that depend on espp/sdcard (esp32-p4-*, lilygo-t5-47, + # m5stack-cardputer, m5stack-tab5, ...), so it is uploaded to the + # registry before they try to resolve it. + # # Note: comments are not allowed in the "components" list, so please # do not add any comments here. components: | @@ -89,6 +94,7 @@ jobs: components/drv2605 components/encoder components/esp-box + components/sdcard components/ethernet components/esp32-p4-eth components/esp32-p4-function-ev-board @@ -156,7 +162,6 @@ jobs: components/rtsp components/runqueue components/rx8130ce - components/sdcard components/seeed-studio-round-display components/serialization components/smartpanlee-sc01-plus diff --git a/components/esp32-p4-eth/include/esp32-p4-eth.hpp b/components/esp32-p4-eth/include/esp32-p4-eth.hpp index 345a592148..314a619631 100644 --- a/components/esp32-p4-eth/include/esp32-p4-eth.hpp +++ b/components/esp32-p4-eth/include/esp32-p4-eth.hpp @@ -457,8 +457,9 @@ class Esp32P4Eth : public BaseComponent { /// \return True if the card was successfully mounted at \c mount_point. bool initialize_sdcard(const SdCardConfig &config); - /// \return True if the SD card is present and mounted. - bool is_sd_card_available() const { return sd_card_initialized_; } + /// \return True if the SD card is initialized and its volume is currently + /// mounted (false after sdcard_component()->unmount()). + bool is_sd_card_available() const { return sdcard_ && sdcard_->is_mounted(); } /// \return The SDMMC card handle, or nullptr if not initialized. sdmmc_card_t *sdcard() const { return sdcard_ ? sdcard_->card() : nullptr; } @@ -637,7 +638,6 @@ class Esp32P4Eth : public BaseComponent { static constexpr gpio_num_t sd_d2_io = GPIO_NUM_41; static constexpr gpio_num_t sd_d3_io = GPIO_NUM_42; - std::atomic sd_card_initialized_{false}; std::unique_ptr sdcard_; ///////////////////////////////////////////////////////////////////////////// diff --git a/components/esp32-p4-eth/src/sdcard.cpp b/components/esp32-p4-eth/src/sdcard.cpp index a60b4ddaef..cce09f4c15 100644 --- a/components/esp32-p4-eth/src/sdcard.cpp +++ b/components/esp32-p4-eth/src/sdcard.cpp @@ -39,12 +39,11 @@ bool Esp32P4Eth::initialize_sdcard(const SdCardConfig &config) { } logger_.info("Filesystem mounted at {}", mount_point); sdcard_->print_info(stdout); - sd_card_initialized_ = true; return true; } bool Esp32P4Eth::get_sd_card_info(uint32_t *size_mb, uint32_t *free_mb) const { - if (!sd_card_initialized_ || !sdcard_) { + if (!sdcard_) { return false; } const auto volume = sdcard_->volume_info(); diff --git a/components/esp32-p4-function-ev-board/include/esp32-p4-function-ev-board.hpp b/components/esp32-p4-function-ev-board/include/esp32-p4-function-ev-board.hpp index 937ef04190..b709be20ae 100644 --- a/components/esp32-p4-function-ev-board/include/esp32-p4-function-ev-board.hpp +++ b/components/esp32-p4-function-ev-board/include/esp32-p4-function-ev-board.hpp @@ -344,9 +344,10 @@ class Esp32P4FunctionEvBoard : public BaseComponent { /// \return True if uSD card was successfully initialized bool initialize_sdcard(const SdCardConfig &config); - /// Check if SD card is present and mounted - /// \return True if SD card is available - bool is_sd_card_available() const { return sd_card_initialized_; } + /// Check if the SD card is initialized and its volume is currently mounted + /// \return True if the SD card is available (false after + /// sdcard_component()->unmount()) + bool is_sd_card_available() const { return sdcard_ && sdcard_->is_mounted(); } /// Get the uSD card handle /// \return A pointer to the uSD card, or nullptr if not initialized @@ -606,7 +607,6 @@ class Esp32P4FunctionEvBoard : public BaseComponent { std::atomic mic_volume_{70.0f}; // uSD card - std::atomic sd_card_initialized_{false}; std::unique_ptr sdcard_; #if CONFIG_ESP_P4_EV_BOARD_ETHERNET diff --git a/components/esp32-p4-function-ev-board/src/sdcard.cpp b/components/esp32-p4-function-ev-board/src/sdcard.cpp index 422973bf48..25244f078e 100644 --- a/components/esp32-p4-function-ev-board/src/sdcard.cpp +++ b/components/esp32-p4-function-ev-board/src/sdcard.cpp @@ -39,12 +39,11 @@ bool Esp32P4FunctionEvBoard::initialize_sdcard(const SdCardConfig &config) { } logger_.info("Filesystem mounted at {}", mount_point); sdcard_->print_info(stdout); - sd_card_initialized_ = true; return true; } bool Esp32P4FunctionEvBoard::get_sd_card_info(uint32_t *size_mb, uint32_t *free_mb) const { - if (!sd_card_initialized_ || !sdcard_) { + if (!sdcard_) { return false; } const auto volume = sdcard_->volume_info(); diff --git a/components/esp32-p4-module-dev-kit/include/esp32-p4-module-dev-kit.hpp b/components/esp32-p4-module-dev-kit/include/esp32-p4-module-dev-kit.hpp index d5681a6884..d8cd999850 100644 --- a/components/esp32-p4-module-dev-kit/include/esp32-p4-module-dev-kit.hpp +++ b/components/esp32-p4-module-dev-kit/include/esp32-p4-module-dev-kit.hpp @@ -481,8 +481,9 @@ class Esp32P4ModuleDevKit : public BaseComponent { /// \return True if the card was successfully mounted at \c mount_point. bool initialize_sdcard(const SdCardConfig &config); - /// \return True if the SD card is present and mounted. - bool is_sd_card_available() const { return sd_card_initialized_; } + /// \return True if the SD card is initialized and its volume is currently + /// mounted (false after sdcard_component()->unmount()). + bool is_sd_card_available() const { return sdcard_ && sdcard_->is_mounted(); } /// \return The SDMMC card handle, or nullptr if not initialized. sdmmc_card_t *sdcard() const { return sdcard_ ? sdcard_->card() : nullptr; } @@ -680,7 +681,6 @@ class Esp32P4ModuleDevKit : public BaseComponent { static constexpr gpio_num_t sd_d2_io = GPIO_NUM_41; static constexpr gpio_num_t sd_d3_io = GPIO_NUM_42; - std::atomic sd_card_initialized_{false}; std::unique_ptr sdcard_; ///////////////////////////////////////////////////////////////////////////// diff --git a/components/esp32-p4-module-dev-kit/src/sdcard.cpp b/components/esp32-p4-module-dev-kit/src/sdcard.cpp index 7b1d0e973d..94e953ab2b 100644 --- a/components/esp32-p4-module-dev-kit/src/sdcard.cpp +++ b/components/esp32-p4-module-dev-kit/src/sdcard.cpp @@ -41,12 +41,11 @@ bool Esp32P4ModuleDevKit::initialize_sdcard(const SdCardConfig &config) { } logger_.info("Filesystem mounted at {}", mount_point); sdcard_->print_info(stdout); - sd_card_initialized_ = true; return true; } bool Esp32P4ModuleDevKit::get_sd_card_info(uint32_t *size_mb, uint32_t *free_mb) const { - if (!sd_card_initialized_ || !sdcard_) { + if (!sdcard_) { return false; } const auto volume = sdcard_->volume_info(); diff --git a/components/esp32-p4-nano/include/esp32-p4-nano.hpp b/components/esp32-p4-nano/include/esp32-p4-nano.hpp index 4683b2fc76..b44ad73c08 100644 --- a/components/esp32-p4-nano/include/esp32-p4-nano.hpp +++ b/components/esp32-p4-nano/include/esp32-p4-nano.hpp @@ -458,8 +458,9 @@ class Esp32P4Nano : public BaseComponent { /// \return True if the card was successfully mounted at \c mount_point. bool initialize_sdcard(const SdCardConfig &config); - /// \return True if the SD card is present and mounted. - bool is_sd_card_available() const { return sd_card_initialized_; } + /// \return True if the SD card is initialized and its volume is currently + /// mounted (false after sdcard_component()->unmount()). + bool is_sd_card_available() const { return sdcard_ && sdcard_->is_mounted(); } /// \return The SDMMC card handle, or nullptr if not initialized. sdmmc_card_t *sdcard() const { return sdcard_ ? sdcard_->card() : nullptr; } @@ -638,7 +639,6 @@ class Esp32P4Nano : public BaseComponent { static constexpr gpio_num_t sd_d2_io = GPIO_NUM_41; static constexpr gpio_num_t sd_d3_io = GPIO_NUM_42; - std::atomic sd_card_initialized_{false}; std::unique_ptr sdcard_; ///////////////////////////////////////////////////////////////////////////// diff --git a/components/esp32-p4-nano/src/sdcard.cpp b/components/esp32-p4-nano/src/sdcard.cpp index efaad7125a..0bc2c385ed 100644 --- a/components/esp32-p4-nano/src/sdcard.cpp +++ b/components/esp32-p4-nano/src/sdcard.cpp @@ -39,12 +39,11 @@ bool Esp32P4Nano::initialize_sdcard(const SdCardConfig &config) { } logger_.info("Filesystem mounted at {}", mount_point); sdcard_->print_info(stdout); - sd_card_initialized_ = true; return true; } bool Esp32P4Nano::get_sd_card_info(uint32_t *size_mb, uint32_t *free_mb) const { - if (!sd_card_initialized_ || !sdcard_) { + if (!sdcard_) { return false; } const auto volume = sdcard_->volume_info(); diff --git a/components/esp32-p4-wifi6-dev-kit/include/esp32-p4-wifi6-dev-kit.hpp b/components/esp32-p4-wifi6-dev-kit/include/esp32-p4-wifi6-dev-kit.hpp index d1492297a8..7c2dc0cf13 100644 --- a/components/esp32-p4-wifi6-dev-kit/include/esp32-p4-wifi6-dev-kit.hpp +++ b/components/esp32-p4-wifi6-dev-kit/include/esp32-p4-wifi6-dev-kit.hpp @@ -525,8 +525,9 @@ class Esp32P4Wifi6DevKit : public BaseComponent { /// \return True if the card was successfully mounted at \c mount_point. bool initialize_sdcard(const SdCardConfig &config); - /// \return True if the SD card is present and mounted. - bool is_sd_card_available() const { return sd_card_initialized_; } + /// \return True if the SD card is initialized and its volume is currently + /// mounted (false after sdcard_component()->unmount()). + bool is_sd_card_available() const { return sdcard_ && sdcard_->is_mounted(); } /// \return The SDMMC card handle, or nullptr if not initialized. sdmmc_card_t *sdcard() const { return sdcard_ ? sdcard_->card() : nullptr; } @@ -723,7 +724,6 @@ class Esp32P4Wifi6DevKit : public BaseComponent { static constexpr gpio_num_t sd_d2_io = GPIO_NUM_41; static constexpr gpio_num_t sd_d3_io = GPIO_NUM_42; - std::atomic sd_card_initialized_{false}; std::unique_ptr sdcard_; ///////////////////////////////////////////////////////////////////////////// diff --git a/components/esp32-p4-wifi6-dev-kit/src/sdcard.cpp b/components/esp32-p4-wifi6-dev-kit/src/sdcard.cpp index 6ea23dea68..4ff315e2b7 100644 --- a/components/esp32-p4-wifi6-dev-kit/src/sdcard.cpp +++ b/components/esp32-p4-wifi6-dev-kit/src/sdcard.cpp @@ -41,12 +41,11 @@ bool Esp32P4Wifi6DevKit::initialize_sdcard(const SdCardConfig &config) { } logger_.info("Filesystem mounted at {}", mount_point); sdcard_->print_info(stdout); - sd_card_initialized_ = true; return true; } bool Esp32P4Wifi6DevKit::get_sd_card_info(uint32_t *size_mb, uint32_t *free_mb) const { - if (!sd_card_initialized_ || !sdcard_) { + if (!sdcard_) { return false; } const auto volume = sdcard_->volume_info(); diff --git a/components/m5stack-tab5/include/m5stack-tab5.hpp b/components/m5stack-tab5/include/m5stack-tab5.hpp index f18165f182..656e70b373 100644 --- a/components/m5stack-tab5/include/m5stack-tab5.hpp +++ b/components/m5stack-tab5/include/m5stack-tab5.hpp @@ -603,8 +603,9 @@ class M5StackTab5 : public BaseComponent { /// \return True if uSD card was successfully initialized bool initialize_sdcard(const SdCardConfig &config); - /// Check if SD card is present and mounted - /// \return True if SD card is available + /// Check if the SD card is initialized and its volume is currently mounted + /// \return True if the SD card is available (false after + /// sdcard_component()->unmount()) bool is_sd_card_available() const; /// Get the uSD card @@ -933,7 +934,6 @@ class M5StackTab5 : public BaseComponent { std::shared_ptr ioexp_0x44_; // Communication interfaces - std::atomic sd_card_initialized_{false}; // uSD Card std::unique_ptr sdcard_; diff --git a/components/m5stack-tab5/src/sdcard.cpp b/components/m5stack-tab5/src/sdcard.cpp index d42d0f335d..4ae58f4305 100644 --- a/components/m5stack-tab5/src/sdcard.cpp +++ b/components/m5stack-tab5/src/sdcard.cpp @@ -42,14 +42,13 @@ bool M5StackTab5::initialize_sdcard(const M5StackTab5::SdCardConfig &config) { } logger_.info("Filesystem mounted at {}", mount_point); sdcard_->print_info(stdout); - sd_card_initialized_ = true; return true; } -bool M5StackTab5::is_sd_card_available() const { return sd_card_initialized_; } +bool M5StackTab5::is_sd_card_available() const { return sdcard_ && sdcard_->is_mounted(); } bool M5StackTab5::get_sd_card_info(uint32_t *size_mb, uint32_t *free_mb) const { - if (!sd_card_initialized_ || !sdcard_) { + if (!sdcard_) { return false; } const auto volume = sdcard_->volume_info(); diff --git a/components/sdcard/example/main/Kconfig.projbuild b/components/sdcard/example/main/Kconfig.projbuild index 979d056033..b21d7411b9 100644 --- a/components/sdcard/example/main/Kconfig.projbuild +++ b/components/sdcard/example/main/Kconfig.projbuild @@ -16,10 +16,14 @@ menu "SD Card Example Configuration" endchoice if SDCARD_EXAMPLE_INTERFACE_SDMMC - config SDCARD_EXAMPLE_SDMMC_BUS_WIDTH - int "Bus width (1 or 4)" - range 1 4 - default 4 + choice SDCARD_EXAMPLE_SDMMC_BUS_WIDTH + prompt "Bus width" + default SDCARD_EXAMPLE_SDMMC_BUS_WIDTH_4 + config SDCARD_EXAMPLE_SDMMC_BUS_WIDTH_4 + bool "4-bit (CLK, CMD, D0-D3)" + config SDCARD_EXAMPLE_SDMMC_BUS_WIDTH_1 + bool "1-bit (CLK, CMD, D0)" + endchoice config SDCARD_EXAMPLE_SDMMC_CLK int "CLK GPIO" default 12 diff --git a/components/sdcard/example/main/sdcard_example.cpp b/components/sdcard/example/main/sdcard_example.cpp index c5dcf55f66..2ccfaa73d4 100644 --- a/components/sdcard/example/main/sdcard_example.cpp +++ b/components/sdcard/example/main/sdcard_example.cpp @@ -53,7 +53,11 @@ extern "C" void app_main(void) { config.log_level = espp::Logger::Verbosity::INFO; #if CONFIG_SDCARD_EXAMPLE_INTERFACE_SDMMC espp::SdCard::SdmmcConfig sdmmc; - sdmmc.bus_width = CONFIG_SDCARD_EXAMPLE_SDMMC_BUS_WIDTH; +#ifdef CONFIG_SDCARD_EXAMPLE_SDMMC_BUS_WIDTH_1 + sdmmc.bus_width = 1; +#else + sdmmc.bus_width = 4; +#endif sdmmc.clk = static_cast(CONFIG_SDCARD_EXAMPLE_SDMMC_CLK); sdmmc.cmd = static_cast(CONFIG_SDCARD_EXAMPLE_SDMMC_CMD); sdmmc.d0 = static_cast(CONFIG_SDCARD_EXAMPLE_SDMMC_D0); diff --git a/components/smartpanlee-sc01-plus/include/smartpanlee-sc01-plus.hpp b/components/smartpanlee-sc01-plus/include/smartpanlee-sc01-plus.hpp index 1240d448b0..5d2f74e7c3 100755 --- a/components/smartpanlee-sc01-plus/include/smartpanlee-sc01-plus.hpp +++ b/components/smartpanlee-sc01-plus/include/smartpanlee-sc01-plus.hpp @@ -279,7 +279,8 @@ class SmartPanleeSc01Plus : public BaseComponent { /// \return The component, or nullptr until initialize_sdcard() succeeded espp::SdCard *sdcard_component() const { return sdcard_.get(); } - /// Check whether the microSD card is currently mounted. + /// Check whether the microSD card is initialized and its volume is currently + /// mounted (false after sdcard_component()->unmount()). /// \return True if the card is mounted and available. bool is_sd_card_available() const; /// Query mounted microSD capacity and free space. @@ -394,7 +395,6 @@ class SmartPanleeSc01Plus : public BaseComponent { i2s_std_config_t audio_std_cfg_{}; std::vector audio_tx_buffer_; - bool sd_card_initialized_{false}; std::unique_ptr sdcard_; }; } // namespace espp diff --git a/components/smartpanlee-sc01-plus/src/smartpanlee-sc01-plus.cpp b/components/smartpanlee-sc01-plus/src/smartpanlee-sc01-plus.cpp old mode 100755 new mode 100644 index d961d7482e..89d559e786 --- a/components/smartpanlee-sc01-plus/src/smartpanlee-sc01-plus.cpp +++ b/components/smartpanlee-sc01-plus/src/smartpanlee-sc01-plus.cpp @@ -464,16 +464,15 @@ bool SmartPanleeSc01Plus::initialize_sdcard(const SmartPanleeSc01Plus::SdCardCon } logger_.info("Filesystem mounted at {}", mount_point); - sd_card_initialized_ = true; return true; } bool SmartPanleeSc01Plus::initialize_sdcard() { return initialize_sdcard(SdCardConfig{}); } -bool SmartPanleeSc01Plus::is_sd_card_available() const { return sd_card_initialized_; } +bool SmartPanleeSc01Plus::is_sd_card_available() const { return sdcard_ && sdcard_->is_mounted(); } bool SmartPanleeSc01Plus::get_sd_card_info(uint32_t *size_mb, uint32_t *free_mb) const { - if (!sd_card_initialized_ || !sdcard_) { + if (!sdcard_) { return false; } const auto volume = sdcard_->volume_info(); diff --git a/doc/Doxyfile b/doc/Doxyfile index 00285d056e..e4a54df928 100755 --- a/doc/Doxyfile +++ b/doc/Doxyfile @@ -173,10 +173,10 @@ EXAMPLE_PATH = \ $(PROJECT_PATH)/components/rmt/example/main/rmt_example.cpp \ $(PROJECT_PATH)/components/rtps/example/main/rtps_example.cpp \ $(PROJECT_PATH)/components/rtsp/example/main/rtsp_example.cpp \ - $(PROJECT_PATH)/components/sdcard/example/main/sdcard_example.cpp \ $(PROJECT_PATH)/components/ping/example/main/ping_example.cpp \ $(PROJECT_PATH)/components/runqueue/example/main/runqueue_example.cpp \ $(PROJECT_PATH)/components/rx8130ce/example/main/rx8130ce_example.cpp \ + $(PROJECT_PATH)/components/sdcard/example/main/sdcard_example.cpp \ $(PROJECT_PATH)/components/serialization/example/main/serialization_example.cpp \ $(PROJECT_PATH)/components/seeed-studio-round-display/example/main/seeed_studio_round_display_example.cpp \ $(PROJECT_PATH)/components/smartpanlee-sc01-plus/example/main/smartpanlee_sc01_plus_example.cpp \ @@ -443,9 +443,9 @@ INPUT = \ $(PROJECT_PATH)/components/rtsp/include/rtsp_client.hpp \ $(PROJECT_PATH)/components/rtsp/include/rtsp_server.hpp \ $(PROJECT_PATH)/components/rtsp/include/rtsp_session.hpp \ - $(PROJECT_PATH)/components/sdcard/include/sdcard.hpp \ $(PROJECT_PATH)/components/runqueue/include/runqueue.hpp \ $(PROJECT_PATH)/components/rx8130ce/include/rx8130ce.hpp \ + $(PROJECT_PATH)/components/sdcard/include/sdcard.hpp \ $(PROJECT_PATH)/components/serialization/include/serialization.hpp \ $(PROJECT_PATH)/components/seeed-studio-round-display/include/seeed-studio-round-display.hpp \ $(PROJECT_PATH)/components/smartpanlee-sc01-plus/include/smartpanlee-sc01-plus.hpp \ From db9cb0a6b582812563018b2e953a68bad8b13b21 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Fri, 18 Sep 2026 09:35:09 -0500 Subject: [PATCH 5/9] feat(usb_device): msc_example can expose an SD card (espp::SdCard pins 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 Claude-Session: https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU --- .../usb_device/msc_example/CMakeLists.txt | 18 +++- components/usb_device/msc_example/README.md | 21 ++++- .../msc_example/main/CMakeLists.txt | 2 +- .../msc_example/main/Kconfig.projbuild | 65 ++++++++++++++ .../msc_example/main/msc_example.cpp | 89 ++++++++++++++++--- 5 files changed, 178 insertions(+), 17 deletions(-) create mode 100644 components/usb_device/msc_example/main/Kconfig.projbuild diff --git a/components/usb_device/msc_example/CMakeLists.txt b/components/usb_device/msc_example/CMakeLists.txt index e15a282968..f5a381e57f 100644 --- a/components/usb_device/msc_example/CMakeLists.txt +++ b/components/usb_device/msc_example/CMakeLists.txt @@ -9,7 +9,23 @@ set(EXTRA_COMPONENT_DIRS "../../../components/base_component" "../../../components/format" "../../../components/logger" + "../../../components/sdcard" "../../../components/usb_device" + # The T-Dongle-S3 BSP (menuconfig: MSC_EXAMPLE_MEDIUM_BSP_T_DONGLE_S3) and the + # components it requires. The BSP is always compiled so the medium can be + # picked in menuconfig; it is only instantiated when selected. + "../../../components/t-dongle-s3" + "../../../components/cli" + "../../../components/color" + "../../../components/display" + "../../../components/display_drivers" + "../../../components/i2c" + "../../../components/interrupt" + "../../../components/led" + "../../../components/led_strip" + "../../../components/lvgl" + "../../../components/spi" + "../../../components/task" ) # With the component manager disabled (IDF_COMPONENT_MANAGER=0, e.g. in CI so the @@ -27,7 +43,7 @@ endif() set( COMPONENTS - "main esptool_py base_component format logger usb_device esp_tinyusb fatfs" + "main esptool_py base_component format logger sdcard usb_device esp_tinyusb fatfs t-dongle-s3" CACHE STRING "List of components to include" ) diff --git a/components/usb_device/msc_example/README.md b/components/usb_device/msc_example/README.md index a4bf56c932..5d8603da0d 100644 --- a/components/usb_device/msc_example/README.md +++ b/components/usb_device/msc_example/README.md @@ -92,7 +92,26 @@ idf.py erase-flash flash # or: esptool.py erase_region 0x110000 0x100000 ## Using an SD card instead -Initialize the card (SDMMC or SDSPI host) but do **not** mount it with +`idf.py menuconfig` → **MSC Example Configuration** → *Medium exposed as the USB +drive* picks what the host sees: + +- **FAT partition in flash** (default): the `storage` partition, as above. +- **SD card via espp::SdCard**: the card is probed on the configured SDMMC pins + (defaults: the LilyGo T-Dongle-S3's microSD slot, 4-bit) and handed to the + MSC function without being mounted by the app. +- **SD card of the LilyGo T-Dongle-S3 (BSP)**: `espp::TDongleS3` brings the card + up with `initialize_sdcard()` (which mounts it), the example releases the + volume with `sdcard_component()->unmount()`, and hands `sdcard()` over. Every + espp BSP with a microSD slot exposes the same two accessors, so this is the + pattern to copy for other boards. + +The example never formats an SD card: a card with no filesystem raises +`MscEvent::FormatRequired`; format it on the PC. Note that on a board whose only +USB port is the native one (the T-Dongle-S3), that port becomes the drive, so +the console is only visible through UART0; the drive showing up on the PC with +`boots.txt` and `README.txt` is the test. + +In code: initialize the card (SDMMC or SDSPI host) but do **not** mount it with `esp_vfs_fat_*_mount()` — the MSC function mounts it at `base_path` itself. `espp::SdCard` (the `sdcard` component) keeps those two steps apart, and every espp BSP with a microSD slot exposes its card through `sdcard()`: diff --git a/components/usb_device/msc_example/main/CMakeLists.txt b/components/usb_device/msc_example/main/CMakeLists.txt index c21b746938..2e5a6490e3 100644 --- a/components/usb_device/msc_example/main/CMakeLists.txt +++ b/components/usb_device/msc_example/main/CMakeLists.txt @@ -1,5 +1,5 @@ idf_component_register( SRC_DIRS "." INCLUDE_DIRS "." - REQUIRES usb_device esp_tinyusb fatfs + REQUIRES usb_device esp_tinyusb fatfs sdcard t-dongle-s3 ) diff --git a/components/usb_device/msc_example/main/Kconfig.projbuild b/components/usb_device/msc_example/main/Kconfig.projbuild new file mode 100644 index 0000000000..0aae1420d1 --- /dev/null +++ b/components/usb_device/msc_example/main/Kconfig.projbuild @@ -0,0 +1,65 @@ +menu "MSC Example Configuration" + + choice MSC_EXAMPLE_MEDIUM + prompt "Medium exposed as the USB drive" + default MSC_EXAMPLE_MEDIUM_FLASH + help + What the mass-storage interface hands to the host. + + config MSC_EXAMPLE_MEDIUM_FLASH + bool "FAT partition in flash ('storage' in partitions.csv)" + config MSC_EXAMPLE_MEDIUM_SDCARD + bool "SD card via espp::SdCard (SDMMC, pins below)" + help + Probes the card with espp::SdCard (no mount) and hands the raw card + to the MSC function. The pin defaults are the LilyGo T-Dongle-S3's + microSD slot. + config MSC_EXAMPLE_MEDIUM_BSP_T_DONGLE_S3 + bool "SD card of the LilyGo T-Dongle-S3 (BSP)" + help + Uses the espp::TDongleS3 board support: initialize_sdcard() mounts + the card, the example unmounts it through sdcard_component() and + hands sdcard() to the MSC function. This is the pattern for any + espp BSP with a microSD slot. + endchoice + + if MSC_EXAMPLE_MEDIUM_SDCARD + choice MSC_EXAMPLE_SDMMC_BUS_WIDTH + prompt "Bus width" + default MSC_EXAMPLE_SDMMC_BUS_WIDTH_4 + config MSC_EXAMPLE_SDMMC_BUS_WIDTH_4 + bool "4-bit (CLK, CMD, D0-D3)" + config MSC_EXAMPLE_SDMMC_BUS_WIDTH_1 + bool "1-bit (CLK, CMD, D0)" + endchoice + config MSC_EXAMPLE_SDMMC_CLK + int "CLK GPIO" + default 12 + config MSC_EXAMPLE_SDMMC_CMD + int "CMD GPIO" + default 16 + config MSC_EXAMPLE_SDMMC_D0 + int "D0 GPIO" + default 14 + config MSC_EXAMPLE_SDMMC_D1 + int "D1 GPIO" + default 17 + depends on MSC_EXAMPLE_SDMMC_BUS_WIDTH_4 + config MSC_EXAMPLE_SDMMC_D2 + int "D2 GPIO" + default 21 + depends on MSC_EXAMPLE_SDMMC_BUS_WIDTH_4 + config MSC_EXAMPLE_SDMMC_D3 + int "D3 GPIO" + default 18 + depends on MSC_EXAMPLE_SDMMC_BUS_WIDTH_4 + config MSC_EXAMPLE_SDMMC_LDO_CHANNEL + int "On-chip LDO channel powering the card (-1 = none)" + default 4 if IDF_TARGET_ESP32P4 + default -1 + help + The ESP32-P4 powers its SD pads from LDO channel 4. Leave -1 on + other targets. + endif + +endmenu diff --git a/components/usb_device/msc_example/main/msc_example.cpp b/components/usb_device/msc_example/main/msc_example.cpp index 8be0241b0b..4626d07689 100644 --- a/components/usb_device/msc_example/main/msc_example.cpp +++ b/components/usb_device/msc_example/main/msc_example.cpp @@ -1,15 +1,23 @@ // USB mass storage (MSC) example. // -// Exposes a FAT partition in the ESP32-S3's flash as a USB drive using -// espp::UsbDevice's MSC function, and shows the ownership model: the -// application reads and writes files through the VFS while it owns the medium, -// the host gets the drive when it mounts the device, and the application gets -// it back when the host ejects it (or the cable is unplugged). +// Exposes a FAT partition in the ESP32-S3's flash -- or an SD card, picked in +// menuconfig -- as a USB drive using espp::UsbDevice's MSC function, and shows +// the ownership model: the application reads and writes files through the VFS +// while it owns the medium, the host gets the drive when it mounts the device, +// and the application gets it back when the host ejects it (or the cable is +// unplugged). // // On boot the app writes a boot counter and a README to the volume. Plug the // native USB port into a PC: the drive appears with those files. Add or edit // files, then eject the drive: the app lists what it now sees, including the -// host's changes. An SD card works the same way (see the README). +// host's changes. +// +// The medium (menuconfig "MSC Example Configuration"): +// - a FAT partition in flash (default), +// - an SD card probed with espp::SdCard (no mount) on configurable SDMMC pins, +// - the SD card of the LilyGo T-Dongle-S3 through its BSP: initialize_sdcard() +// mounts it, sdcard_component()->unmount() releases the volume, sdcard() is +// handed to the MSC function -- the pattern for any espp BSP with a uSD slot. #include #include @@ -20,8 +28,13 @@ #include #include "logger.hpp" +#include "sdcard.hpp" #include "usb_device.hpp" +#if CONFIG_MSC_EXAMPLE_MEDIUM_BSP_T_DONGLE_S3 +#include "t-dongle-s3.hpp" +#endif + using namespace std::chrono_literals; using MscOwner = espp::UsbDevice::MscOwner; using MscEvent = espp::UsbDevice::MscEvent; @@ -101,18 +114,66 @@ extern "C" void app_main(void) { // would unmount /msc in the middle of the application's writes. cfg.connect_on_initialize = false; - espp::UsbDevice::MscMedium flash; - flash.type = espp::UsbDevice::MscMedium::Type::FlashPartition; - flash.partition_label = "storage"; // `data, fat` partition in partitions.csv - flash.base_path = kBasePath; - flash.volume_label = "ESPP MSC"; // the name the host shows for the drive + espp::UsbDevice::MscMedium medium; + medium.base_path = kBasePath; + medium.initial_owner = MscOwner::App; // write the boot files before a host takes it +#if CONFIG_MSC_EXAMPLE_MEDIUM_FLASH + medium.type = espp::UsbDevice::MscMedium::Type::FlashPartition; + medium.partition_label = "storage"; // `data, fat` partition in partitions.csv + medium.volume_label = "ESPP MSC"; // the name the host shows for the drive // Safe here: this is the only FAT volume on the device (see the header docs). - flash.format_if_unformatted = true; - flash.initial_owner = MscOwner::App; // write the boot files before a host takes it + medium.format_if_unformatted = true; +#else + // An SD card: initialized by the firmware, but NOT mounted -- the MSC function + // mounts it at base_path while the app owns it. Never formatted by the example + // (a card with no filesystem raises MscEvent::FormatRequired; format it on the + // PC). + medium.type = espp::UsbDevice::MscMedium::Type::SdCard; +#if CONFIG_MSC_EXAMPLE_MEDIUM_SDCARD + espp::SdCard::SdmmcConfig sdmmc; +#if CONFIG_MSC_EXAMPLE_SDMMC_BUS_WIDTH_1 + sdmmc.bus_width = 1; +#else + sdmmc.bus_width = 4; + sdmmc.d1 = static_cast(CONFIG_MSC_EXAMPLE_SDMMC_D1); + sdmmc.d2 = static_cast(CONFIG_MSC_EXAMPLE_SDMMC_D2); + sdmmc.d3 = static_cast(CONFIG_MSC_EXAMPLE_SDMMC_D3); +#endif + sdmmc.clk = static_cast(CONFIG_MSC_EXAMPLE_SDMMC_CLK); + sdmmc.cmd = static_cast(CONFIG_MSC_EXAMPLE_SDMMC_CMD); + sdmmc.d0 = static_cast(CONFIG_MSC_EXAMPLE_SDMMC_D0); + sdmmc.ldo_channel = CONFIG_MSC_EXAMPLE_SDMMC_LDO_CHANNEL; + espp::SdCard::Config sd_config; + sd_config.interface = sdmmc; + sd_config.mount_on_initialize = false; // probe only; the MSC function mounts it + sd_config.log_level = espp::Logger::Verbosity::INFO; + // Declared before the UsbDevice below so it outlives it. + static espp::SdCard sdcard(sd_config); + if (std::error_code sd_ec; !sdcard.initialize(sd_ec)) { + logger.error("Failed to initialize the SD card: {}", sd_ec.message()); + return; + } + sdcard.print_info(stdout); + medium.sd_card = sdcard.card(); +#else // CONFIG_MSC_EXAMPLE_MEDIUM_BSP_T_DONGLE_S3 + auto &board = espp::TDongleS3::get(); + // The BSP mounts the volume at its own mount point; release it so the MSC + // function can own the card (it re-mounts it at base_path for the app). + if (!board.initialize_sdcard({})) { + logger.error("Failed to initialize the SD card (is a FAT-formatted card inserted?)"); + return; + } + if (std::error_code sd_ec; !board.sdcard_component()->unmount(sd_ec)) { + logger.error("Failed to unmount the SD card: {}", sd_ec.message()); + return; + } + medium.sd_card = board.sdcard(); +#endif +#endif espp::UsbDevice::MscFunction msc; msc.interface_name = "espp MSC Example"; - msc.media = {flash}; + msc.media = {medium}; msc.auto_handover = true; // host takes the drive on mount, app gets it back on eject msc.on_event = [&](size_t lun, MscEvent event, MscOwner owner) { if (event == MscEvent::OwnerChanged) { From ab15905d56e549320d6728432b36cf73d23ba55d Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Fri, 18 Sep 2026 10:01:11 -0500 Subject: [PATCH 6/9] docs(usb_device): msc_example manager-off build needs the external/tinyusb submodule Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU --- components/usb_device/msc_example/CMakeLists.txt | 3 ++- components/usb_device/msc_example/README.md | 11 +++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/components/usb_device/msc_example/CMakeLists.txt b/components/usb_device/msc_example/CMakeLists.txt index f5a381e57f..c2ffb0d4df 100644 --- a/components/usb_device/msc_example/CMakeLists.txt +++ b/components/usb_device/msc_example/CMakeLists.txt @@ -36,8 +36,9 @@ set(EXTRA_COMPONENT_DIRS # must be discoverable. if(DEFINED ENV{IDF_COMPONENT_MANAGER} AND "$ENV{IDF_COMPONENT_MANAGER}" STREQUAL "0") list(APPEND EXTRA_COMPONENT_DIRS + "../../../components/sdcard" + "../../../external/esp-usb/device/esp_tinyusb" "../../../external/esp-usb/device/esp_tinyusb" - "../../../external/tinyusb" ) endif() diff --git a/components/usb_device/msc_example/README.md b/components/usb_device/msc_example/README.md index 5d8603da0d..5d57e5312e 100644 --- a/components/usb_device/msc_example/README.md +++ b/components/usb_device/msc_example/README.md @@ -27,6 +27,17 @@ idf.py -p flash monitor # console is on UART0 (USB-UART adapter) The console is on **UART0**: on the ESP32-S3 USB-Serial-JTAG shares the native USB port's PHY with USB-OTG, which the mass storage interface takes over. +The build works with the IDF component manager on (esp_tinyusb comes from the +registry) or off, which is how CI builds it and the way to go while a local espp +component the example uses is not published yet. With the manager off, +esp_tinyusb and TinyUSB come from the vendored submodules, so initialize them +first: + +```sh +git submodule update --init external/esp-usb external/tinyusb components/lvgl components/format/detail/fmt +IDF_COMPONENT_MANAGER=0 idf.py -p flash monitor +``` + The example's `sdkconfig.defaults` enables `CONFIG_TINYUSB_MSC_ENABLED`, uses a custom `partitions.csv` with a 1 MiB `storage` FAT partition, and selects **4096-byte wear-levelling sectors** with a matching MSC buffer: From c5b05c4991632a3a5e7dea00412e2f43a329e3cb Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Fri, 18 Sep 2026 10:25:44 -0500 Subject: [PATCH 7/9] fix(usb_device): restore external/tinyusb in the msc_example manager-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 Claude-Session: https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU --- components/usb_device/msc_example/CMakeLists.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/components/usb_device/msc_example/CMakeLists.txt b/components/usb_device/msc_example/CMakeLists.txt index c2ffb0d4df..f5a381e57f 100644 --- a/components/usb_device/msc_example/CMakeLists.txt +++ b/components/usb_device/msc_example/CMakeLists.txt @@ -36,9 +36,8 @@ set(EXTRA_COMPONENT_DIRS # must be discoverable. if(DEFINED ENV{IDF_COMPONENT_MANAGER} AND "$ENV{IDF_COMPONENT_MANAGER}" STREQUAL "0") list(APPEND EXTRA_COMPONENT_DIRS - "../../../components/sdcard" - "../../../external/esp-usb/device/esp_tinyusb" "../../../external/esp-usb/device/esp_tinyusb" + "../../../external/tinyusb" ) endif() From abd5425adecfe121271565bf4badd112c009a38b Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Fri, 18 Sep 2026 16:30:56 -0500 Subject: [PATCH 8/9] fix(sdcard): detach the FatFs drive before freeing the VFS FATFS on mount 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 Claude-Session: https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU --- components/sdcard/src/sdcard.cpp | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/components/sdcard/src/sdcard.cpp b/components/sdcard/src/sdcard.cpp index 4b9cfacd31..52360686d9 100644 --- a/components/sdcard/src/sdcard.cpp +++ b/components/sdcard/src/sdcard.cpp @@ -305,31 +305,38 @@ bool SdCard::mount_locked(std::error_code &ec) { return false; } + // f_mount() registers `fs` in FatFs's drive table even when the volume + // itself fails to mount, so every failure path below must detach the drive + // (f_mount(nullptr, ...)) before the VFS frees that FATFS object -- the same + // order as ESP-IDF's esp_vfs_fat_sdmmc_mount() cleanup. + auto detach = [&]() { + f_mount(nullptr, drive.c_str(), 0); + esp_vfs_fat_unregister_path(config_.mount_point.c_str()); + ff_diskio_unregister(pdrv); + }; + FRESULT res = f_mount(fs, drive.c_str(), 1); if (res == FR_NO_FILESYSTEM || res == FR_INT_ERR) { if (!config_.format_if_mount_failed) { logger_.error("No FAT filesystem on the card (format() it, or set " "format_if_mount_failed)"); - esp_vfs_fat_unregister_path(config_.mount_point.c_str()); - ff_diskio_unregister(pdrv); + detach(); ec = std::make_error_code(std::errc::no_such_device); return false; } logger_.warn("No FAT filesystem on the card; formatting it"); pdrv_ = pdrv; - if (!format_locked(ec)) { - pdrv_ = kNoDrive; - esp_vfs_fat_unregister_path(config_.mount_point.c_str()); - ff_diskio_unregister(pdrv); + const bool formatted = format_locked(ec); // sets ec on failure + pdrv_ = kNoDrive; + if (!formatted) { + detach(); return false; } - pdrv_ = kNoDrive; res = f_mount(fs, drive.c_str(), 1); } if (res != FR_OK) { logger_.error("Mounting the card failed (FatFs result {})", static_cast(res)); - esp_vfs_fat_unregister_path(config_.mount_point.c_str()); - ff_diskio_unregister(pdrv); + detach(); ec = std::make_error_code(std::errc::io_error); return false; } From b4686c2a27f243ec06b78d4c62ce5d660df3e75e Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Fri, 18 Sep 2026 16:36:13 -0500 Subject: [PATCH 9/9] fix(sdcard): allocate the format work buffer from internal RAM; document 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 Claude-Session: https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU --- components/esp32-p4-eth/include/esp32-p4-eth.hpp | 3 +++ .../include/esp32-p4-function-ev-board.hpp | 3 +++ .../include/esp32-p4-module-dev-kit.hpp | 3 +++ components/esp32-p4-nano/include/esp32-p4-nano.hpp | 3 +++ .../include/esp32-p4-wifi6-dev-kit.hpp | 3 +++ components/lilygo-t5-47/include/lilygo-t5-47.hpp | 5 ++++- .../m5stack-cardputer/include/m5stack-cardputer.hpp | 5 +++-- components/m5stack-tab5/include/m5stack-tab5.hpp | 5 +++-- components/sdcard/src/sdcard.cpp | 8 ++++++-- .../include/smartpanlee-sc01-plus.hpp | 5 ++++- components/t-deck/include/t-deck.hpp | 5 +++-- components/t-dongle-s3/include/t-dongle-s3.hpp | 5 +++-- components/usb_device/include/usb_device.hpp | 5 +++-- components/ws-s3-geek/include/ws-s3-geek.hpp | 5 +++-- components/ws-s3-lcd-1-47/include/ws-s3-lcd-1-47.hpp | 5 +++-- .../xiao-esp32s3-sense/include/xiao-esp32s3-sense.hpp | 7 +++++-- 16 files changed, 55 insertions(+), 20 deletions(-) diff --git a/components/esp32-p4-eth/include/esp32-p4-eth.hpp b/components/esp32-p4-eth/include/esp32-p4-eth.hpp index 314a619631..8c7ff2f725 100644 --- a/components/esp32-p4-eth/include/esp32-p4-eth.hpp +++ b/components/esp32-p4-eth/include/esp32-p4-eth.hpp @@ -462,6 +462,9 @@ class Esp32P4Eth : public BaseComponent { bool is_sd_card_available() const { return sdcard_ && sdcard_->is_mounted(); } /// \return The SDMMC card handle, or nullptr if not initialized. + /// \note nullptr until initialize_sdcard() succeeded. The card stays valid while + /// its volume is unmounted (sdcard_component()->unmount()), e.g. to hand it + /// to a USB host through espp::UsbDevice's MSC function. sdmmc_card_t *sdcard() const { return sdcard_ ? sdcard_->card() : nullptr; } /// Get the SD card component: mount() / unmount() (e.g. to hand the card to a diff --git a/components/esp32-p4-function-ev-board/include/esp32-p4-function-ev-board.hpp b/components/esp32-p4-function-ev-board/include/esp32-p4-function-ev-board.hpp index b709be20ae..8cd2b4e7c3 100644 --- a/components/esp32-p4-function-ev-board/include/esp32-p4-function-ev-board.hpp +++ b/components/esp32-p4-function-ev-board/include/esp32-p4-function-ev-board.hpp @@ -351,6 +351,9 @@ class Esp32P4FunctionEvBoard : public BaseComponent { /// Get the uSD card handle /// \return A pointer to the uSD card, or nullptr if not initialized + /// \note nullptr until initialize_sdcard() succeeded. The card stays valid while + /// its volume is unmounted (sdcard_component()->unmount()), e.g. to hand it + /// to a USB host through espp::UsbDevice's MSC function. sdmmc_card_t *sdcard() const { return sdcard_ ? sdcard_->card() : nullptr; } /// Get the SD card component: mount() / unmount() (e.g. to hand the card to a diff --git a/components/esp32-p4-module-dev-kit/include/esp32-p4-module-dev-kit.hpp b/components/esp32-p4-module-dev-kit/include/esp32-p4-module-dev-kit.hpp index d8cd999850..cd3f72d06c 100644 --- a/components/esp32-p4-module-dev-kit/include/esp32-p4-module-dev-kit.hpp +++ b/components/esp32-p4-module-dev-kit/include/esp32-p4-module-dev-kit.hpp @@ -486,6 +486,9 @@ class Esp32P4ModuleDevKit : public BaseComponent { bool is_sd_card_available() const { return sdcard_ && sdcard_->is_mounted(); } /// \return The SDMMC card handle, or nullptr if not initialized. + /// \note nullptr until initialize_sdcard() succeeded. The card stays valid while + /// its volume is unmounted (sdcard_component()->unmount()), e.g. to hand it + /// to a USB host through espp::UsbDevice's MSC function. sdmmc_card_t *sdcard() const { return sdcard_ ? sdcard_->card() : nullptr; } /// Get the SD card component: mount() / unmount() (e.g. to hand the card to a diff --git a/components/esp32-p4-nano/include/esp32-p4-nano.hpp b/components/esp32-p4-nano/include/esp32-p4-nano.hpp index b44ad73c08..5c1f71114b 100644 --- a/components/esp32-p4-nano/include/esp32-p4-nano.hpp +++ b/components/esp32-p4-nano/include/esp32-p4-nano.hpp @@ -463,6 +463,9 @@ class Esp32P4Nano : public BaseComponent { bool is_sd_card_available() const { return sdcard_ && sdcard_->is_mounted(); } /// \return The SDMMC card handle, or nullptr if not initialized. + /// \note nullptr until initialize_sdcard() succeeded. The card stays valid while + /// its volume is unmounted (sdcard_component()->unmount()), e.g. to hand it + /// to a USB host through espp::UsbDevice's MSC function. sdmmc_card_t *sdcard() const { return sdcard_ ? sdcard_->card() : nullptr; } /// Get the SD card component: mount() / unmount() (e.g. to hand the card to a diff --git a/components/esp32-p4-wifi6-dev-kit/include/esp32-p4-wifi6-dev-kit.hpp b/components/esp32-p4-wifi6-dev-kit/include/esp32-p4-wifi6-dev-kit.hpp index 7c2dc0cf13..33915832e0 100644 --- a/components/esp32-p4-wifi6-dev-kit/include/esp32-p4-wifi6-dev-kit.hpp +++ b/components/esp32-p4-wifi6-dev-kit/include/esp32-p4-wifi6-dev-kit.hpp @@ -530,6 +530,9 @@ class Esp32P4Wifi6DevKit : public BaseComponent { bool is_sd_card_available() const { return sdcard_ && sdcard_->is_mounted(); } /// \return The SDMMC card handle, or nullptr if not initialized. + /// \note nullptr until initialize_sdcard() succeeded. The card stays valid while + /// its volume is unmounted (sdcard_component()->unmount()), e.g. to hand it + /// to a USB host through espp::UsbDevice's MSC function. sdmmc_card_t *sdcard() const { return sdcard_ ? sdcard_->card() : nullptr; } /// Get the SD card component: mount() / unmount() (e.g. to hand the card to a diff --git a/components/lilygo-t5-47/include/lilygo-t5-47.hpp b/components/lilygo-t5-47/include/lilygo-t5-47.hpp index 6c2362abe5..2d127d3b6f 100644 --- a/components/lilygo-t5-47/include/lilygo-t5-47.hpp +++ b/components/lilygo-t5-47/include/lilygo-t5-47.hpp @@ -340,8 +340,11 @@ class LilyGoT547 : public BaseComponent { /// initialize_display(). bool initialize_sdcard(const SdCardConfig &config); - /// Get the mounted microSD card. + /// Get the microSD card. /// \return Pointer to the sdmmc_card_t, or nullptr if not initialized + /// \note nullptr until initialize_sdcard() succeeded. The card stays valid while + /// its volume is unmounted (sdcard_component()->unmount()), e.g. to hand it + /// to a USB host through espp::UsbDevice's MSC function. sdmmc_card_t *sdcard() const { return sdcard_ ? sdcard_->card() : nullptr; } /// Get the SD card component: mount() / unmount() (e.g. to hand the card to a diff --git a/components/m5stack-cardputer/include/m5stack-cardputer.hpp b/components/m5stack-cardputer/include/m5stack-cardputer.hpp index 94489535b4..b8d2b121fa 100755 --- a/components/m5stack-cardputer/include/m5stack-cardputer.hpp +++ b/components/m5stack-cardputer/include/m5stack-cardputer.hpp @@ -500,8 +500,9 @@ class M5StackCardputer : public BaseComponent { /// Get the uSD card /// \return A pointer to the uSD card - /// \note The uSD card is only available if it was successfully initialized - /// and the mount point is valid + /// \note nullptr until initialize_sdcard() succeeded. The card stays valid while + /// its volume is unmounted (sdcard_component()->unmount()), e.g. to hand it + /// to a USB host through espp::UsbDevice's MSC function. sdmmc_card_t *sdcard() const { return sdcard_ ? sdcard_->card() : nullptr; } /// Get the SD card component: mount() / unmount() (e.g. to hand the card to a diff --git a/components/m5stack-tab5/include/m5stack-tab5.hpp b/components/m5stack-tab5/include/m5stack-tab5.hpp index 656e70b373..4e11da0bdf 100644 --- a/components/m5stack-tab5/include/m5stack-tab5.hpp +++ b/components/m5stack-tab5/include/m5stack-tab5.hpp @@ -610,8 +610,9 @@ class M5StackTab5 : public BaseComponent { /// Get the uSD card /// \return A pointer to the uSD card - /// \note The uSD card is only available if it was successfully initialized - /// and the mount point is valid + /// \note nullptr until initialize_sdcard() succeeded. The card stays valid while + /// its volume is unmounted (sdcard_component()->unmount()), e.g. to hand it + /// to a USB host through espp::UsbDevice's MSC function. sdmmc_card_t *sdcard() const { return sdcard_ ? sdcard_->card() : nullptr; } /// Get the SD card component: mount() / unmount() (e.g. to hand the card to a diff --git a/components/sdcard/src/sdcard.cpp b/components/sdcard/src/sdcard.cpp index 52360686d9..9def1e9be4 100644 --- a/components/sdcard/src/sdcard.cpp +++ b/components/sdcard/src/sdcard.cpp @@ -2,6 +2,7 @@ #include +#include #include #include #include // SD_OCR_SDHC_CAP @@ -429,8 +430,11 @@ bool SdCard::format(std::error_code &ec) { bool SdCard::format_locked(std::error_code &ec) { // pdrv_ must be registered (not necessarily mounted) when this runs const std::string drive = fat_drive_string(pdrv_); + // Internal RAM: the SDMMC host DMAs from this buffer. (Not ff_memalloc(): FatFs + // only declares it when long file names live on the heap, and it may prefer + // PSRAM.) constexpr size_t kWorkBufferSize = 4096; - void *work = ff_memalloc(kWorkBufferSize); + void *work = heap_caps_malloc(kWorkBufferSize, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); if (!work) { ec = std::make_error_code(std::errc::not_enough_memory); return false; @@ -442,7 +446,7 @@ bool SdCard::format_locked(std::error_code &ec) { opt.au_size = config_.allocation_unit_size; logger_.info("Formatting the card (allocation unit {} bytes)", config_.allocation_unit_size); const FRESULT res = f_mkfs(drive.c_str(), &opt, work, kWorkBufferSize); - ff_memfree(work); + heap_caps_free(work); if (res != FR_OK) { logger_.error("Formatting failed (FatFs result {})", static_cast(res)); ec = std::make_error_code(std::errc::io_error); diff --git a/components/smartpanlee-sc01-plus/include/smartpanlee-sc01-plus.hpp b/components/smartpanlee-sc01-plus/include/smartpanlee-sc01-plus.hpp index 5d2f74e7c3..5c2e34b8c2 100755 --- a/components/smartpanlee-sc01-plus/include/smartpanlee-sc01-plus.hpp +++ b/components/smartpanlee-sc01-plus/include/smartpanlee-sc01-plus.hpp @@ -269,8 +269,11 @@ class SmartPanleeSc01Plus : public BaseComponent { /// \param config Mount configuration. /// \return True if the card was mounted successfully, false otherwise. bool initialize_sdcard(const SdCardConfig &config); - /// Get the mounted microSD card. + /// Get the microSD card. /// \return Pointer to the sdmmc_card_t, or nullptr if not initialized + /// \note nullptr until initialize_sdcard() succeeded. The card stays valid while + /// its volume is unmounted (sdcard_component()->unmount()), e.g. to hand it + /// to a USB host through espp::UsbDevice's MSC function. sdmmc_card_t *sdcard() const { return sdcard_ ? sdcard_->card() : nullptr; } /// Get the SD card component: mount() / unmount() (e.g. to hand the card to a diff --git a/components/t-deck/include/t-deck.hpp b/components/t-deck/include/t-deck.hpp index fee5e91940..6204baf9f3 100644 --- a/components/t-deck/include/t-deck.hpp +++ b/components/t-deck/include/t-deck.hpp @@ -147,8 +147,9 @@ class TDeck : public BaseComponent { /// Get the uSD card /// \return A pointer to the uSD card - /// \note The uSD card is only available if it was successfully initialized - /// and the mount point is valid + /// \note nullptr until initialize_sdcard() succeeded. The card stays valid while + /// its volume is unmounted (sdcard_component()->unmount()), e.g. to hand it + /// to a USB host through espp::UsbDevice's MSC function. sdmmc_card_t *sdcard() const { return sdcard_ ? sdcard_->card() : nullptr; } /// Get the SD card component: mount() / unmount() (e.g. to hand the card to a diff --git a/components/t-dongle-s3/include/t-dongle-s3.hpp b/components/t-dongle-s3/include/t-dongle-s3.hpp index bc7977960f..7bb2407ffc 100644 --- a/components/t-dongle-s3/include/t-dongle-s3.hpp +++ b/components/t-dongle-s3/include/t-dongle-s3.hpp @@ -230,8 +230,9 @@ class TDongleS3 : public BaseComponent { /// Get the uSD card /// \return A pointer to the uSD card - /// \note The uSD card is only available if it was successfully initialized - /// and the mount point is valid + /// \note nullptr until initialize_sdcard() succeeded. The card stays valid while + /// its volume is unmounted (sdcard_component()->unmount()), e.g. to hand it + /// to a USB host through espp::UsbDevice's MSC function. sdmmc_card_t *sdcard() const { return sdcard_ ? sdcard_->card() : nullptr; } /// Get the SD card component: mount() / unmount() (e.g. to hand the card to a diff --git a/components/usb_device/include/usb_device.hpp b/components/usb_device/include/usb_device.hpp index 7fbcb9ed70..ae5889481f 100644 --- a/components/usb_device/include/usb_device.hpp +++ b/components/usb_device/include/usb_device.hpp @@ -256,8 +256,9 @@ class UsbDevice : public BaseComponent { }; Type type{Type::FlashPartition}; /**< Which storage backs this LUN. */ /** For Type::SdCard: a caller-owned card initialized with sdmmc_card_init() on - * an SDMMC or SDSPI host. Must outlive the UsbDevice. Do not pass the card - * from esp_vfs_fat_sdmmc_mount() / esp_vfs_fat_sdspi_mount(): the matching + * an SDMMC or SDSPI host, e.g. espp::SdCard::card() with its volume unmounted. + * Must outlive the UsbDevice. Do not pass the card from + * esp_vfs_fat_sdmmc_mount() / esp_vfs_fat_sdspi_mount(): the matching * esp_vfs_fat_sdcard_unmount() frees it. Requires a target with an SDMMC host * peripheral (e.g. ESP32-S3, ESP32-P4), even when the card is on SPI. */ sdmmc_card_t *sd_card{nullptr}; diff --git a/components/ws-s3-geek/include/ws-s3-geek.hpp b/components/ws-s3-geek/include/ws-s3-geek.hpp index 70022fb6ee..3a0bb22423 100644 --- a/components/ws-s3-geek/include/ws-s3-geek.hpp +++ b/components/ws-s3-geek/include/ws-s3-geek.hpp @@ -184,8 +184,9 @@ class WsS3Geek : public BaseComponent { /// Get the uSD card /// \return A pointer to the uSD card - /// \note The uSD card is only available if it was successfully initialized - /// and the mount point is valid + /// \note nullptr until initialize_sdcard() succeeded. The card stays valid while + /// its volume is unmounted (sdcard_component()->unmount()), e.g. to hand it + /// to a USB host through espp::UsbDevice's MSC function. sdmmc_card_t *sdcard() const { return sdcard_ ? sdcard_->card() : nullptr; } /// Get the SD card component: mount() / unmount() (e.g. to hand the card to a diff --git a/components/ws-s3-lcd-1-47/include/ws-s3-lcd-1-47.hpp b/components/ws-s3-lcd-1-47/include/ws-s3-lcd-1-47.hpp index 123c5e3266..3c81d99894 100644 --- a/components/ws-s3-lcd-1-47/include/ws-s3-lcd-1-47.hpp +++ b/components/ws-s3-lcd-1-47/include/ws-s3-lcd-1-47.hpp @@ -200,8 +200,9 @@ class WsS3Lcd147 : public BaseComponent { /// Get the uSD card /// \return A pointer to the uSD card - /// \note The uSD card is only available if it was successfully initialized - /// and the mount point is valid + /// \note nullptr until initialize_sdcard() succeeded. The card stays valid while + /// its volume is unmounted (sdcard_component()->unmount()), e.g. to hand it + /// to a USB host through espp::UsbDevice's MSC function. sdmmc_card_t *sdcard() const { return sdcard_ ? sdcard_->card() : nullptr; } /// Get the SD card component: mount() / unmount() (e.g. to hand the card to a diff --git a/components/xiao-esp32s3-sense/include/xiao-esp32s3-sense.hpp b/components/xiao-esp32s3-sense/include/xiao-esp32s3-sense.hpp index 7a85330669..efa70ae909 100644 --- a/components/xiao-esp32s3-sense/include/xiao-esp32s3-sense.hpp +++ b/components/xiao-esp32s3-sense/include/xiao-esp32s3-sense.hpp @@ -118,9 +118,12 @@ class XiaoEsp32S3Sense : public BaseComponent { /// otherwise. bool initialize_sdcard(const SdCardConfig &config); - /// Get the mounted microSD card handle. - /// \return Pointer to the mounted card, or nullptr if the card has not been + /// Get the microSD card handle. + /// \return Pointer to the card, or nullptr if the card has not been /// initialized successfully. + /// \note nullptr until initialize_sdcard() succeeded. The card stays valid while + /// its volume is unmounted (sdcard_component()->unmount()), e.g. to hand it + /// to a USB host through espp::UsbDevice's MSC function. sdmmc_card_t *sdcard() const { return sdcard_ ? sdcard_->card() : nullptr; } /// Get the SD card component: mount() / unmount() (e.g. to hand the card to a