From 98fee5a68a68dd459850ecf62208540969be1721 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20M=C3=BAdry?= Date: Thu, 2 Jul 2026 14:13:22 +0200 Subject: [PATCH] feat(storage): Add generic partition BDL example Add an example under examples/storage/generic_partition_bdl that partitions a raw block device at run time using only the Block Device Layer (BDL) interface. The example obtains a whole-disk BDL (SPI flash data partition by default, or an SD/eMMC card via menuconfig), writes an MBR partition table onto it with the esp_ext_part_tables managed component (fetched via idf_component.yml), then creates a generic-partition BDL for each MBR entry with esp_blockdev_generic_partition_get() and mounts FATFS on the FAT slice and LittleFS on the LittleFS slice. --- docs/en/api-reference/storage/blockdev.rst | 5 + docs/zh_CN/api-reference/storage/blockdev.rst | 5 + examples/storage/.build-test-rules.yml | 15 + .../generic_partition_bdl/CMakeLists.txt | 9 + .../storage/generic_partition_bdl/README.md | 228 +++++++++++ .../generic_partition_bdl/main/CMakeLists.txt | 9 + .../main/Kconfig.projbuild | 188 +++++++++ .../main/generic_partition_bdl_main.c | 371 ++++++++++++++++++ .../main/idf_component.yml | 7 + .../generic_partition_bdl/main/sd_card_bdl.c | 211 ++++++++++ .../generic_partition_bdl/main/sd_card_bdl.h | 38 ++ .../partitions_example.csv | 8 + .../pytest_generic_partition_bdl_example.py | 51 +++ .../generic_partition_bdl/sdkconfig.ci.sdmmc | 9 + .../generic_partition_bdl/sdkconfig.ci.sdspi | 9 + .../sdkconfig.ci.spiflash | 11 + .../generic_partition_bdl/sdkconfig.defaults | 8 + 17 files changed, 1182 insertions(+) create mode 100644 examples/storage/generic_partition_bdl/CMakeLists.txt create mode 100644 examples/storage/generic_partition_bdl/README.md create mode 100644 examples/storage/generic_partition_bdl/main/CMakeLists.txt create mode 100644 examples/storage/generic_partition_bdl/main/Kconfig.projbuild create mode 100644 examples/storage/generic_partition_bdl/main/generic_partition_bdl_main.c create mode 100644 examples/storage/generic_partition_bdl/main/idf_component.yml create mode 100644 examples/storage/generic_partition_bdl/main/sd_card_bdl.c create mode 100644 examples/storage/generic_partition_bdl/main/sd_card_bdl.h create mode 100644 examples/storage/generic_partition_bdl/partitions_example.csv create mode 100644 examples/storage/generic_partition_bdl/pytest_generic_partition_bdl_example.py create mode 100644 examples/storage/generic_partition_bdl/sdkconfig.ci.sdmmc create mode 100644 examples/storage/generic_partition_bdl/sdkconfig.ci.sdspi create mode 100644 examples/storage/generic_partition_bdl/sdkconfig.ci.spiflash create mode 100644 examples/storage/generic_partition_bdl/sdkconfig.defaults diff --git a/docs/en/api-reference/storage/blockdev.rst b/docs/en/api-reference/storage/blockdev.rst index c34c2655a69..a4a32c063b9 100644 --- a/docs/en/api-reference/storage/blockdev.rst +++ b/docs/en/api-reference/storage/blockdev.rst @@ -227,6 +227,11 @@ When an overlap or invalid range is detected, the build fails with a diagnostic .. _blockdev-apis: +Application Examples +-------------------- + +- :example:`storage/generic_partition_bdl` demonstrates how to partition a raw block device at run time using only the Block Device Layer, by writing an MBR partition table onto a whole-disk BDL (a SPI flash data partition or an SD/eMMC card) and mounting FATFS and LittleFS on generic-partition BDL slices. + API Reference ------------- diff --git a/docs/zh_CN/api-reference/storage/blockdev.rst b/docs/zh_CN/api-reference/storage/blockdev.rst index e537ce770e6..ce438d11af9 100644 --- a/docs/zh_CN/api-reference/storage/blockdev.rst +++ b/docs/zh_CN/api-reference/storage/blockdev.rst @@ -157,6 +157,11 @@ .. _blockdev-apis: +应用示例 +-------- + +- :example:`storage/generic_partition_bdl` 演示了如何仅使用块设备层在运行时对裸块设备进行分区:将 MBR 分区表写入整盘 BDL(SPI flash 数据分区或 SD/eMMC 卡),并在通用分区 BDL 切片上挂载 FATFS 和 LittleFS。 + API 参考 -------- diff --git a/examples/storage/.build-test-rules.yml b/examples/storage/.build-test-rules.yml index 46d14b814dc..cd4cb79ad19 100644 --- a/examples/storage/.build-test-rules.yml +++ b/examples/storage/.build-test-rules.yml @@ -18,6 +18,21 @@ examples/storage/emmc: - if: IDF_TARGET in ["esp32s3", "esp32p4"] reason: only support on esp32s3 and esp32p4 +examples/storage/generic_partition_bdl: + depends_components: + - esp_blockdev + - esp_blockdev_util + - esp_partition + - fatfs + - vfs + - sdmmc + - esp_driver_sdmmc + - esp_driver_sdspi + - esp_driver_spi + disable_test: + - if: IDF_TARGET != "esp32" + reason: only one target needed + examples/storage/partition_api/partition_find: depends_components: - esp_partition diff --git a/examples/storage/generic_partition_bdl/CMakeLists.txt b/examples/storage/generic_partition_bdl/CMakeLists.txt new file mode 100644 index 00000000000..e0ba3091398 --- /dev/null +++ b/examples/storage/generic_partition_bdl/CMakeLists.txt @@ -0,0 +1,9 @@ +# 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.22) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) + +set(COMPONENTS main) + +project(generic_partition_bdl) diff --git a/examples/storage/generic_partition_bdl/README.md b/examples/storage/generic_partition_bdl/README.md new file mode 100644 index 00000000000..fa57a9198ba --- /dev/null +++ b/examples/storage/generic_partition_bdl/README.md @@ -0,0 +1,228 @@ +| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-C61 | ESP32-H2 | ESP32-H21 | ESP32-H4 | ESP32-P4 | ESP32-S2 | ESP32-S3 | ESP32-S31 | +| ----------------- | ----- | -------- | -------- | -------- | -------- | --------- | -------- | --------- | -------- | -------- | -------- | -------- | --------- | + +# Generic Partition over the Block Device Layer (BDL) + +This example shows how to partition a raw block device **at run time** and mount +two different filesystems on the resulting slices, using only the generic +Block Device Layer (BDL) interface (`esp_blockdev`). + +It combines three building blocks: + +* a **whole-disk BDL** — a flash data partition (`esp_partition_get_blockdev()`) + or an SD/eMMC card over SDMMC or SDSPI (`sdmmc_get_blockdev()`); +* the [`esp_ext_part_tables`](https://components.espressif.com/components/espressif/esp_ext_part_tables) + managed component — to generate/write and read/parse an [**MBR**](https://en.wikipedia.org/wiki/Master_boot_record) partition table; +* the **generic-partition BDL** (`esp_blockdev_generic_partition_get()`) — to expose + each MBR partition entry as its own BDL that maps a slice of the whole disk. + +> Note: Putting an MBR partition table on a **SPI flash** data partition (the +> default medium) is mainly for demonstration — it needs no extra hardware and +> runs in QEMU/CI. In real projects flash is normally partitioned with the ESP-IDF +> partition table instead. MBR partitioning at run time makes practical sense for +> **removable/large media such as SD cards and eMMC**, where a standard, host-PC +> readable partition table is expected. + +## When to use this example + +- You need to partition a raw block device **at run time**, with a standard MBR + table that a host PC can also read. +- You are working with removable or large media (SD card, eMMC) that is expected + to carry its own partition table. +- You want several filesystems (FATFS and LittleFS here) on slices of one device, + sharing the same `esp_blockdev` interface. + +Use a different example when you need: + +- FATFS over the Block Device Layer on a single wear-levelled flash partition → + [fatfs/bdl_wl](../fatfs/bdl_wl/) +- the classic wear levelling + FATFS flow on SPI flash → + [wear_levelling](../wear_levelling/) +- a plain FATFS SD card mount without run-time partitioning → + [sd_card/sdmmc](../sd_card/sdmmc/) or [sd_card/sdspi](../sd_card/sdspi/) +- LittleFS on a single ESP-IDF partition → [littlefs](../littlefs/) + +## BDL stack + +``` + +--------------+ +--------------+ + | LittleFS | | FATFS | file systems (VFS) + +--------------+ +--------------+ +-----------------------+ + | | | <-- | WL BDL (if SPI flash) | + | generic-part | +- - - - - - - + +-----------------------+ + | BDL | | generic-part | esp_blockdev_generic_partition_get() + | | | BDL | (one slice per MBR partition entry) + +------+--------------+--+--------------+ + | MBR | whole-disk BDL | esp_partition_get_blockdev() / + +------+ | sdmmc_get_blockdev() + +---------------------------------------+ + | SPI flash partition / SD card | physical storage + +---------------------------------------+ +``` + +The whole disk is laid out as an MBR followed by the two data slices. Partition +starts use 4 KiB alignment on SPI flash and 1 MiB alignment on SD/eMMC, matching +the usual alignment used by PC partitioning tools. + +For the 2 MiB SPI flash `storage` partition, the layout is: + +``` + 0x0 0x200 0x1000 0x101000 disk end + +-------+-----------------+-----------------------------+---------------------+-----+ + | MBR | pad (align gap) | LittleFS slice (1 MiB, | FAT slice |[tail] + | 512 B | ~3.5 KiB | MBR entry 0, type 0xC3) | (rest of disk, | gap + | | (unused) | | MBR entry 1, | + | | | | type 0x0C) | + +-------+-----------------+-----------------------------+---------------------+-----+ + ^-- reserved MBR block ---^ ^ + (align_up(512, 4 KiB)) align_down() may drop a few KiB here --+ +``` + +On SD/eMMC, FAT has a fixed size of 16 MiB to keep formatting time bounded. +The card must have at least 18 MiB for the alignment gap and both partitions: + +``` + 0x0 0x200 0x100000 0x200000 0x1200000 disk end + +-------+-----------------+----------------------------+---------------------+----------------+ + | MBR | pad (align gap) | LittleFS slice (1 MiB, | FAT slice | Unallocated | + | 512 B | ~1 MiB - 512 B | MBR entry 0, type 0xC3) | (16 MiB, | remainder | + | | (unused) | | MBR entry 1, | | + | | | | type 0x0C) | | + +-------+-----------------+----------------------------+---------------------+----------------+ + ^-- reserved MBR block ---^ + (align_up(512, 1 MiB)) +``` + +Notes on the gaps: + +* **MBR padding**: the MBR is only 512 B, but the first block is reserved up to + the selected alignment (`align_up(MBR_SIZE, align)`). This leaves ~3.5 KiB + unused on SPI flash (`0x200`..`0x1000`) or ~1 MiB - 512 B on SD/eMMC + (`0x200`..`0x100000`). +* **Between slices**: none — LittleFS is a whole number of aligned blocks, so the + FAT slice starts immediately after it (`fat_start = lfs_start + lfs_size`). +* **SPI flash tail** (`[tail] gap`): `fat_size` is rounded *down* to the alignment, so if the + disk size is not a multiple of the alignment, less than one alignment unit at + the very end stays unused. For the 2 MiB flash partition the numbers divide + evenly, so this gap is zero. +* **SD/eMMC remainder**: space after the fixed FAT slice (offset 18 MiB) is left + unallocated. The example still overwrites the card's partition table and data; + the smaller FAT slice does not make it safe to use a card containing valuable data. + +Because every layer speaks the same BDL interface, the same +`esp_blockdev_generic_partition_get()` slice works with any bottom device, and the +FATFS / LittleFS integration code does not depend on the storage driver. + +## What the example does + +1. Creates a whole-disk BDL from the selected storage medium. +2. Computes a layout that reserves space for the MBR up to the selected partition + alignment (4 KiB on SPI flash, 1 MiB on SD/eMMC), places a fixed 1 MiB LittleFS + slice next, then a FAT slice of 16 MiB on SD/eMMC or the remaining space on SPI flash. +3. Builds an in-memory partition list, generates an MBR, and writes it to the + whole disk with `esp_ext_part_list_bdl_write()`. +4. Reads the MBR back with `esp_ext_part_list_bdl_read()` and, for each entry, + creates a generic-partition BDL and mounts the matching filesystem + (formatting it on first run). +5. Writes and reads back a small file on each filesystem, then unmounts and + releases every BDL handle. + +## How to use example + +### Choose the storage medium + +Run `idf.py menuconfig` and open **Example Configuration → Whole-disk block device**: + +* **SPI flash 'storage' data partition** (default): no extra hardware needed, also + works in QEMU/CI. +* **SD/eMMC card**: uses a real card and overwrites its existing partition table and + data. Pick the peripheral under **SD card host peripheral**: + * **SDMMC host**: uses the default SDMMC slot. You can select the bus width (1 or + 4 lines) in the same menu. + * **SD SPI**: accesses the card over the generic SPI bus. Set the MOSI/MISO/CLK/CS + GPIOs in the same menu; the data bus is always 1-line. + + **SD power supply comes from internal LDO IO** defaults to enabled on supported + targets, with LDO ID 4 on ESP32-P4 and 1 on ESP32-S31. Check your board schematic + and adjust the ID, or disable this option if the card uses an external supply. + > Warning: this overwrites the card's existing partition table and data. + +### Build and flash + +``` +idf.py -p PORT flash monitor +``` + +(To exit the serial monitor, type `Ctrl-]`.) + +The managed components (`esp_ext_part_tables`, `joltwallet/littlefs`) are fetched +automatically by the IDF Component Manager from +[`main/idf_component.yml`](main/idf_component.yml). + +## Example output + +``` +I (321) example: Whole disk: SPI flash data partition 'storage' +I (331) example: Whole disk BDL: disk_size=2097152, read_size=1, write_size=1, erase_size=4096 +I (341) example: Layout: LittleFS = 1024 KiB (fixed), FAT = remainder of the disk +I (361) example: Writing MBR partition table to the whole disk +I (401) example: Reading MBR partition table back +I (411) example: Mountable partition 0: type=4, address=0x00001000, size=0x00100000 +I (421) example: Mounting LittleFS on the LittleFS partition BDL +I (521) example: Writing '/littlefs/hello.txt' +I (611) example: Read back from /littlefs/hello.txt: 'Hello from LittleFS over a generic-partition BDL!' +I (621) example: Mountable partition 1: type=3, address=0x00101000, size=0x000ff000 +I (631) example: Mounting FATFS on the FAT partition BDL +I (811) example: Writing '/fat/hello.txt' +I (951) example: Read back from /fat/hello.txt: 'Hello from FATFS over a generic-partition BDL!' +I (961) example: Unmounting FATFS +I (971) example: Unmounting LittleFS +I (981) example: Releasing whole-disk BDL +I (991) example: Done +``` + +## Notes + +* An MBR holds at most **4 primary partition entries**, so this scheme supports up + to 4 slices; this example uses 2 (FAT + LittleFS). `esp_mbr_generate()` keeps only + the first 4 entries and logs a warning if the partition list is longer. +* When SPI flash is selected, the FAT partition BDL is wrapped in a wear-levelling + BDL before FATFS is mounted. This layer handles flash erases and distributes + writes across the partition. LittleFS is mounted directly on its partition BDL + because it handles erase-before-write and wear levelling internally. +* `esp_ext_part_list_bdl_write()` writes the raw MBR sector without erasing first, + so on flash-like devices (`erase_before_write` flag set) the example erases the + MBR block beforehand. +* The MBR partition **type** byte drives the filesystem choice: FAT entries are + mounted with FATFS and the LittleFS entry with LittleFS. The generator writes + the raw MBR type bytes `0x0C` (FAT32 with LBA) and `0xC3` (LittleFS); the + `type=` values printed on read-back (`3` for FAT32, `4` for LittleFS) are the + `esp_ext_part_tables` enum (`esp_ext_part_type_known_t`), not the raw bytes. +* LittleFS has no standard MBR type, so `esp_ext_part_tables` uses a **custom + `0xC3` "hack"**: `0xC3` = `0x83` (Linux-style filesystem) `| 0x40` (a flag meaning + "the CHS field carries the LittleFS block size") `| 0x10` (hidden). Because the + block size is smuggled into the entry's otherwise-unused CHS field, the example + must supply it via the `extra` field together with the `ESP_EXT_PART_FLAG_EXTRA` + flag (it passes the BDL `erase_size`, which LittleFS uses as its block size in + classic mode). This is a non-standard convention, not something a PC OS will + interpret as LittleFS. +* The example lets `esp_ext_part_tables` place the partitions automatically: + each entry sets `ESP_EXT_PART_FLAG_AUTO_ADDRESS` (so the generator assigns an + aligned start address after the previous entry and past the MBR sector). + On SPI flash, the FAT entry additionally sets `ESP_EXT_PART_FLAG_FILL` with + `size == 0` to fill the rest of the disk. On SD/eMMC, it sets a fixed 16 MiB size + without `ESP_EXT_PART_FLAG_FILL`. The example explicitly sets `total_size` + from the device geometry, enabling the generator's built-in overlap and + "fits within the disk" checks. If `total_size` is left at zero, + `esp_ext_part_list_bdl_write()` supplies it from the device geometry instead. +* The example requests `ESP_EXT_PART_ALIGN_4KiB` for SPI flash and + `ESP_EXT_PART_ALIGN_1MiB` for SD/eMMC when generating the MBR. Both alignments + satisfy the underlying media's BDL erase-alignment requirement. +* On the **first** run the data area is empty, so both filesystems fail to mount + and are formatted automatically (`format_if_mount_failed`). You will therefore + see a few `W`/`E` log lines the first time, for example + `esp_littlefs: ... Corrupted dir pair` followed by `mount failed ... formatting`. + Subsequent runs mount the existing filesystems directly. +* FATFS probes the block device with an `ioctl` that the flash whole-disk BDL does + not implement, so a harmless `esp_blockdev/generic_partition: ... Parent device + does not implement ioctl` error may be logged; the mount still succeeds. diff --git a/examples/storage/generic_partition_bdl/main/CMakeLists.txt b/examples/storage/generic_partition_bdl/main/CMakeLists.txt new file mode 100644 index 00000000000..93ac19a683a --- /dev/null +++ b/examples/storage/generic_partition_bdl/main/CMakeLists.txt @@ -0,0 +1,9 @@ +set(srcs "generic_partition_bdl_main.c") +if(CONFIG_EXAMPLE_STORAGE_MEDIA_SDCARD) + list(APPEND srcs "sd_card_bdl.c") +endif() + +# fatfs already provides the SDMMC/SDSPI driver dependencies on hardware targets. +idf_component_register(SRCS ${srcs} + PRIV_REQUIRES vfs fatfs esp_partition esp_blockdev esp_blockdev_util + INCLUDE_DIRS ".") diff --git a/examples/storage/generic_partition_bdl/main/Kconfig.projbuild b/examples/storage/generic_partition_bdl/main/Kconfig.projbuild new file mode 100644 index 00000000000..264a277d764 --- /dev/null +++ b/examples/storage/generic_partition_bdl/main/Kconfig.projbuild @@ -0,0 +1,188 @@ +menu "Example Configuration" + + choice EXAMPLE_STORAGE_MEDIA + prompt "Whole-disk block device" + default EXAMPLE_STORAGE_MEDIA_SPIFLASH + help + Select the underlying "whole-disk" block device that gets partitioned + with an MBR and split into a FAT and a LittleFS partition using the + generic-partition Block Device Layer (BDL). + + config EXAMPLE_STORAGE_MEDIA_SPIFLASH + bool "SPI flash 'storage' data partition" + help + Use a SPI flash data partition (labelled "storage") as the whole disk. + No extra hardware is required, so this option also works in QEMU/CI. + + config EXAMPLE_STORAGE_MEDIA_SDCARD + bool "SD/eMMC card" + help + Use an SD/eMMC card as the whole disk. The card can be accessed + through the SDMMC host or over the generic SPI bus (SDSPI); pick + the peripheral below. + WARNING: running the example overwrites the card's partition table. + endchoice + + choice EXAMPLE_SD_HOST + prompt "SD card host peripheral" + depends on EXAMPLE_STORAGE_MEDIA_SDCARD + default EXAMPLE_SD_HOST_SDMMC if SOC_SDMMC_HOST_SUPPORTED + default EXAMPLE_SD_HOST_SDSPI + help + Select the peripheral used to talk to the SD/eMMC card. + + config EXAMPLE_SD_HOST_SDMMC + bool "SDMMC host" + depends on SOC_SDMMC_HOST_SUPPORTED + help + Access the card through the dedicated SDMMC host controller. + + config EXAMPLE_SD_HOST_SDSPI + bool "SD SPI (SD card over the SPI bus)" + help + Access the card over the generic SPI bus using the SDSPI protocol. + Works on any target with a free SPI host, but is slower and always + uses a 1-line data bus. + endchoice + + choice EXAMPLE_SDMMC_BUS_WIDTH + prompt "SDMMC bus width" + depends on EXAMPLE_SD_HOST_SDMMC + default EXAMPLE_SDMMC_BUS_WIDTH_4 + + config EXAMPLE_SDMMC_BUS_WIDTH_4 + bool "4 lines (D0 - D3)" + + config EXAMPLE_SDMMC_BUS_WIDTH_1 + bool "1 line (D0)" + endchoice + + config EXAMPLE_SDMMC_INTERNAL_PULLUP + bool "Enable internal pull-ups on the SDMMC lines" + depends on EXAMPLE_SD_HOST_SDMMC + default n + help + Enable the internal pull-ups on the SDMMC CMD/DATA lines. The SD + specification requires external 10 kOhm pull-ups; the internal ones are + weak and only adequate for boards that do not provide external pull-ups + (for example the ESP32-S3-USB-OTG devkit). Prefer external pull-ups when + the board has them. + + # On SoCs whose SDMMC controller routes through the GPIO matrix (e.g. ESP32-S3, + # ESP32-P4) the slot pins are freely assignable, so expose them here. On SoCs + # with a fixed SDMMC IO_MUX (e.g. ESP32) the slot default pins are used instead. + config EXAMPLE_SDMMC_PIN_CLK + int "SDMMC CLK GPIO number" + depends on EXAMPLE_SD_HOST_SDMMC && SOC_SDMMC_USE_GPIO_MATRIX + default 36 if IDF_TARGET_ESP32S3 + default 43 if IDF_TARGET_ESP32P4 + default -1 + help + GPIO for the SDMMC CLK line. The ESP32-S3-USB-OTG devkit uses GPIO 36. + + config EXAMPLE_SDMMC_PIN_CMD + int "SDMMC CMD GPIO number" + depends on EXAMPLE_SD_HOST_SDMMC && SOC_SDMMC_USE_GPIO_MATRIX + default 35 if IDF_TARGET_ESP32S3 + default 44 if IDF_TARGET_ESP32P4 + default -1 + help + GPIO for the SDMMC CMD line. The ESP32-S3-USB-OTG devkit uses GPIO 35. + + config EXAMPLE_SDMMC_PIN_D0 + int "SDMMC D0 GPIO number" + depends on EXAMPLE_SD_HOST_SDMMC && SOC_SDMMC_USE_GPIO_MATRIX + default 37 if IDF_TARGET_ESP32S3 + default 39 if IDF_TARGET_ESP32P4 + default -1 + help + GPIO for the SDMMC D0 line. The ESP32-S3-USB-OTG devkit uses GPIO 37. + + config EXAMPLE_SDMMC_PIN_D1 + int "SDMMC D1 GPIO number" + depends on EXAMPLE_SD_HOST_SDMMC && SOC_SDMMC_USE_GPIO_MATRIX && EXAMPLE_SDMMC_BUS_WIDTH_4 + default 38 if IDF_TARGET_ESP32S3 + default 40 if IDF_TARGET_ESP32P4 + default -1 + help + GPIO for the SDMMC D1 line (4-line mode only). The ESP32-S3-USB-OTG + devkit uses GPIO 38. + + config EXAMPLE_SDMMC_PIN_D2 + int "SDMMC D2 GPIO number" + depends on EXAMPLE_SD_HOST_SDMMC && SOC_SDMMC_USE_GPIO_MATRIX && EXAMPLE_SDMMC_BUS_WIDTH_4 + default 33 if IDF_TARGET_ESP32S3 + default 41 if IDF_TARGET_ESP32P4 + default -1 + help + GPIO for the SDMMC D2 line (4-line mode only). The ESP32-S3-USB-OTG + devkit uses GPIO 33. + + config EXAMPLE_SDMMC_PIN_D3 + int "SDMMC D3 GPIO number" + depends on EXAMPLE_SD_HOST_SDMMC && SOC_SDMMC_USE_GPIO_MATRIX && EXAMPLE_SDMMC_BUS_WIDTH_4 + default 34 if IDF_TARGET_ESP32S3 + default 42 if IDF_TARGET_ESP32P4 + default -1 + help + GPIO for the SDMMC D3 line (4-line mode only). The ESP32-S3-USB-OTG + devkit uses GPIO 34. + + config EXAMPLE_SDSPI_PIN_MOSI + int "SDSPI MOSI GPIO number" + depends on EXAMPLE_SD_HOST_SDSPI + default 15 if IDF_TARGET_ESP32 + default 35 if IDF_TARGET_ESP32S2 + default 4 if IDF_TARGET_ESP32S3 + default 5 if IDF_TARGET_ESP32H2 + default 36 if IDF_TARGET_ESP32P4 + default 4 + + config EXAMPLE_SDSPI_PIN_MISO + int "SDSPI MISO GPIO number" + depends on EXAMPLE_SD_HOST_SDSPI + default 2 if IDF_TARGET_ESP32 + default 37 if IDF_TARGET_ESP32S2 + default 5 if IDF_TARGET_ESP32S3 + default 0 if IDF_TARGET_ESP32H2 + default 47 if IDF_TARGET_ESP32P4 + default 6 + + config EXAMPLE_SDSPI_PIN_CLK + int "SDSPI CLK GPIO number" + depends on EXAMPLE_SD_HOST_SDSPI + default 14 if IDF_TARGET_ESP32 + default 36 if IDF_TARGET_ESP32S2 + default 2 if IDF_TARGET_ESP32S3 + default 4 if IDF_TARGET_ESP32H2 + default 53 if IDF_TARGET_ESP32P4 + default 5 + + config EXAMPLE_SDSPI_PIN_CS + int "SDSPI CS GPIO number" + depends on EXAMPLE_SD_HOST_SDSPI + default 13 if IDF_TARGET_ESP32 + default 34 if IDF_TARGET_ESP32S2 + default 8 if IDF_TARGET_ESP32S3 + default 33 if IDF_TARGET_ESP32P4 + default 1 + + config EXAMPLE_SD_PWR_CTRL_LDO_INTERNAL_IO + depends on EXAMPLE_STORAGE_MEDIA_SDCARD + depends on SOC_SDMMC_IO_POWER_EXTERNAL || SOC_SDMMC_IO_UHS_POWER_EXTERNAL + bool "SD power supply comes from internal LDO IO (READ HELP!)" + default y + help + Only needed when the SD card is connected to specific IO pins which can be used for high-speed SDMMC. + Please read the schematic first and check if the SD VDD is connected to any internal LDO output. + Unselect this option if the SD card is powered by an external power supply. + + config EXAMPLE_SD_PWR_CTRL_LDO_IO_ID + depends on EXAMPLE_SD_PWR_CTRL_LDO_INTERNAL_IO + int "LDO ID" + default 4 if IDF_TARGET_ESP32P4 + default 1 if IDF_TARGET_ESP32S31 + help + Please read the schematic first and input your LDO ID. + +endmenu diff --git a/examples/storage/generic_partition_bdl/main/generic_partition_bdl_main.c b/examples/storage/generic_partition_bdl/main/generic_partition_bdl_main.c new file mode 100644 index 00000000000..d7b36e96f61 --- /dev/null +++ b/examples/storage/generic_partition_bdl/main/generic_partition_bdl_main.c @@ -0,0 +1,371 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ + +/* + * Generic-partition Block Device Layer (BDL) example + * + * Demonstrates partitioning a raw block device at run time and mounting two + * different filesystems on the resulting sub-partitions: + * + * +--------------+ +--------------+ + * | LittleFS | | FATFS | file systems (VFS) + * +--------------+ +--------------+ +-----------------------+ + * | | | <-- | WL BDL (if SPI flash) | + * | generic-part | +- - - - - - - + +-----------------------+ + * | BDL | | generic-part | esp_blockdev_generic_partition_get() + * | | | BDL | (one slice per MBR partition entry) + * +------+--------------+--+--------------+ + * | MBR | whole-disk BDL | esp_partition_get_blockdev() / + * +------+ | sdmmc_get_blockdev() + * +---------------------------------------+ + * | SPI flash partition / SD card | physical storage + * +---------------------------------------+ + * + * Steps performed: + * 1. Obtain a "whole-disk" BDL from either a SPI flash data partition or an + * SD/eMMC card accessed over SDMMC or SDSPI (selectable in menuconfig). + * 2. Lay out and write an MBR partition table onto the whole disk using the + * `esp_ext_part_tables` managed component (fetched via idf_component.yml). + * 3. Read the partition table back and, for each entry, create a + * generic-partition BDL that maps only that slice of the whole disk. + * 4. Format (if needed) and mount FATFS on the FAT entry and LittleFS on the + * LittleFS entry, then perform a simple write/read on each. + */ + +#include +#include +#include +#include + +#include "esp_err.h" +#include "esp_check.h" +#include "esp_log.h" +#include "sdkconfig.h" + +#include "esp_blockdev.h" +#include "esp_blockdev/generic_partition.h" + +#include "esp_ext_part_tables.h" +#include "esp_mbr.h" + +#include "esp_vfs_fat.h" +#include "esp_littlefs.h" + +#if CONFIG_EXAMPLE_STORAGE_MEDIA_SPIFLASH +#include "esp_partition.h" +#include "wear_levelling.h" +#else +#include "sd_card_bdl.h" +#endif + +static const char *TAG = "example"; + +#define WHOLE_DISK_PARTITION_LABEL "storage" +#define FAT_MOUNT_POINT "/fat" +#define LITTLEFS_MOUNT_POINT "/littlefs" + +/* Keep SD/eMMC formatting bounded; SPI flash FAT uses the remaining space. */ +#define LITTLEFS_PARTITION_SIZE (1 * 1024 * 1024) +#define FAT_PARTITION_SIZE (16 * 1024 * 1024) + +/* The example lays out two data partitions on the whole disk. Their start + * addresses (and the SPI flash FAT size) are computed by esp_ext_part_tables + * during MBR generation - see build_and_write_partition_table(). */ +enum { + EXAMPLE_PART_LITTLEFS, /* first slice, right after the MBR block */ + EXAMPLE_PART_FAT, + EXAMPLE_PART_COUNT, +}; + +/* ---------------------------------------------------------------------- */ +/* Whole-disk BDL creation / release (media specific) */ +/* ---------------------------------------------------------------------- */ + +#if CONFIG_EXAMPLE_STORAGE_MEDIA_SPIFLASH + +static esp_err_t obtain_whole_disk_bdl(esp_blockdev_handle_t *out) +{ + ESP_LOGI(TAG, "Whole disk: SPI flash data partition '%s'", WHOLE_DISK_PARTITION_LABEL); + return esp_partition_get_blockdev(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_ANY, + WHOLE_DISK_PARTITION_LABEL, out); +} + +static esp_err_t release_whole_disk_bdl(esp_blockdev_handle_t disk) +{ + return disk->ops->release(disk); +} + +#else /* CONFIG_EXAMPLE_STORAGE_MEDIA_SDCARD */ + +static esp_err_t obtain_whole_disk_bdl(esp_blockdev_handle_t *out) +{ + /* Card setup (SDMMC or SDSPI, selectable in menuconfig) lives in sd_card_bdl.c. */ + return example_sd_card_bdl_create(out); +} + +static esp_err_t release_whole_disk_bdl(esp_blockdev_handle_t disk) +{ + return example_sd_card_bdl_release(disk); +} + +#endif + +/* ---------------------------------------------------------------------- */ +/* Partitioning */ +/* ---------------------------------------------------------------------- */ + +static esp_err_t build_and_write_partition_table(esp_blockdev_handle_t disk) +{ + esp_ext_part_list_t part_list = {0}; + esp_err_t ret = ESP_OK; + + /* Describe the two partitions and let esp_ext_part_tables place them: + * ESP_EXT_PART_FLAG_AUTO_ADDRESS makes the generator compute each start + * address (after the previous entry, aligned to the requested alignment and + * past the MBR sector), so `.address` is left unset. LittleFS gets a fixed + * size; FATFS gets 16 MiB on SD/eMMC. On SPI flash, FATFS instead uses + * ESP_EXT_PART_FLAG_FILL with `.size == 0` to take the remaining space. */ + esp_ext_part_list_item_t partitions[EXAMPLE_PART_COUNT] = { + /* The LittleFS MBR entry (type 0xC3) stores the filesystem block size in + * the CHS "hack" field, passed via `.extra` + ESP_EXT_PART_FLAG_EXTRA. + * LittleFS in classic mode uses the BDL erase size as its block size. */ + [EXAMPLE_PART_LITTLEFS] = { + .info = { + .size = LITTLEFS_PARTITION_SIZE, + .type = ESP_EXT_PART_TYPE_LITTLEFS, + .extra = disk->geometry.erase_size, + .flags = ESP_EXT_PART_FLAG_EXTRA | ESP_EXT_PART_FLAG_AUTO_ADDRESS, + .label = NULL, + }, + }, + [EXAMPLE_PART_FAT] = { + .info = { + .type = ESP_EXT_PART_TYPE_FAT32, +#if CONFIG_EXAMPLE_STORAGE_MEDIA_SPIFLASH + .size = 0, /* FILL: sized to the rest of the disk by the generator */ + .flags = ESP_EXT_PART_FLAG_AUTO_ADDRESS | ESP_EXT_PART_FLAG_FILL, +#else + .size = FAT_PARTITION_SIZE, + .flags = ESP_EXT_PART_FLAG_AUTO_ADDRESS, +#endif + .label = NULL, + }, + }, + }; + + /* Choose the partition-start alignment based on the storage medium: + * - SPI flash: 4 KiB, matching the flash erase (sector) size. A larger + * alignment would waste a big chunk of the small (few-MiB) flash disk. + * - SD/eMMC card: 1 MiB, matching how PC tools (fdisk/parted) align + * partitions to the card's erase/allocation unit for best performance. + * Either value is >= the BDL erase size, so it also satisfies the + * generic-partition BDL erase-alignment requirement. + * + * `total_size` is the whole-disk size: the generator needs it to size the + * SPI flash FILL (FAT) partition and to run its overlap / off-disk checks. + * (esp_ext_part_list_bdl_write() would auto-fill it from the device geometry + * when left 0, but the example sets it explicitly for clarity.) */ + esp_mbr_generate_extra_args_t gen_args = { + .total_size = disk->geometry.disk_size, + .sector_size = ESP_EXT_PART_SECTOR_SIZE_512B, +#if CONFIG_EXAMPLE_STORAGE_MEDIA_SPIFLASH + .alignment = ESP_EXT_PART_ALIGN_4KiB, +#else + .alignment = ESP_EXT_PART_ALIGN_1MiB, +#endif + }; + + /* Insert in on-disk order so the MBR entries match the physical layout. */ + for (int i = 0; i < EXAMPLE_PART_COUNT; i++) { + ESP_GOTO_ON_ERROR(esp_ext_part_list_insert(&part_list, &partitions[i]), cleanup, TAG, "insert partition entry %d", i); + } + + /* esp_ext_part_list_bdl_write() writes the raw 512-byte MBR without erasing + * first. On flash-like devices the target block must be erased beforehand. */ + if (disk->device_flags.erase_before_write) { + ESP_GOTO_ON_ERROR(disk->ops->erase(disk, 0, disk->geometry.erase_size), cleanup, TAG, "erase MBR area"); + } + + ESP_GOTO_ON_ERROR(esp_ext_part_list_bdl_write(disk, &part_list, ESP_EXT_PART_LIST_SIGNATURE_MBR, &gen_args), + cleanup, TAG, "write MBR"); + +cleanup: + esp_ext_part_list_deinit(&part_list); + return ret; +} + +/* ---------------------------------------------------------------------- */ +/* Filesystem usage */ +/* ---------------------------------------------------------------------- */ + +static void write_and_read_back(const char *path, const char *content); + +static esp_err_t mount_fat(esp_blockdev_handle_t part) +{ + ESP_LOGI(TAG, "Mounting FATFS on the FAT partition BDL"); + const esp_vfs_fat_mount_config_t mount_config = { + .max_files = 4, + .format_if_mount_failed = true, + /* Use larger clusters to keep the FAT tables small. */ + .allocation_unit_size = 16 * 1024, + .use_one_fat = false, + }; + ESP_RETURN_ON_ERROR(esp_vfs_fat_bdl_mount(FAT_MOUNT_POINT, part, &mount_config), TAG, "FAT mount failed"); + return ESP_OK; +} + +static esp_err_t mount_littlefs(esp_blockdev_handle_t part) +{ + ESP_LOGI(TAG, "Mounting LittleFS on the LittleFS partition BDL"); + const esp_vfs_littlefs_conf_t conf = { + .base_path = LITTLEFS_MOUNT_POINT, + .blockdev = part, + .format_if_mount_failed = true, + .dont_mount = false, + }; + ESP_RETURN_ON_ERROR(esp_vfs_littlefs_register(&conf), TAG, "LittleFS mount failed"); + return ESP_OK; +} + +/* ---------------------------------------------------------------------- */ + +void app_main(void) +{ + /* Step 1: obtain the whole-disk block device. */ + esp_blockdev_handle_t disk = NULL; + ESP_ERROR_CHECK(obtain_whole_disk_bdl(&disk)); + ESP_LOGI(TAG, "Whole disk BDL: disk_size=%" PRIu64 ", read_size=%zu, write_size=%zu, erase_size=%zu", + disk->geometry.disk_size, disk->geometry.read_size, disk->geometry.write_size, disk->geometry.erase_size); + + /* Step 2: lay out and write the MBR partition table. The partition start + * addresses (and the SPI flash FAT size) are assigned by esp_ext_part_tables during + * generation; the actual offsets are logged from the read-back below. */ +#if CONFIG_EXAMPLE_STORAGE_MEDIA_SPIFLASH + ESP_LOGI(TAG, "Layout: LittleFS = %u KiB (fixed), FAT = remainder of the disk", + (unsigned)(LITTLEFS_PARTITION_SIZE / 1024)); +#else + ESP_LOGI(TAG, "Layout: LittleFS = %u KiB (fixed), FAT = %u KiB (fixed)", + (unsigned)(LITTLEFS_PARTITION_SIZE / 1024), (unsigned)(FAT_PARTITION_SIZE / 1024)); +#endif + ESP_LOGI(TAG, "Writing MBR partition table to the whole disk"); + ESP_ERROR_CHECK(build_and_write_partition_table(disk)); + + /* Step 3: read the partition table back and create a generic-partition BDL + * for each entry, mounting the matching filesystem. */ + ESP_LOGI(TAG, "Reading MBR partition table back"); + esp_ext_part_list_t part_list = {0}; + esp_mbr_parse_extra_args_t parse_args = { + .sector_size = ESP_EXT_PART_SECTOR_SIZE_512B, + /* .match left zero (fn == NULL): keep every recognized partition (the two + * we wrote). To have the parser drop non-mountable entries up front, set + * .match = esp_ext_part_match_mountable() instead. */ + }; + ESP_ERROR_CHECK(esp_ext_part_list_bdl_read(disk, &part_list, ESP_EXT_PART_LIST_SIGNATURE_MBR, &parse_args)); + + /* A LOSSY list means the parser could not represent every on-disk partition + * (e.g. it was filtered, or the table held more than the list can hold). */ + if (part_list.flags & ESP_EXT_PART_LIST_FLAG_LOSSY) { + ESP_LOGW(TAG, "Parsed partition list is LOSSY (some entries were dropped)"); + } + +#if CONFIG_EXAMPLE_STORAGE_MEDIA_SPIFLASH + esp_blockdev_handle_t wl_part = NULL; +#endif + esp_blockdev_handle_t fat_part = NULL; + esp_blockdev_handle_t lfs_part = NULL; + + /* esp_ext_part_match_mountable() reports whether a partition holds a + * filesystem this build can mount: FAT (always part of ESP-IDF) and LittleFS + * when the LittleFS component is linked. esp_ext_part_tables detects LittleFS + * via its build system (it defines ESP_EXT_PART_HAS_LITTLEFS when the + * component is present), so the stock predicate works here without a custom + * one. esp_ext_part_list_next_matching() then walks only the matching + * (mountable) entries; any other partition types are skipped automatically. */ + esp_ext_part_match_t mountable = esp_ext_part_match_mountable(); + + int index = 0; + for (esp_ext_part_list_item_t *it = esp_ext_part_list_next_matching(NULL, &part_list, &mountable); + it != NULL; + it = esp_ext_part_list_next_matching(it, &part_list, &mountable), index++) { + ESP_LOGI(TAG, "Mountable partition %d: type=%u, address=0x%08llx, size=0x%08llx", + index, (unsigned)it->info.type, + (unsigned long long)it->info.address, (unsigned long long)it->info.size); + + /* FAT and LittleFS need different mount calls. */ + switch (it->info.type) { + case ESP_EXT_PART_TYPE_FAT12: + case ESP_EXT_PART_TYPE_FAT16: + case ESP_EXT_PART_TYPE_FAT32: +#if CONFIG_EXAMPLE_STORAGE_MEDIA_SPIFLASH + ESP_ERROR_CHECK(esp_blockdev_generic_partition_get(disk, it->info.address, it->info.size, &wl_part)); + ESP_ERROR_CHECK(wl_get_blockdev(wl_part, &fat_part)); +#else + ESP_ERROR_CHECK(esp_blockdev_generic_partition_get(disk, it->info.address, it->info.size, &fat_part)); +#endif + ESP_ERROR_CHECK(mount_fat(fat_part)); + write_and_read_back(FAT_MOUNT_POINT "/hello.txt", "Hello from FATFS over a generic-partition BDL!"); + break; + case ESP_EXT_PART_TYPE_LITTLEFS: + ESP_ERROR_CHECK(esp_blockdev_generic_partition_get(disk, it->info.address, it->info.size, &lfs_part)); + ESP_ERROR_CHECK(mount_littlefs(lfs_part)); + write_and_read_back(LITTLEFS_MOUNT_POINT "/hello.txt", "Hello from LittleFS over a generic-partition BDL!"); + break; + default: + ESP_LOGW(TAG, "No mount handler for mountable type %u", (unsigned)it->info.type); + break; + } + } + /* Step 4: tear everything down. */ + + esp_ext_part_list_deinit(&part_list); + if (fat_part != NULL) { + ESP_LOGI(TAG, "Unmounting FATFS"); + ESP_ERROR_CHECK(esp_vfs_fat_bdl_unmount(FAT_MOUNT_POINT, fat_part)); + /* FAT unmount does NOT release the BDL handle - the caller owns it. */ + ESP_ERROR_CHECK(fat_part->ops->release(fat_part)); +#if CONFIG_EXAMPLE_STORAGE_MEDIA_SPIFLASH + ESP_LOGI(TAG, "Releasing WL BDL"); + ESP_ERROR_CHECK(wl_part->ops->release(wl_part)); +#endif + } + if (lfs_part != NULL) { + ESP_LOGI(TAG, "Unmounting LittleFS"); + /* LittleFS unregister releases the BDL handle for us. */ + ESP_ERROR_CHECK(esp_vfs_littlefs_unregister_blockdev(lfs_part)); + } + + ESP_LOGI(TAG, "Releasing whole-disk BDL"); + ESP_ERROR_CHECK(release_whole_disk_bdl(disk)); + + ESP_LOGI(TAG, "Done"); +} + +static void write_and_read_back(const char *path, const char *content) +{ + ESP_LOGI(TAG, "Writing '%s'", path); + FILE *f = fopen(path, "wb"); + if (f == NULL) { + ESP_LOGE(TAG, "Failed to open %s for writing", path); + return; + } + fprintf(f, "%s\n", content); + fclose(f); + + FILE *fr = fopen(path, "r"); + if (fr == NULL) { + ESP_LOGE(TAG, "Failed to open %s for reading", path); + return; + } + char line[128] = {0}; + if (fgets(line, sizeof(line), fr) != NULL) { + char *nl = strchr(line, '\n'); + if (nl) { + *nl = '\0'; + } + ESP_LOGI(TAG, "Read back from %s: '%s'", path, line); + } + fclose(fr); +} diff --git a/examples/storage/generic_partition_bdl/main/idf_component.yml b/examples/storage/generic_partition_bdl/main/idf_component.yml new file mode 100644 index 00000000000..9077ab2c092 --- /dev/null +++ b/examples/storage/generic_partition_bdl/main/idf_component.yml @@ -0,0 +1,7 @@ +## IDF Component Manager Manifest File +dependencies: + idf: ">=6.0" + # Provides MBR parsing/generation and the BDL read/write helpers. + espressif/esp_ext_part_tables: "^0.4.0" + # LittleFS with Block Device Layer (BDL) support (esp_vfs_littlefs_conf_t.blockdev). + joltwallet/littlefs: "^1.22.2" diff --git a/examples/storage/generic_partition_bdl/main/sd_card_bdl.c b/examples/storage/generic_partition_bdl/main/sd_card_bdl.c new file mode 100644 index 00000000000..92825a8bdab --- /dev/null +++ b/examples/storage/generic_partition_bdl/main/sd_card_bdl.c @@ -0,0 +1,211 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ + +/* + * SD/eMMC whole-disk block device helpers for the generic-partition BDL example. + * + * Both the SDMMC and the SDSPI host paths initialize an `sdmmc_card_t` and then + * hand it to `sdmmc_get_blockdev()`, so the rest of the example works with a + * plain BDL handle and does not care which peripheral is used underneath. + */ + +#include + +#include "esp_check.h" +#include "esp_log.h" +#include "sdkconfig.h" + +#include "sdmmc_cmd.h" + +#if CONFIG_EXAMPLE_SD_HOST_SDMMC +#include "driver/sdmmc_host.h" +#elif CONFIG_EXAMPLE_SD_HOST_SDSPI +#include "driver/sdspi_host.h" +#include "driver/spi_common.h" +#endif + +#if SOC_SDMMC_IO_POWER_EXTERNAL || SOC_SDMMC_IO_UHS_POWER_EXTERNAL +#include "sd_pwr_ctrl_by_on_chip_ldo.h" +#endif + +#include "sd_card_bdl.h" + +#if CONFIG_EXAMPLE_STORAGE_MEDIA_SDCARD + +static const char *TAG = "example_sd"; + +#if CONFIG_EXAMPLE_SD_HOST_SDMMC +static sdmmc_host_t s_host = SDMMC_HOST_DEFAULT(); +#elif CONFIG_EXAMPLE_SD_HOST_SDSPI +static sdmmc_host_t s_host = SDSPI_HOST_DEFAULT(); +static spi_host_device_t s_spi_host_id; /* SPI bus id, saved before host.slot is reused for the device handle */ +#endif + +static sdmmc_card_t *s_card; + +#if SOC_SDMMC_IO_POWER_EXTERNAL || SOC_SDMMC_IO_UHS_POWER_EXTERNAL +static sd_pwr_ctrl_handle_t s_pwr_ctrl_handle; + +static esp_err_t init_power_control(sdmmc_host_t *host) +{ +#if CONFIG_EXAMPLE_SD_PWR_CTRL_LDO_INTERNAL_IO + sd_pwr_ctrl_ldo_config_t ldo_config = { + .ldo_chan_id = CONFIG_EXAMPLE_SD_PWR_CTRL_LDO_IO_ID, + }; + ESP_RETURN_ON_ERROR(sd_pwr_ctrl_new_on_chip_ldo(&ldo_config, &s_pwr_ctrl_handle), TAG, + "failed to create on-chip LDO power control driver"); + host->pwr_ctrl_handle = s_pwr_ctrl_handle; +#else + (void)host; +#endif + return ESP_OK; +} + +static void deinit_power_control(void) +{ + if (s_pwr_ctrl_handle != NULL) { + esp_err_t err = sd_pwr_ctrl_del_on_chip_ldo(s_pwr_ctrl_handle); + if (err != ESP_OK) { + ESP_LOGE(TAG, "failed to delete on-chip LDO power control driver: %s", esp_err_to_name(err)); + } + s_pwr_ctrl_handle = NULL; + } +} +#else +static inline esp_err_t init_power_control(sdmmc_host_t *host) +{ + (void)host; + return ESP_OK; +} +static inline void deinit_power_control(void) {} +#endif + +/* ---------------------------------------------------------------------- */ + +#if CONFIG_EXAMPLE_SD_HOST_SDMMC + +static esp_err_t init_card(void) +{ + ESP_LOGI(TAG, "Whole disk: SD/eMMC card via SDMMC (slot %d)", s_host.slot); + + sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT(); +#if CONFIG_EXAMPLE_SDMMC_BUS_WIDTH_1 + slot_config.width = 1; +#else + slot_config.width = 4; +#endif + +#if CONFIG_EXAMPLE_SDMMC_INTERNAL_PULLUP + /* Boards without external pull-ups on the SD lines (e.g. ESP32-S3-USB-OTG) + * need the internal ones enabled. */ + slot_config.flags |= SDMMC_SLOT_FLAG_INTERNAL_PULLUP; +#endif + +#if SOC_SDMMC_USE_GPIO_MATRIX + /* On SoCs that route the SDMMC controller through the GPIO matrix the slot + * pins are assignable, so apply the configured GPIOs. */ + slot_config.clk = CONFIG_EXAMPLE_SDMMC_PIN_CLK; + slot_config.cmd = CONFIG_EXAMPLE_SDMMC_PIN_CMD; + slot_config.d0 = CONFIG_EXAMPLE_SDMMC_PIN_D0; +#if CONFIG_EXAMPLE_SDMMC_BUS_WIDTH_4 + slot_config.d1 = CONFIG_EXAMPLE_SDMMC_PIN_D1; + slot_config.d2 = CONFIG_EXAMPLE_SDMMC_PIN_D2; + slot_config.d3 = CONFIG_EXAMPLE_SDMMC_PIN_D3; +#endif +#endif // SOC_SDMMC_USE_GPIO_MATRIX + + ESP_RETURN_ON_ERROR(init_power_control(&s_host), TAG, "power control init failed"); + ESP_RETURN_ON_ERROR(sdmmc_host_init(), TAG, "SDMMC host init failed"); + ESP_RETURN_ON_ERROR(sdmmc_host_init_slot(s_host.slot, &slot_config), TAG, "SDMMC slot init failed"); + return ESP_OK; +} + +static void deinit_card(void) +{ + sdmmc_host_deinit(); + deinit_power_control(); +} + +#elif CONFIG_EXAMPLE_SD_HOST_SDSPI + +static esp_err_t init_card(void) +{ + ESP_LOGI(TAG, "Whole disk: SD card via SPI (SDSPI)"); + + /* SDSPI_HOST_DEFAULT() puts the SPI host id in host.slot; keep it for later + * as host.slot is overwritten with the SDSPI device handle below. */ + s_spi_host_id = s_host.slot; + + spi_bus_config_t bus_cfg = { + .mosi_io_num = CONFIG_EXAMPLE_SDSPI_PIN_MOSI, + .miso_io_num = CONFIG_EXAMPLE_SDSPI_PIN_MISO, + .sclk_io_num = CONFIG_EXAMPLE_SDSPI_PIN_CLK, + .quadwp_io_num = -1, + .quadhd_io_num = -1, + .max_transfer_sz = 4000, + }; + + ESP_RETURN_ON_ERROR(init_power_control(&s_host), TAG, "power control init failed"); + ESP_RETURN_ON_ERROR(spi_bus_initialize(s_spi_host_id, &bus_cfg, SDSPI_DEFAULT_DMA), TAG, + "failed to initialize SPI bus"); + + sdspi_device_config_t dev_config = SDSPI_DEVICE_CONFIG_DEFAULT(); + dev_config.gpio_cs = CONFIG_EXAMPLE_SDSPI_PIN_CS; + dev_config.host_id = s_spi_host_id; + + sdspi_dev_handle_t dev_handle; + ESP_RETURN_ON_ERROR(sdspi_host_init(), TAG, "SDSPI host init failed"); + ESP_RETURN_ON_ERROR(sdspi_host_init_device(&dev_config, &dev_handle), TAG, "SDSPI device init failed"); + + /* Route the card commands through the freshly created SDSPI device. */ + s_host.slot = dev_handle; + return ESP_OK; +} + +static void deinit_card(void) +{ + sdspi_host_deinit(); + spi_bus_free(s_spi_host_id); + deinit_power_control(); +} + +#endif + +/* ---------------------------------------------------------------------- */ + +esp_err_t example_sd_card_bdl_create(esp_blockdev_handle_t *out) +{ + esp_err_t ret = ESP_OK; + ESP_LOGW(TAG, "This will overwrite the card's existing partition table!"); + + ESP_RETURN_ON_ERROR(init_card(), TAG, "card host init failed"); + + s_card = calloc(1, sizeof(sdmmc_card_t)); + ESP_GOTO_ON_FALSE(s_card != NULL, ESP_ERR_NO_MEM, cleanup, TAG, "no mem for card"); + + ESP_GOTO_ON_ERROR(sdmmc_card_init(&s_host, s_card), cleanup, TAG, "SD/eMMC card init failed"); + sdmmc_card_print_info(stdout, s_card); + + ESP_GOTO_ON_ERROR(sdmmc_get_blockdev(s_card, out), cleanup, TAG, "SD/eMMC block device creation failed"); + return ESP_OK; + +cleanup: + free(s_card); + s_card = NULL; + deinit_card(); + return ret; +} + +esp_err_t example_sd_card_bdl_release(esp_blockdev_handle_t disk) +{ + esp_err_t err = disk->ops->release(disk); + deinit_card(); + free(s_card); + s_card = NULL; + return err; +} + +#endif /* CONFIG_EXAMPLE_STORAGE_MEDIA_SDCARD */ diff --git a/examples/storage/generic_partition_bdl/main/sd_card_bdl.h b/examples/storage/generic_partition_bdl/main/sd_card_bdl.h new file mode 100644 index 00000000000..32678da9e65 --- /dev/null +++ b/examples/storage/generic_partition_bdl/main/sd_card_bdl.h @@ -0,0 +1,38 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ + +#pragma once + +#include "esp_err.h" +#include "esp_blockdev.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Create a whole-disk BDL backed by an SD/eMMC card. + * + * Initializes the card over the host peripheral selected in menuconfig + * (SDMMC or SDSPI) and returns a block device that maps the whole card. + * + * @param[out] out Handle of the created whole-disk block device. + * @return ESP_OK on success, an error code otherwise. + */ +esp_err_t example_sd_card_bdl_create(esp_blockdev_handle_t *out); + +/** + * @brief Release a whole-disk BDL previously created with + * example_sd_card_bdl_create() and de-initialize the card host. + * + * @param disk Handle returned by example_sd_card_bdl_create(). + * @return ESP_OK on success, an error code otherwise. + */ +esp_err_t example_sd_card_bdl_release(esp_blockdev_handle_t disk); + +#ifdef __cplusplus +} +#endif diff --git a/examples/storage/generic_partition_bdl/partitions_example.csv b/examples/storage/generic_partition_bdl/partitions_example.csv new file mode 100644 index 00000000000..cfc1ad907fa --- /dev/null +++ b/examples/storage/generic_partition_bdl/partitions_example.csv @@ -0,0 +1,8 @@ +# Name, Type, SubType, Offset, Size, Flags +# Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap +nvs, data, nvs, 0x9000, 0x6000, +phy_init, data, phy, 0xf000, 0x1000, +factory, app, factory, 0x10000, 1M, +# Whole-disk data partition. The example writes an MBR onto it at run time and +# splits it into a FAT and a LittleFS sub-partition using generic-partition BDLs. +storage, data, fat, , 2M, diff --git a/examples/storage/generic_partition_bdl/pytest_generic_partition_bdl_example.py b/examples/storage/generic_partition_bdl/pytest_generic_partition_bdl_example.py new file mode 100644 index 00000000000..d39e3998b76 --- /dev/null +++ b/examples/storage/generic_partition_bdl/pytest_generic_partition_bdl_example.py @@ -0,0 +1,51 @@ +# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Unlicense OR CC0-1.0 +import pytest +from pytest_embedded import Dut +from pytest_embedded_idf.utils import idf_parametrize + + +def _expect_partition_bdl_flow(dut: Dut) -> None: + # The partitions are enumerated in on-disk order: LittleFS (MBR entry 0) comes + # before FATFS (MBR entry 1), so the mount/read-back logs appear in that order. + dut.expect('example: Writing MBR partition table to the whole disk', timeout=90) + dut.expect('example: Reading MBR partition table back', timeout=90) + dut.expect('example: Mounting LittleFS on the LittleFS partition BDL', timeout=90) + dut.expect( + "example: Read back from /littlefs/hello.txt: 'Hello from LittleFS over a generic-partition BDL!'", + timeout=90, + ) + dut.expect('example: Mounting FATFS on the FAT partition BDL', timeout=90) + dut.expect("example: Read back from /fat/hello.txt: 'Hello from FATFS over a generic-partition BDL!'", timeout=90) + dut.expect('example: Done', timeout=90) + + +@pytest.mark.generic +@pytest.mark.parametrize('config', ['spiflash'], indirect=True) +@idf_parametrize('target', ['esp32'], indirect=['target']) +def test_examples_generic_partition_bdl_spiflash(dut: Dut) -> None: + _expect_partition_bdl_flow(dut) + + +# The SPI-flash configuration needs no external peripherals, so it can also run +# under QEMU (which emulates SPI flash but has no SD host). The SD card configs +# below require real hardware and therefore have no QEMU variant. +@pytest.mark.qemu +@pytest.mark.parametrize('config', ['spiflash'], indirect=True) +@idf_parametrize('target', ['esp32'], indirect=['target']) +def test_examples_generic_partition_bdl_spiflash_qemu(dut: Dut) -> None: + _expect_partition_bdl_flow(dut) + + +@pytest.mark.sdcard_sdmode +@pytest.mark.parametrize('config', ['sdmmc'], indirect=True) +@idf_parametrize('target', ['esp32'], indirect=['target']) +def test_examples_generic_partition_bdl_sdmmc(dut: Dut) -> None: + _expect_partition_bdl_flow(dut) + + +@pytest.mark.sdcard_spimode +@pytest.mark.parametrize('config', ['sdspi'], indirect=True) +@idf_parametrize('target', ['esp32'], indirect=['target']) +def test_examples_generic_partition_bdl_sdspi(dut: Dut) -> None: + _expect_partition_bdl_flow(dut) diff --git a/examples/storage/generic_partition_bdl/sdkconfig.ci.sdmmc b/examples/storage/generic_partition_bdl/sdkconfig.ci.sdmmc new file mode 100644 index 00000000000..efced7c1e90 --- /dev/null +++ b/examples/storage/generic_partition_bdl/sdkconfig.ci.sdmmc @@ -0,0 +1,9 @@ +CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y + +# Whole-disk block device: SD/eMMC card over the SDMMC host +CONFIG_EXAMPLE_STORAGE_MEDIA_SDCARD=y +CONFIG_EXAMPLE_SD_HOST_SDMMC=y + +# FATFS +# Runtime FAT sector size is derived from the SD card BDL geometry (512 bytes). +CONFIG_FATFS_VFS_FSTAT_BLKSIZE=4096 diff --git a/examples/storage/generic_partition_bdl/sdkconfig.ci.sdspi b/examples/storage/generic_partition_bdl/sdkconfig.ci.sdspi new file mode 100644 index 00000000000..27e43fe90a6 --- /dev/null +++ b/examples/storage/generic_partition_bdl/sdkconfig.ci.sdspi @@ -0,0 +1,9 @@ +CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y + +# Whole-disk block device: SD/eMMC card over the SPI bus (SDSPI) +CONFIG_EXAMPLE_STORAGE_MEDIA_SDCARD=y +CONFIG_EXAMPLE_SD_HOST_SDSPI=y + +# FATFS +# Runtime FAT sector size is derived from the SD card BDL geometry (512 bytes). +CONFIG_FATFS_VFS_FSTAT_BLKSIZE=4096 diff --git a/examples/storage/generic_partition_bdl/sdkconfig.ci.spiflash b/examples/storage/generic_partition_bdl/sdkconfig.ci.spiflash new file mode 100644 index 00000000000..e1111115f8f --- /dev/null +++ b/examples/storage/generic_partition_bdl/sdkconfig.ci.spiflash @@ -0,0 +1,11 @@ +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions_example.csv" +CONFIG_PARTITION_TABLE_FILENAME="partitions_example.csv" +CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y + +# Whole-disk block device: SPI flash 'storage' data partition +CONFIG_EXAMPLE_STORAGE_MEDIA_SPIFLASH=y + +# FATFS +# CONFIG_FATFS_SECTOR_4096=y # Does nothing in this case as the partitions are generated at run-time +CONFIG_FATFS_VFS_FSTAT_BLKSIZE=4096 diff --git a/examples/storage/generic_partition_bdl/sdkconfig.defaults b/examples/storage/generic_partition_bdl/sdkconfig.defaults new file mode 100644 index 00000000000..58665f0eced --- /dev/null +++ b/examples/storage/generic_partition_bdl/sdkconfig.defaults @@ -0,0 +1,8 @@ +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions_example.csv" +CONFIG_PARTITION_TABLE_FILENAME="partitions_example.csv" +CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y +# FATFS +# CONFIG_FATFS_SECTOR_* only selects the sector size for host-generated FAT images. +# This example formats at run time using the sector size derived from BDL geometry. +CONFIG_FATFS_VFS_FSTAT_BLKSIZE=4096