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.
This commit is contained in:
Adam Múdry
2026-07-02 14:13:22 +02:00
parent 9d8b1812fd
commit 98fee5a68a
17 changed files with 1182 additions and 0 deletions

View File

@@ -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
-------------

View File

@@ -157,6 +157,11 @@
.. _blockdev-apis:
应用示例
--------
- :example:`storage/generic_partition_bdl` 演示了如何仅使用块设备层在运行时对裸块设备进行分区:将 MBR 分区表写入整盘 BDLSPI flash 数据分区或 SD/eMMC 卡),并在通用分区 BDL 切片上挂载 FATFS 和 LittleFS。
API 参考
--------

View File

@@ -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

View File

@@ -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)

View File

@@ -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.

View File

@@ -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 ".")

View File

@@ -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

View File

@@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <inttypes.h>
#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);
}

View File

@@ -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"

View File

@@ -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 <stdlib.h>
#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 */

View File

@@ -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

View File

@@ -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,
1 # Name, Type, SubType, Offset, Size, Flags
2 # Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap
3 nvs, data, nvs, 0x9000, 0x6000,
4 phy_init, data, phy, 0xf000, 0x1000,
5 factory, app, factory, 0x10000, 1M,
6 # Whole-disk data partition. The example writes an MBR onto it at run time and
7 # splits it into a FAT and a LittleFS sub-partition using generic-partition BDLs.
8 storage, data, fat, , 2M,

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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