feat(fatfs): Added BDL support to FatFS component

This commit is contained in:
Martin Vychodil
2026-03-23 21:41:15 +01:00
parent 6ab1721056
commit 7043fb0d14
32 changed files with 1438 additions and 7 deletions

View File

@@ -3,17 +3,19 @@ idf_build_get_property(target IDF_TARGET)
set(srcs "diskio/diskio.c"
"diskio/diskio_rawflash.c"
"diskio/diskio_wl.c"
"diskio/diskio_bdl.c"
"src/ff.c"
"src/ffunicode.c")
set(include_dirs "diskio" "src")
set(requires "wear_levelling")
set(requires "wear_levelling" "esp_blockdev")
# for linux, we do not have support for sdmmc, for real targets, add respective sources
if(${target} STREQUAL "linux")
list(APPEND srcs "port/linux/ffsystem.c"
"vfs/vfs_fat.c")
"vfs/vfs_fat.c"
"vfs/vfs_fat_bdl.c")
list(APPEND include_dirs "vfs")
list(APPEND priv_requires "vfs" "linux")
else()
@@ -21,7 +23,8 @@ else()
"diskio/diskio_sdmmc.c"
"vfs/vfs_fat.c"
"vfs/vfs_fat_sdmmc.c"
"vfs/vfs_fat_spiflash.c")
"vfs/vfs_fat_spiflash.c"
"vfs/vfs_fat_bdl.c")
list(APPEND include_dirs "vfs")

View File

@@ -0,0 +1,274 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <string.h>
#include "diskio_impl.h"
#include "ffconf.h"
#include "ff.h"
#include "esp_log.h"
#include "diskio_bdl.h"
#include "esp_compiler.h"
static const char *TAG = "ff_diskio_bdl";
/* ------------------------------------------------------------------ */
/* LCM helpers for FatFS sector-size derivation from BDL geometry */
/* ------------------------------------------------------------------ */
static inline size_t gcd_size(size_t a, size_t b)
{
while (b != 0) {
size_t t = b;
b = a % b;
a = t;
}
return a;
}
static inline size_t lcm2_size(size_t a, size_t b)
{
return (a && b) ? (a / gcd_size(a, b)) * b : 0;
}
/**
* Derive the FatFS logical sector size purely from BDL geometry.
*
* The sector must be a common multiple of read_size, write_size and
* FF_MIN_SS (typically 512). When erase_size can be included without
* exceeding FF_MAX_SS the sector is also erase-aligned — correct for
* NOR-style devices and optimal for any device. When erase alignment
* would push the sector beyond FF_MAX_SS (typical for NAND where
* erase blocks >> page size) erase_size is omitted; such devices must
* handle erase internally (FTL / wear-levelling layer).
*
* No BDL flags are inspected — the NOR/NAND distinction is implicit
* in the geometry: NOR erase blocks fit within FF_MAX_SS, NAND ones
* do not.
*
* @return valid power-of-two sector size in [FF_MIN_SS, FF_MAX_SS],
* or 0 if the geometry is incompatible with FatFS.
*/
static size_t compute_fs_sector_size(esp_blockdev_handle_t dev)
{
const esp_blockdev_geometry_t *g = &dev->geometry;
size_t result = (size_t)FF_MIN_SS;
if (g->read_size > 1) {
result = lcm2_size(result, g->read_size);
}
if (g->write_size > 1) {
result = lcm2_size(result, g->write_size);
}
if (g->erase_size > 1) {
size_t with_erase = lcm2_size(result, g->erase_size);
if (with_erase && with_erase <= FF_MAX_SS) {
result = with_erase;
}
}
if (result < FF_MIN_SS || result > FF_MAX_SS || (result & (result - 1)) != 0) {
return 0;
}
return result;
}
/* ------------------------------------------------------------------ */
typedef struct {
esp_blockdev_handle_t handle;
size_t fs_sector_size;
} bdl_drive_t;
static bdl_drive_t s_bdl_drives[FF_VOLUMES];
static DSTATUS ff_bdl_initialize(BYTE pdrv)
{
esp_blockdev_handle_t dev = s_bdl_drives[pdrv].handle;
assert(dev != ESP_BLOCKDEV_HANDLE_INVALID);
if (dev->device_flags.read_only) {
return STA_PROTECT;
}
return 0;
}
static DSTATUS ff_bdl_status(BYTE pdrv)
{
esp_blockdev_handle_t dev = s_bdl_drives[pdrv].handle;
assert(dev != ESP_BLOCKDEV_HANDLE_INVALID);
if (dev->device_flags.read_only) {
return STA_PROTECT;
}
return 0;
}
static DRESULT ff_bdl_read(BYTE pdrv, BYTE *buff, DWORD sector, UINT count)
{
bdl_drive_t *drv = &s_bdl_drives[pdrv];
assert(drv->handle != ESP_BLOCKDEV_HANDLE_INVALID);
size_t sec_size = drv->fs_sector_size;
ESP_LOGV(TAG, "read - pdrv=%u, sector=%lu, count=%u, sec_size=%u",
(unsigned)pdrv, (unsigned long)sector, (unsigned)count, (unsigned)sec_size);
esp_err_t err = drv->handle->ops->read(drv->handle, buff, count * sec_size,
(uint64_t)sector * sec_size, count * sec_size);
if (unlikely(err != ESP_OK)) {
ESP_LOGE(TAG, "BDL read failed (0x%x)", err);
return RES_ERROR;
}
return RES_OK;
}
static DRESULT ff_bdl_write(BYTE pdrv, const BYTE *buff, DWORD sector, UINT count)
{
bdl_drive_t *drv = &s_bdl_drives[pdrv];
assert(drv->handle != ESP_BLOCKDEV_HANDLE_INVALID);
if (drv->handle->device_flags.read_only) {
return RES_WRPRT;
}
size_t sec_size = drv->fs_sector_size;
uint64_t addr = (uint64_t)sector * sec_size;
size_t len = count * sec_size;
ESP_LOGV(TAG, "write - pdrv=%u, sector=%lu, count=%u", (unsigned)pdrv, (unsigned long)sector, (unsigned)count);
if (drv->handle->device_flags.erase_before_write || drv->handle->device_flags.and_type_write) {
size_t erase_sz = drv->handle->geometry.erase_size;
if ((addr % erase_sz == 0) && (len % erase_sz == 0)) {
esp_err_t err = drv->handle->ops->erase(drv->handle, addr, len);
if (unlikely(err != ESP_OK)) {
ESP_LOGE(TAG, "BDL erase failed (0x%x)", err);
return RES_ERROR;
}
}
}
esp_err_t err = drv->handle->ops->write(drv->handle, buff, addr, len);
if (unlikely(err != ESP_OK)) {
ESP_LOGE(TAG, "BDL write failed (0x%x)", err);
return RES_ERROR;
}
return RES_OK;
}
static DRESULT ff_bdl_ioctl(BYTE pdrv, BYTE cmd, void *buff)
{
bdl_drive_t *drv = &s_bdl_drives[pdrv];
assert(drv->handle != ESP_BLOCKDEV_HANDLE_INVALID);
ESP_LOGV(TAG, "ioctl: cmd=%u", (unsigned)cmd);
switch (cmd) {
case CTRL_SYNC:
if (drv->handle->ops->sync) {
esp_err_t err = drv->handle->ops->sync(drv->handle);
if (unlikely(err != ESP_OK)) {
ESP_LOGE(TAG, "BDL sync failed (0x%x)", err);
return RES_ERROR;
}
}
return RES_OK;
case GET_SECTOR_COUNT:
*((DWORD *)buff) = (DWORD)(drv->handle->geometry.disk_size / drv->fs_sector_size);
return RES_OK;
case GET_SECTOR_SIZE:
*((WORD *)buff) = (WORD)drv->fs_sector_size;
return RES_OK;
case GET_BLOCK_SIZE: {
size_t erase_sz = drv->handle->geometry.erase_size;
*((DWORD *)buff) = (erase_sz >= drv->fs_sector_size)
? (DWORD)(erase_sz / drv->fs_sector_size)
: 1;
return RES_OK;
}
#if FF_USE_TRIM
case CTRL_TRIM: {
if (drv->handle->ops->ioctl == NULL) {
return RES_OK;
}
size_t sec_size = drv->fs_sector_size;
DWORD start_sector = *((DWORD *)buff);
DWORD end_sector = *((DWORD *)buff + 1);
esp_blockdev_cmd_arg_erase_t erase_arg = {
.start_addr = (uint64_t)start_sector * sec_size,
.erase_len = (size_t)(end_sector - start_sector + 1) * sec_size,
};
esp_err_t err = drv->handle->ops->ioctl(drv->handle, ESP_BLOCKDEV_CMD_MARK_DELETED, &erase_arg);
if (unlikely(err != ESP_OK && err != ESP_ERR_NOT_SUPPORTED)) {
ESP_LOGE(TAG, "BDL TRIM ioctl failed (0x%x)", err);
return RES_ERROR;
}
return RES_OK;
}
#endif
}
return RES_ERROR;
}
esp_err_t ff_diskio_register_bdl(BYTE pdrv, esp_blockdev_handle_t bdl_handle)
{
if (pdrv >= FF_VOLUMES) {
return ESP_ERR_INVALID_ARG;
}
if (bdl_handle == ESP_BLOCKDEV_HANDLE_INVALID) {
return ESP_ERR_INVALID_ARG;
}
if (bdl_handle->geometry.read_size == 0 || bdl_handle->geometry.disk_size == 0) {
return ESP_ERR_INVALID_ARG;
}
size_t fs_sec = compute_fs_sector_size(bdl_handle);
if (fs_sec == 0) {
ESP_LOGE(TAG, "BDL geometry incompatible with FatFS "
"(read=%u, write=%u, erase=%u, FF_MAX_SS=%u)",
(unsigned)bdl_handle->geometry.read_size,
(unsigned)bdl_handle->geometry.write_size,
(unsigned)bdl_handle->geometry.erase_size,
(unsigned)FF_MAX_SS);
return ESP_ERR_INVALID_ARG;
}
static const ff_diskio_impl_t bdl_impl = {
.init = &ff_bdl_initialize,
.status = &ff_bdl_status,
.read = &ff_bdl_read,
.write = &ff_bdl_write,
.ioctl = &ff_bdl_ioctl
};
s_bdl_drives[pdrv] = (bdl_drive_t){
.handle = bdl_handle,
.fs_sector_size = fs_sec,
};
ff_diskio_register(pdrv, &bdl_impl);
ESP_LOGD(TAG, "pdrv=%u registered, fs_sector_size=%u, erase_size=%u, disk_size=%llu",
(unsigned)pdrv, (unsigned)fs_sec,
(unsigned)bdl_handle->geometry.erase_size,
(unsigned long long)bdl_handle->geometry.disk_size);
return ESP_OK;
}
BYTE ff_diskio_get_pdrv_bdl(esp_blockdev_handle_t bdl_handle)
{
for (int i = 0; i < FF_VOLUMES; i++) {
if (bdl_handle == s_bdl_drives[i].handle) {
return i;
}
}
return 0xff;
}
void ff_diskio_clear_pdrv_bdl(esp_blockdev_handle_t bdl_handle)
{
for (int i = 0; i < FF_VOLUMES; i++) {
if (bdl_handle == s_bdl_drives[i].handle) {
s_bdl_drives[i] = (bdl_drive_t){0};
}
}
}

View File

@@ -0,0 +1,66 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#include "esp_blockdev.h"
#include "esp_err.h"
/**
* @brief Register a BDL (Block Device Layer) device as a FatFS diskio driver
*
* The FatFS logical sector size is derived from BDL geometry at registration
* time as: LCM(FF_MIN_SS, read_size, write_size [, erase_size]).
* erase_size is included only when the result still fits within FF_MAX_SS;
* this makes the sector erase-aligned for NOR-style devices (small erase
* blocks) and page-aligned for NAND-style devices (large erase blocks,
* erase handled internally by an FTL/WL layer). No BDL flags are
* inspected — the NOR/NAND distinction is implicit in the geometry.
*
* The computed sector size is cached and used for all subsequent I/O.
* GET_BLOCK_SIZE returns erase_size / sector_size (minimum 1) so that
* FatFS can align clusters to erase boundaries.
*
* Erase-before-write is performed when device_flags.erase_before_write or
* device_flags.and_type_write is set and the write range is aligned to
* geometry.erase_size. Read-only
* devices are supported (write returns RES_WRPRT, status returns
* STA_PROTECT).
*
* @param pdrv Drive number (0..FF_VOLUMES-1)
* @param bdl_handle BDL device handle providing the storage
*
* @return
* - ESP_OK on success
* - ESP_ERR_INVALID_ARG if pdrv is out of range, bdl_handle is invalid,
* or geometry is incompatible with FatFS (sector size not a power of
* two or exceeds FF_MAX_SS)
*/
esp_err_t ff_diskio_register_bdl(unsigned char pdrv, esp_blockdev_handle_t bdl_handle);
/**
* @brief Get the drive number associated with a BDL handle
*
* @param bdl_handle BDL device handle to look up
*
* @return Drive number (0..FF_VOLUMES-1) or 0xFF if not found
*/
unsigned char ff_diskio_get_pdrv_bdl(esp_blockdev_handle_t bdl_handle);
/**
* @brief Clear the internal BDL handle association for a given handle
*
* @param bdl_handle BDL device handle to clear
*/
void ff_diskio_clear_pdrv_bdl(esp_blockdev_handle_t bdl_handle);
#ifdef __cplusplus
}
#endif

View File

@@ -5,3 +5,7 @@ components/fatfs/host_test:
- if: IDF_TARGET == "esp32p4"
temporary: true
reason: test not pass, should be re-enable # TODO: IDF-8980
components/fatfs/host_test/bdl:
enable:
- if: IDF_TARGET == "linux"

View File

@@ -0,0 +1,7 @@
cmake_minimum_required(VERSION 3.22)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
set(COMPONENTS main)
list(APPEND EXTRA_COMPONENT_DIRS "$ENV{IDF_PATH}/tools/mocks/freertos/")
project(fatfs_bdl_host_test)

View File

@@ -0,0 +1,6 @@
idf_component_register(SRCS "test_fatfs_bdl.cpp"
REQUIRES fatfs vfs esp_blockdev esp_blockdev_util wear_levelling
WHOLE_ARCHIVE
)
target_link_libraries(${COMPONENT_LIB} PRIVATE Catch2WithMain)

View File

@@ -0,0 +1,2 @@
dependencies:
espressif/catch2: "^3.4.0"

View File

@@ -0,0 +1,276 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <cstdio>
#include <cstring>
#include <fcntl.h>
#include <sys/stat.h>
#include "ff.h"
#include "diskio_impl.h"
#include "diskio_bdl.h"
#include "esp_blockdev.h"
#include "esp_blockdev/memory.h"
#include "esp_vfs_fat.h"
#include "esp_vfs.h"
#include "esp_partition.h"
#include "wear_levelling.h"
#include "diskio_wl.h"
#include <catch2/catch_test_macros.hpp>
/* ===================================================================== */
/* Test 1: FatFS directly on memory BDL (no wear-levelling) */
/* ===================================================================== */
TEST_CASE("BDL: Create volume on memory BDL, write and read back data", "[fatfs][bdl]")
{
static uint8_t backing[256 * 1024];
memset(backing, 0xFF, sizeof(backing));
const esp_blockdev_geometry_t geometry = {
.disk_size = sizeof(backing),
.read_size = 1,
.write_size = 1,
.erase_size = 4096,
.recommended_write_size = 0,
.recommended_read_size = 0,
.recommended_erase_size = 0,
};
esp_blockdev_handle_t mem_dev = NULL;
REQUIRE(esp_blockdev_memory_get_from_buffer(backing, sizeof(backing),
&geometry, false, &mem_dev) == ESP_OK);
BYTE pdrv;
REQUIRE(ff_diskio_get_drive(&pdrv) == ESP_OK);
REQUIRE(ff_diskio_register_bdl(pdrv, mem_dev) == ESP_OK);
char drv[3] = {(char)('0' + pdrv), ':', 0};
LBA_t part_list[] = {100, 0, 0, 0};
BYTE work_area[FF_MAX_SS];
REQUIRE(f_fdisk(pdrv, part_list, work_area) == FR_OK);
const MKFS_PARM opt = {(BYTE)(FM_ANY | FM_SFD), 0, 0, 128, 0};
REQUIRE(f_mkfs(drv, &opt, work_area, sizeof(work_area)) == FR_OK);
FATFS fs;
REQUIRE(f_mount(&fs, drv, 1) == FR_OK);
FIL file;
UINT bw;
REQUIRE(f_open(&file, "test.txt", FA_OPEN_ALWAYS | FA_READ | FA_WRITE) == FR_OK);
uint32_t data_size = 1000;
char *data = (char *)malloc(data_size);
char *read_buf = (char *)malloc(data_size);
for (uint32_t i = 0; i < data_size; i += sizeof(i)) {
*((uint32_t *)(data + i)) = i;
}
REQUIRE(f_write(&file, data, data_size, &bw) == FR_OK);
REQUIRE(bw == data_size);
REQUIRE(f_lseek(&file, 0) == FR_OK);
REQUIRE(f_read(&file, read_buf, data_size, &bw) == FR_OK);
REQUIRE(bw == data_size);
REQUIRE(memcmp(data, read_buf, data_size) == 0);
REQUIRE(f_close(&file) == FR_OK);
REQUIRE(f_mount(0, drv, 0) == FR_OK);
free(read_buf);
free(data);
ff_diskio_unregister(pdrv);
ff_diskio_clear_pdrv_bdl(mem_dev);
mem_dev->ops->release(mem_dev);
}
/* ===================================================================== */
/* Test 2: FatFS BDL diskio driver registration and geometry */
/* ===================================================================== */
TEST_CASE("BDL: Geometry is correctly reported via ioctl", "[fatfs][bdl]")
{
static uint8_t backing[128 * 1024];
memset(backing, 0xFF, sizeof(backing));
const esp_blockdev_geometry_t geometry = {
.disk_size = sizeof(backing),
.read_size = 1,
.write_size = 1,
.erase_size = 512,
.recommended_write_size = 0,
.recommended_read_size = 0,
.recommended_erase_size = 0,
};
esp_blockdev_handle_t mem_dev = NULL;
REQUIRE(esp_blockdev_memory_get_from_buffer(backing, sizeof(backing),
&geometry, false, &mem_dev) == ESP_OK);
BYTE pdrv;
REQUIRE(ff_diskio_get_drive(&pdrv) == ESP_OK);
REQUIRE(ff_diskio_register_bdl(pdrv, mem_dev) == ESP_OK);
WORD sec_size = 0;
REQUIRE(ff_disk_ioctl(pdrv, GET_SECTOR_SIZE, &sec_size) == RES_OK);
REQUIRE(sec_size == 512);
DWORD sec_count = 0;
REQUIRE(ff_disk_ioctl(pdrv, GET_SECTOR_COUNT, &sec_count) == RES_OK);
REQUIRE(sec_count == sizeof(backing) / 512);
REQUIRE(ff_disk_ioctl(pdrv, CTRL_SYNC, NULL) == RES_OK);
ff_diskio_unregister(pdrv);
ff_diskio_clear_pdrv_bdl(mem_dev);
mem_dev->ops->release(mem_dev);
}
/* ===================================================================== */
/* Test 3: FatFS BDL pdrv lookup functions */
/* ===================================================================== */
TEST_CASE("BDL: pdrv lookup and clear", "[fatfs][bdl]")
{
static uint8_t backing[64 * 1024];
memset(backing, 0xFF, sizeof(backing));
const esp_blockdev_geometry_t geometry = {
.disk_size = sizeof(backing),
.read_size = 1,
.write_size = 1,
.erase_size = 4096,
.recommended_write_size = 0,
.recommended_read_size = 0,
.recommended_erase_size = 0,
};
esp_blockdev_handle_t mem_dev = NULL;
REQUIRE(esp_blockdev_memory_get_from_buffer(backing, sizeof(backing),
&geometry, false, &mem_dev) == ESP_OK);
REQUIRE(ff_diskio_get_pdrv_bdl(mem_dev) == 0xff);
BYTE pdrv;
REQUIRE(ff_diskio_get_drive(&pdrv) == ESP_OK);
REQUIRE(ff_diskio_register_bdl(pdrv, mem_dev) == ESP_OK);
REQUIRE(ff_diskio_get_pdrv_bdl(mem_dev) == pdrv);
ff_diskio_clear_pdrv_bdl(mem_dev);
REQUIRE(ff_diskio_get_pdrv_bdl(mem_dev) == 0xff);
ff_diskio_unregister(pdrv);
mem_dev->ops->release(mem_dev);
}
/* ===================================================================== */
/* Test 4: FatFS BDL VFS mount/unmount via esp_vfs_fat_bdl_mount() */
/* ===================================================================== */
TEST_CASE("BDL VFS: mount, write and read via POSIX API", "[fatfs][bdl][vfs]")
{
static uint8_t backing[256 * 1024];
memset(backing, 0xFF, sizeof(backing));
const esp_blockdev_geometry_t geometry = {
.disk_size = sizeof(backing),
.read_size = 1,
.write_size = 1,
.erase_size = 4096,
.recommended_write_size = 0,
.recommended_read_size = 0,
.recommended_erase_size = 0,
};
esp_blockdev_handle_t mem_dev = NULL;
REQUIRE(esp_blockdev_memory_get_from_buffer(backing, sizeof(backing),
&geometry, false, &mem_dev) == ESP_OK);
esp_vfs_fat_mount_config_t mount_config = {
.format_if_mount_failed = true,
.max_files = 5,
};
REQUIRE(esp_vfs_fat_bdl_mount("/bdl", mem_dev, &mount_config) == ESP_OK);
const char *test_str = "BDL FatFS test data!\n";
const char *filename = "/bdl/hello.txt";
int fd = open(filename, O_CREAT | O_RDWR, 0777);
REQUIRE(fd != -1);
ssize_t sz = write(fd, test_str, strlen(test_str));
REQUIRE(sz == (ssize_t)strlen(test_str));
REQUIRE(0 == close(fd));
fd = open(filename, O_RDONLY);
REQUIRE(fd != -1);
char buf[64] = {};
sz = read(fd, buf, sizeof(buf));
REQUIRE(sz == (ssize_t)strlen(test_str));
REQUIRE(0 == memcmp(buf, test_str, strlen(test_str)));
REQUIRE(0 == close(fd));
REQUIRE(esp_vfs_fat_bdl_unmount("/bdl", mem_dev) == ESP_OK);
mem_dev->ops->release(mem_dev);
}
/* ===================================================================== */
/* Test 5: FatFS BDL on partition (via WL legacy path for reference) */
/* Uses the classic WL path alongside BDL to show they coexist. */
/* ===================================================================== */
TEST_CASE("BDL and legacy WL coexist on different drives", "[fatfs][bdl]")
{
static uint8_t backing[256 * 1024];
memset(backing, 0xFF, sizeof(backing));
const esp_blockdev_geometry_t geometry = {
.disk_size = sizeof(backing),
.read_size = 1,
.write_size = 1,
.erase_size = 4096,
.recommended_write_size = 0,
.recommended_read_size = 0,
.recommended_erase_size = 0,
};
esp_blockdev_handle_t mem_dev = NULL;
REQUIRE(esp_blockdev_memory_get_from_buffer(backing, sizeof(backing),
&geometry, false, &mem_dev) == ESP_OK);
const esp_partition_t *partition = esp_partition_find_first(
ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_FAT, "storage");
REQUIRE(partition != NULL);
wl_handle_t wl_handle;
REQUIRE(wl_mount(partition, &wl_handle) == ESP_OK);
BYTE pdrv_wl;
REQUIRE(ff_diskio_get_drive(&pdrv_wl) == ESP_OK);
REQUIRE(ff_diskio_register_wl_partition(pdrv_wl, wl_handle) == ESP_OK);
BYTE pdrv_bdl;
REQUIRE(ff_diskio_get_drive(&pdrv_bdl) == ESP_OK);
REQUIRE(ff_diskio_register_bdl(pdrv_bdl, mem_dev) == ESP_OK);
REQUIRE(pdrv_wl != pdrv_bdl);
WORD sec_size_wl = 0;
REQUIRE(ff_disk_ioctl(pdrv_wl, GET_SECTOR_SIZE, &sec_size_wl) == RES_OK);
WORD sec_size_bdl = 0;
REQUIRE(ff_disk_ioctl(pdrv_bdl, GET_SECTOR_SIZE, &sec_size_bdl) == RES_OK);
REQUIRE(sec_size_bdl == 4096);
ff_diskio_unregister(pdrv_bdl);
ff_diskio_clear_pdrv_bdl(mem_dev);
ff_diskio_unregister(pdrv_wl);
ff_diskio_clear_pdrv_wl(wl_handle);
REQUIRE(wl_unmount(wl_handle) == ESP_OK);
mem_dev->ops->release(mem_dev);
}

View File

@@ -0,0 +1,6 @@
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x6000,
phy_init, data, phy, 0xf000, 0x1000,
factory, app, factory, 0x10000, 1M,
storage, data, fat, , 1M,
storage2, data, fat, , 32k,
1 # Name Type SubType Offset Size Flags
2 nvs data nvs 0x9000 0x6000
3 phy_init data phy 0xf000 0x1000
4 factory app factory 0x10000 1M
5 storage data fat 1M
6 storage2 data fat 32k

View File

@@ -0,0 +1,11 @@
# 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
@pytest.mark.host_test
@idf_parametrize('target', ['linux'], indirect=['target'])
def test_fatfs_bdl_linux(dut: Dut) -> None:
dut.expect_exact('All tests passed', timeout=120)

View File

@@ -0,0 +1,11 @@
CONFIG_IDF_TARGET="linux"
CONFIG_COMPILER_CXX_EXCEPTIONS=y
CONFIG_UNITY_ENABLE_IDF_TEST_RUNNER=n
CONFIG_WL_SECTOR_SIZE=4096
CONFIG_LOG_DEFAULT_LEVEL=3
CONFIG_PARTITION_TABLE_OFFSET=0x8000
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partition_table.csv"
CONFIG_MMU_PAGE_SIZE=0X10000
CONFIG_ESP_PARTITION_ENABLE_STATS=y
CONFIG_FATFS_VOLUME_COUNT=3

View File

@@ -1,5 +1,15 @@
# Documentation: .gitlab/ci/README.md#manifest-file-to-control-the-buildtest-apps
components/fatfs/test_apps/bdl:
disable_test:
- if: IDF_TARGET != "esp32"
reason: only one target needed
depends_components:
- esp_blockdev
- esp_partition
- fatfs
- vfs
components/fatfs/test_apps/dyn_buffers:
disable_test:
- if: IDF_TARGET != "esp32"

View File

@@ -1,5 +1,5 @@
| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-C61 | ESP32-H2 | ESP32-P4 | ESP32-S2 | ESP32-S3 |
| ----------------- | ----- | -------- | -------- | -------- | -------- | --------- | -------- | -------- | -------- | -------- |
| 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 |
| ----------------- | ----- | -------- | -------- | -------- | -------- | --------- | -------- | --------- | -------- | -------- | -------- | -------- | --------- |
# fatfs component target tests

View File

@@ -0,0 +1,7 @@
cmake_minimum_required(VERSION 3.22)
set(COMPONENTS main)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(test_fatfs_bdl)

View File

@@ -0,0 +1,4 @@
idf_component_register(SRCS "test_fatfs_bdl.c"
INCLUDE_DIRS "."
PRIV_REQUIRES unity fatfs vfs esp_blockdev esp_partition
WHOLE_ARCHIVE)

View File

@@ -0,0 +1,220 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/unistd.h>
#include <sys/stat.h>
#include "unity.h"
#include "esp_log.h"
#include "esp_partition.h"
#include "esp_blockdev.h"
#include "esp_vfs.h"
#include "esp_vfs_fat.h"
#include "ff.h"
#include "diskio_impl.h"
#include "diskio_bdl.h"
static const char *TAG = "test_fatfs_bdl";
void app_main(void)
{
unity_run_menu();
}
/* ===================================================================== */
/* Helper: create partition BDL and erase it */
/* ===================================================================== */
static esp_blockdev_handle_t s_test_bdl = NULL;
static void test_setup_partition_bdl(const char *label)
{
esp_err_t err = esp_partition_get_blockdev(
ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_FAT,
label, &s_test_bdl);
TEST_ESP_OK(err);
TEST_ASSERT_NOT_NULL(s_test_bdl);
ESP_LOGI(TAG, "Partition BDL: disk_size=%llu, erase_size=%u",
(unsigned long long)s_test_bdl->geometry.disk_size,
(unsigned)s_test_bdl->geometry.erase_size);
}
static void test_teardown_partition_bdl(void)
{
if (s_test_bdl) {
s_test_bdl->ops->release(s_test_bdl);
s_test_bdl = NULL;
}
}
/* ===================================================================== */
/* Test: BDL diskio low-level on partition BDL */
/* ===================================================================== */
TEST_CASE("(BDL) diskio register, format, write and read on partition", "[fatfs][bdl]")
{
test_setup_partition_bdl("storage");
BYTE pdrv;
TEST_ESP_OK(ff_diskio_get_drive(&pdrv));
TEST_ESP_OK(ff_diskio_register_bdl(pdrv, s_test_bdl));
char drv[3] = {(char)('0' + pdrv), ':', 0};
WORD sec_size = 0;
TEST_ASSERT_EQUAL(RES_OK, ff_disk_ioctl(pdrv, GET_SECTOR_SIZE, &sec_size));
ESP_LOGI(TAG, "Sector size: %u", (unsigned)sec_size);
DWORD sec_count = 0;
TEST_ASSERT_EQUAL(RES_OK, ff_disk_ioctl(pdrv, GET_SECTOR_COUNT, &sec_count));
ESP_LOGI(TAG, "Sector count: %lu", (unsigned long)sec_count);
BYTE work_area[FF_MAX_SS];
const MKFS_PARM opt = {(BYTE)(FM_ANY | FM_SFD), 2, 0, 0, sec_size};
TEST_ASSERT_EQUAL(FR_OK, f_mkfs(drv, &opt, work_area, sizeof(work_area)));
FATFS fs;
TEST_ASSERT_EQUAL(FR_OK, f_mount(&fs, drv, 1));
FIL file;
UINT bw;
TEST_ASSERT_EQUAL(FR_OK, f_open(&file, "test.txt", FA_OPEN_ALWAYS | FA_READ | FA_WRITE));
const char *test_data = "Hello from FatFS over BDL partition!";
TEST_ASSERT_EQUAL(FR_OK, f_write(&file, test_data, strlen(test_data), &bw));
TEST_ASSERT_EQUAL(strlen(test_data), bw);
TEST_ASSERT_EQUAL(FR_OK, f_lseek(&file, 0));
char read_buf[128] = {};
TEST_ASSERT_EQUAL(FR_OK, f_read(&file, read_buf, sizeof(read_buf) - 1, &bw));
TEST_ASSERT_EQUAL(strlen(test_data), bw);
TEST_ASSERT_EQUAL_STRING(test_data, read_buf);
TEST_ASSERT_EQUAL(FR_OK, f_close(&file));
TEST_ASSERT_EQUAL(FR_OK, f_mount(0, drv, 0));
ff_diskio_unregister(pdrv);
ff_diskio_clear_pdrv_bdl(s_test_bdl);
test_teardown_partition_bdl();
}
/* ===================================================================== */
/* Test: BDL VFS mount/unmount on partition BDL */
/* ===================================================================== */
TEST_CASE("(BDL) VFS mount, file operations and unmount", "[fatfs][bdl]")
{
test_setup_partition_bdl("storage");
esp_vfs_fat_mount_config_t mount_config = {
.format_if_mount_failed = true,
.max_files = 5,
};
TEST_ESP_OK(esp_vfs_fat_bdl_mount("/bdltest", s_test_bdl, &mount_config));
const char *hello_str = "Hello from BDL VFS FatFS!\n";
const char *filename = "/bdltest/hello.txt";
FILE *f = fopen(filename, "w");
TEST_ASSERT_NOT_NULL(f);
fprintf(f, "%s", hello_str);
fclose(f);
f = fopen(filename, "r");
TEST_ASSERT_NOT_NULL(f);
char buf[128] = {};
TEST_ASSERT_NOT_NULL(fgets(buf, sizeof(buf), f));
fclose(f);
TEST_ASSERT_EQUAL_STRING(hello_str, buf);
struct stat st;
TEST_ASSERT_EQUAL(0, stat(filename, &st));
TEST_ASSERT_EQUAL(strlen(hello_str), st.st_size);
TEST_ASSERT_EQUAL(0, unlink(filename));
TEST_ESP_OK(esp_vfs_fat_bdl_unmount("/bdltest", s_test_bdl));
test_teardown_partition_bdl();
}
/* ===================================================================== */
/* Test: BDL geometry is correct for partition BDL */
/* ===================================================================== */
TEST_CASE("(BDL) partition BDL geometry matches partition size", "[fatfs][bdl]")
{
const esp_partition_t *part = esp_partition_find_first(
ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_FAT, "storage");
TEST_ASSERT_NOT_NULL(part);
test_setup_partition_bdl("storage");
TEST_ASSERT_EQUAL(part->size, s_test_bdl->geometry.disk_size);
TEST_ASSERT(s_test_bdl->geometry.erase_size > 0);
TEST_ASSERT(s_test_bdl->geometry.read_size > 0);
TEST_ASSERT_EQUAL(0, s_test_bdl->geometry.disk_size % s_test_bdl->geometry.erase_size);
test_teardown_partition_bdl();
}
/* ===================================================================== */
/* Test: Two BDL volumes on separate partitions */
/* ===================================================================== */
TEST_CASE("(BDL) two BDL volumes coexist", "[fatfs][bdl]")
{
esp_blockdev_handle_t bdl1 = NULL;
esp_blockdev_handle_t bdl2 = NULL;
TEST_ESP_OK(esp_partition_get_blockdev(
ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_FAT,
"storage", &bdl1));
TEST_ESP_OK(esp_partition_get_blockdev(
ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_FAT,
"storage2", &bdl2));
esp_vfs_fat_mount_config_t mount_config = {
.format_if_mount_failed = true,
.max_files = 5,
};
TEST_ESP_OK(esp_vfs_fat_bdl_mount("/bdl1", bdl1, &mount_config));
TEST_ESP_OK(esp_vfs_fat_bdl_mount("/bdl2", bdl2, &mount_config));
FILE *f1 = fopen("/bdl1/a.txt", "w");
TEST_ASSERT_NOT_NULL(f1);
fprintf(f1, "vol1");
fclose(f1);
FILE *f2 = fopen("/bdl2/b.txt", "w");
TEST_ASSERT_NOT_NULL(f2);
fprintf(f2, "vol2");
fclose(f2);
char buf[16] = {};
f1 = fopen("/bdl1/a.txt", "r");
TEST_ASSERT_NOT_NULL(f1);
fgets(buf, sizeof(buf), f1);
fclose(f1);
TEST_ASSERT_EQUAL_STRING("vol1", buf);
memset(buf, 0, sizeof(buf));
f2 = fopen("/bdl2/b.txt", "r");
TEST_ASSERT_NOT_NULL(f2);
fgets(buf, sizeof(buf), f2);
fclose(f2);
TEST_ASSERT_EQUAL_STRING("vol2", buf);
TEST_ESP_OK(esp_vfs_fat_bdl_unmount("/bdl1", bdl1));
TEST_ESP_OK(esp_vfs_fat_bdl_unmount("/bdl2", bdl2));
bdl1->ops->release(bdl1);
bdl2->ops->release(bdl2);
}

View File

@@ -0,0 +1,4 @@
# Name, Type, SubType, Offset, Size, Flags
factory, app, factory, 0x10000, 768k,
storage, data, fat, , 528k,
storage2, data, fat, , 528k,
1 # Name Type SubType Offset Size Flags
2 factory app factory 0x10000 768k
3 storage data fat 528k
4 storage2 data fat 528k

View File

@@ -0,0 +1,10 @@
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Unlicense OR CC0-1.0
import pytest
from pytest_embedded import Dut
@pytest.mark.esp32
@pytest.mark.generic
def test_fatfs_bdl(dut: Dut) -> None:
dut.run_all_single_board_cases(timeout=120)

View File

@@ -0,0 +1,14 @@
# General options for additional checks
CONFIG_HEAP_POISONING_COMPREHENSIVE=y
CONFIG_COMPILER_WARN_WRITE_STRINGS=y
CONFIG_BOOTLOADER_LOG_LEVEL_WARN=y
CONFIG_FREERTOS_WATCHPOINT_END_OF_STACK=y
CONFIG_COMPILER_STACK_CHECK_MODE_STRONG=y
CONFIG_COMPILER_STACK_CHECK=y
# disable task watchdog since this app uses an interactive menu
CONFIG_ESP_TASK_WDT_INIT=n
# use custom partition table
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"

View File

@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2015-2025 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2015-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
@@ -13,6 +13,7 @@
#endif
#include "ff.h"
#include "wear_levelling.h"
#include "esp_blockdev.h"
#ifdef __cplusplus
extern "C" {
@@ -403,6 +404,48 @@ esp_err_t esp_vfs_fat_spiflash_mount_ro(const char* base_path,
*/
esp_err_t esp_vfs_fat_spiflash_unmount_ro(const char* base_path, const char* partition_label);
/**
* @brief Convenience function to mount a FatFS volume on a BDL (Block Device Layer) device
*
* The FatFS logical sector size is derived from BDL geometry as
* LCM(FF_MIN_SS, read_size, write_size [, erase_size]). erase_size is
* included when it fits within FF_MAX_SS, making the sector erase-aligned
* for NOR-style devices and page-aligned for NAND-style devices (where
* the FTL/WL layer handles erase internally).
*
* The caller is responsible for constructing the BDL stack (e.g. partition BDL ->
* WL BDL) before calling this function. Read-only devices are detected
* automatically.
*
* @param base_path path where FATFS partition should be mounted (e.g. "/spiflash")
* @param bdl_handle BDL device handle providing the storage
* @param mount_config pointer to structure with extra parameters for mounting FATFS
*
* @return
* - ESP_OK on success
* - ESP_ERR_INVALID_ARG if any of the arguments is invalid
* - ESP_ERR_NO_MEM if memory can not be allocated or no free drives
* - ESP_FAIL if partition can not be mounted
*/
esp_err_t esp_vfs_fat_bdl_mount(const char *base_path,
esp_blockdev_handle_t bdl_handle,
const esp_vfs_fat_mount_config_t *mount_config);
/**
* @brief Unmount FAT filesystem and release resources acquired using esp_vfs_fat_bdl_mount
*
* @note This function does NOT release the BDL device handle — the caller owns
* the BDL stack lifecycle.
*
* @param base_path path where partition was registered (e.g. "/spiflash")
* @param bdl_handle BDL device handle used during mount
*
* @return
* - ESP_OK on success
* - ESP_ERR_INVALID_STATE if esp_vfs_fat_bdl_mount hasn't been called
*/
esp_err_t esp_vfs_fat_bdl_unmount(const char *base_path, esp_blockdev_handle_t bdl_handle);
/**
* @brief Get information for FATFS partition
*

View File

@@ -0,0 +1,204 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdlib.h>
#include <string.h>
#include "esp_check.h"
#include "esp_log.h"
#include "esp_vfs_fat.h"
#include "vfs_fat_internal.h"
#include "diskio_impl.h"
#include "diskio_bdl.h"
static const char *TAG = "vfs_fat_bdl";
static vfs_fat_bdl_ctx_t *s_bdl_ctx[FF_VOLUMES] = {};
extern esp_err_t esp_vfs_set_readonly_flag(const char *base_path);
static bool get_ctx_id_by_bdl(esp_blockdev_handle_t bdl, uint32_t *out_id)
{
for (int i = 0; i < FF_VOLUMES; i++) {
if (s_bdl_ctx[i] && s_bdl_ctx[i]->bdl_handle == bdl) {
*out_id = i;
return true;
}
}
return false;
}
static uint32_t get_unused_ctx_id(void)
{
for (uint32_t i = 0; i < FF_VOLUMES; i++) {
if (!s_bdl_ctx[i]) {
return i;
}
}
return FF_VOLUMES;
}
static esp_err_t try_mount_rw(FATFS *fs, const char *drv,
const esp_vfs_fat_mount_config_t *mount_config,
vfs_fat_x_ctx_flags_t *out_flags,
size_t sec_num, size_t sec_size)
{
FRESULT fresult = f_mount(fs, drv, 1);
if (fresult == FR_OK) {
if (out_flags) {
*out_flags &= ~FORMATTED_DURING_LAST_MOUNT;
}
return ESP_OK;
}
bool recoverable = (fresult == FR_NO_FILESYSTEM || fresult == FR_INT_ERR);
if (!recoverable || !mount_config->format_if_mount_failed) {
ESP_LOGE(TAG, "f_mount failed (%d)", fresult);
return ESP_FAIL;
}
ESP_LOGW(TAG, "f_mount failed (%d), formatting...", fresult);
const size_t workbuf_size = 4096;
void *workbuf = ff_memalloc(workbuf_size);
if (workbuf == NULL) {
return ESP_ERR_NO_MEM;
}
size_t alloc_unit_size = esp_vfs_fat_get_allocation_unit_size(
sec_size, mount_config->allocation_unit_size);
ESP_LOGI(TAG, "Formatting FATFS partition, allocation unit size=%d", alloc_unit_size);
UINT root_dir_entries = (sec_size == 512) ? 16 : 128;
const MKFS_PARM opt = {
(BYTE)(FM_ANY | FM_SFD),
(mount_config->use_one_fat ? 1 : 2),
0,
(sec_num <= 128 ? root_dir_entries : 0),
alloc_unit_size
};
fresult = f_mkfs(drv, &opt, workbuf, workbuf_size);
free(workbuf);
ESP_RETURN_ON_FALSE(fresult == FR_OK, ESP_FAIL, TAG, "f_mkfs failed (%d)", fresult);
if (out_flags) {
*out_flags |= FORMATTED_DURING_LAST_MOUNT;
}
ESP_LOGI(TAG, "Mounting again");
fresult = f_mount(fs, drv, 1);
ESP_RETURN_ON_FALSE(fresult == FR_OK, ESP_FAIL, TAG, "f_mount failed after formatting (%d)", fresult);
return ESP_OK;
}
esp_err_t esp_vfs_fat_bdl_mount(const char *base_path,
esp_blockdev_handle_t bdl_handle,
const esp_vfs_fat_mount_config_t *mount_config)
{
esp_err_t ret = ESP_OK;
vfs_fat_bdl_ctx_t *ctx = NULL;
ESP_RETURN_ON_FALSE(base_path, ESP_ERR_INVALID_ARG, TAG, "base_path is NULL");
ESP_RETURN_ON_FALSE(bdl_handle != ESP_BLOCKDEV_HANDLE_INVALID, ESP_ERR_INVALID_ARG, TAG, "invalid BDL handle");
ESP_RETURN_ON_FALSE(mount_config, ESP_ERR_INVALID_ARG, TAG, "mount_config is NULL");
BYTE pdrv = 0xFF;
if (ff_diskio_get_drive(&pdrv) != ESP_OK) {
ESP_LOGD(TAG, "the maximum count of volumes is already mounted");
return ESP_ERR_NO_MEM;
}
ESP_LOGD(TAG, "using pdrv=%i", pdrv);
char drv[3] = {(char)('0' + pdrv), ':', 0};
ESP_GOTO_ON_ERROR(ff_diskio_register_bdl(pdrv, bdl_handle), fail, TAG,
"ff_diskio_register_bdl failed pdrv=%i, error - 0x(%x)", pdrv, ret);
FATFS *fs;
esp_vfs_fat_conf_t conf = {
.base_path = base_path,
.fat_drive = drv,
.max_files = mount_config->max_files,
};
ret = esp_vfs_fat_register(&conf, &fs);
if (ret == ESP_ERR_INVALID_STATE) {
// already registered with VFS
} else if (ret != ESP_OK) {
ESP_LOGD(TAG, "esp_vfs_fat_register failed 0x(%x)", ret);
goto fail;
}
WORD sec_size_w;
if (disk_ioctl(pdrv, GET_SECTOR_SIZE, &sec_size_w) != RES_OK) {
ESP_LOGE(TAG, "failed to query sector size from diskio");
ret = ESP_FAIL;
goto fail;
}
size_t sec_size = (size_t)sec_size_w;
size_t sec_num = (size_t)(bdl_handle->geometry.disk_size / sec_size);
if (bdl_handle->device_flags.read_only) {
FRESULT fresult = f_mount(fs, drv, 1);
if (fresult != FR_OK) {
ESP_LOGW(TAG, "f_mount failed (%d)", fresult);
ret = ESP_FAIL;
goto fail;
}
} else {
vfs_fat_x_ctx_flags_t flags = 0;
ret = try_mount_rw(fs, drv, mount_config, &flags, sec_num, sec_size);
if (ret != ESP_OK) {
goto fail;
}
}
ctx = calloc(1, sizeof(vfs_fat_bdl_ctx_t));
ESP_GOTO_ON_FALSE(ctx, ESP_ERR_NO_MEM, fail, TAG, "no mem");
ctx->bdl_handle = bdl_handle;
ctx->pdrv = pdrv;
ctx->fs = fs;
memcpy(&ctx->mount_config, mount_config, sizeof(esp_vfs_fat_mount_config_t));
uint32_t ctx_id = get_unused_ctx_id();
assert(ctx_id != FF_VOLUMES);
s_bdl_ctx[ctx_id] = ctx;
if (bdl_handle->device_flags.read_only) {
esp_vfs_set_readonly_flag(base_path);
}
return ESP_OK;
fail:
f_mount(0, drv, 0);
esp_vfs_fat_unregister_path(base_path);
ff_diskio_unregister(pdrv);
free(ctx);
return ret;
}
esp_err_t esp_vfs_fat_bdl_unmount(const char *base_path, esp_blockdev_handle_t bdl_handle)
{
BYTE pdrv = ff_diskio_get_pdrv_bdl(bdl_handle);
ESP_RETURN_ON_FALSE(pdrv != 0xff, ESP_ERR_INVALID_STATE, TAG,
"BDL device isn't registered, call esp_vfs_fat_bdl_mount first");
uint32_t id = FF_VOLUMES;
ESP_RETURN_ON_FALSE(get_ctx_id_by_bdl(bdl_handle, &id), ESP_ERR_INVALID_STATE, TAG,
"BDL device isn't registered, call esp_vfs_fat_bdl_mount first");
assert(id != FF_VOLUMES);
char drv[3] = {(char)('0' + pdrv), ':', 0};
f_mount(0, drv, 0);
ff_diskio_unregister(pdrv);
ff_diskio_clear_pdrv_bdl(bdl_handle);
esp_err_t err = esp_vfs_fat_unregister_path(base_path);
free(s_bdl_ctx[id]);
s_bdl_ctx[id] = NULL;
return err;
}

View File

@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2018-2025 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2018-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
@@ -9,6 +9,7 @@
#include "esp_vfs_fat.h"
#include "diskio_impl.h"
#include "esp_partition.h"
#include "esp_blockdev.h"
#ifndef CONFIG_IDF_TARGET_LINUX
#include "sdmmc_cmd.h"
#endif
@@ -36,6 +37,13 @@ static inline size_t esp_vfs_fat_get_allocation_unit_size(
return alloc_unit_size;
}
typedef struct vfs_fat_bdl_ctx_t {
esp_blockdev_handle_t bdl_handle; //BDL device handle
BYTE pdrv; //Drive number that is mounted
FATFS *fs; //FAT structure pointer that is registered
esp_vfs_fat_mount_config_t mount_config; //Mount configuration
} vfs_fat_bdl_ctx_t;
#ifndef CONFIG_IDF_TARGET_LINUX
typedef struct vfs_fat_sd_ctx_t {
BYTE pdrv; //Drive number that is mounted

View File

@@ -87,6 +87,8 @@ Examples
- Demonstrates the capabilities of Python-based tooling for FATFS images available on host computers.
* - :example:`ext_flash_fatfs <storage/fatfs/ext_flash>`
- Demonstrates using FATFS over wear leveling on external flash.
* - :example:`bdl_wl <storage/fatfs/bdl_wl>`
- Demonstrates using FATFS over BDL wear-levelling stack on internal flash.
* - :example:`wear_leveling <storage/wear_levelling>`
- Demonstrates using FATFS over wear leveling on internal flash.

View File

@@ -87,6 +87,8 @@
- 演示了在主机上使用 Python 工具生成 FATFS 镜像的相关功能。
* - :example:`ext_flash_fatfs <storage/fatfs/ext_flash>`
- 演示了在外部 flash 上使用带有磨损均衡功能的 FATFS。
* - :example:`bdl_wl <storage/fatfs/bdl_wl>`
- 演示了在内部 flash 上通过 BDL 磨损均衡堆栈使用 FATFS。
* - :example:`wear_leveling <storage/wear_levelling>`
- 演示了在内部 flash 上使用带有磨损均衡功能的 FATFS。

View File

@@ -9,6 +9,17 @@ examples/storage/fatfs:
- if: IDF_TARGET != "esp32"
reason: only one target needed
examples/storage/fatfs/bdl_wl:
depends_components:
- *common_components
- esp_blockdev
- fatfs
- vfs
- wear_leveling
disable_test:
- if: IDF_TARGET != "esp32"
reason: only one target needed
examples/storage/fatfs/ext_flash:
depends_components:
- *common_components

View File

@@ -0,0 +1,7 @@
# 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)
idf_build_set_property(MINIMAL_BUILD ON)
project(fatfs_bdl_wl)

View File

@@ -0,0 +1,55 @@
| 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 |
| ----------------- | ----- | -------- | -------- | -------- | -------- | --------- | -------- | --------- | -------- | -------- | -------- | -------- | --------- |
# FatFS over BDL (Block Device Layer) - Wear-Levelling Stack
This example demonstrates mounting a FAT filesystem using the Block Device Layer (BDL) interface
instead of the legacy `wl_handle_t`-based API.
## BDL Stack
The BDL stack constructed in this example:
```
FatFS (VFS + POSIX API)
|
diskio_bdl (generic BDL diskio adapter)
|
WL BDL (wear-levelling, via wl_get_blockdev())
|
Partition BDL (flash partition, via esp_partition_get_blockdev())
|
SPI Flash (physical storage)
```
The key advantage of BDL is that **the same `diskio_bdl` adapter works with any BDL device**.
You can swap the bottom of the stack (e.g., use `sdmmc_get_blockdev()` for an SD card) without
changing the FatFS integration code.
## How to use example
### Build and flash
```
idf.py -p PORT flash monitor
```
(To exit the serial monitor, type `Ctrl-]`.)
## Example output
```
I (321) example: Creating partition BDL for 'storage' partition
I (331) example: Partition BDL: disk_size=1048576, erase_size=4096
I (331) example: Creating WL BDL on top of partition BDL
I (341) example: WL BDL: disk_size=...., erase_size=4096
I (341) example: Mounting FAT filesystem via BDL
I (741) example: Filesystem mounted
I (741) example: Opening file
I (841) example: File written
I (841) example: Reading file
I (841) example: Read from file: 'Hello from FatFS over BDL!'
I (841) example: Unmounting FAT filesystem
I (941) example: Releasing BDL devices
I (941) example: Done
```

View File

@@ -0,0 +1,3 @@
idf_component_register(SRCS "fatfs_bdl_wl_main.c"
PRIV_REQUIRES vfs fatfs esp_blockdev esp_partition wear_levelling
INCLUDE_DIRS ".")

View File

@@ -0,0 +1,133 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
/*
* FatFS over BDL (Block Device Layer) - Wear-Levelling stack example
*
* Demonstrates building a BDL stack and mounting FatFS on top of it:
*
* +-----------+
* | FatFS | <- file system (VFS + FatFS)
* +-----------+
* | diskio_bdl| <- FatFS diskio driver for BDL devices
* +-----------+
* | WL BDL | <- wear-levelling BDL layer (wl_get_blockdev)
* +-----------+
* | Part BDL | <- partition BDL layer (esp_partition_get_blockdev)
* +-----------+
* | SPI Flash | <- physical storage
* +-----------+
*
* The BDL approach decouples FatFS from any specific storage driver.
* The same diskio_bdl adapter works with any BDL-compatible bottom device:
* - partition BDL (flash partition)
* - sdmmc BDL (SD/eMMC card)
* - memory BDL (RAM disk for testing)
* - or any custom BDL implementation
*/
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include "esp_log.h"
#include "esp_vfs.h"
#include "esp_vfs_fat.h"
#include "esp_partition.h"
#include "esp_blockdev.h"
#include "wear_levelling.h"
static const char *TAG = "example";
const char *base_path = "/spiflash";
void app_main(void)
{
/* ------------------------------------------------------------------ */
/* Step 1: Build the BDL stack */
/* ------------------------------------------------------------------ */
ESP_LOGI(TAG, "Creating partition BDL for 'storage' partition");
esp_blockdev_handle_t part_bdl = NULL;
ESP_ERROR_CHECK(esp_partition_get_blockdev(
ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_FAT,
"storage", &part_bdl));
ESP_LOGI(TAG, " Partition BDL: disk_size=%llu, erase_size=%u",
(unsigned long long)part_bdl->geometry.disk_size,
(unsigned)part_bdl->geometry.erase_size);
ESP_LOGI(TAG, "Creating WL BDL on top of partition BDL");
esp_blockdev_handle_t wl_bdl = NULL;
ESP_ERROR_CHECK(wl_get_blockdev(part_bdl, &wl_bdl));
ESP_LOGI(TAG, " WL BDL: disk_size=%llu, erase_size=%u",
(unsigned long long)wl_bdl->geometry.disk_size,
(unsigned)wl_bdl->geometry.erase_size);
/* ------------------------------------------------------------------ */
/* Step 2: Mount FatFS on the BDL device */
/* ------------------------------------------------------------------ */
ESP_LOGI(TAG, "Mounting FAT filesystem via BDL");
const esp_vfs_fat_mount_config_t mount_config = {
.max_files = 4,
.format_if_mount_failed = true,
.allocation_unit_size = CONFIG_WL_SECTOR_SIZE,
.use_one_fat = false,
};
ESP_ERROR_CHECK(esp_vfs_fat_bdl_mount(base_path, wl_bdl, &mount_config));
ESP_LOGI(TAG, "Filesystem mounted");
/* ------------------------------------------------------------------ */
/* Step 3: Use POSIX file operations */
/* ------------------------------------------------------------------ */
const char *filename = "/spiflash/example.txt";
ESP_LOGI(TAG, "Opening file");
FILE *f = fopen(filename, "wb");
if (f == NULL) {
ESP_LOGE(TAG, "Failed to open file for writing");
return;
}
fprintf(f, "Hello from FatFS over BDL!\n");
fclose(f);
ESP_LOGI(TAG, "File written");
ESP_LOGI(TAG, "Reading file");
f = fopen(filename, "r");
if (f == NULL) {
ESP_LOGE(TAG, "Failed to open file for reading");
return;
}
char line[128];
fgets(line, sizeof(line), f);
fclose(f);
char *pos = strchr(line, '\n');
if (pos) {
*pos = '\0';
}
ESP_LOGI(TAG, "Read from file: '%s'", line);
/* ------------------------------------------------------------------ */
/* Step 4: Unmount and tear down the BDL stack */
/* ------------------------------------------------------------------ */
ESP_LOGI(TAG, "Unmounting FAT filesystem");
ESP_ERROR_CHECK(esp_vfs_fat_bdl_unmount(base_path, wl_bdl));
ESP_LOGI(TAG, "Releasing BDL devices");
wl_bdl->ops->release(wl_bdl);
part_bdl->ops->release(part_bdl);
ESP_LOGI(TAG, "Done");
}

View File

@@ -0,0 +1,6 @@
# 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,
storage, data, fat, , 1M,
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 storage, data, fat, , 1M,

View File

@@ -0,0 +1,18 @@
# 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
@pytest.mark.generic
@idf_parametrize('target', ['esp32'], indirect=['target'])
def test_examples_fatfs_bdl_wl(dut: Dut) -> None:
dut.expect('example: Mounting FAT filesystem via BDL', timeout=90)
dut.expect('example: Filesystem mounted', timeout=90)
dut.expect('example: Opening file', timeout=90)
dut.expect('example: File written', timeout=90)
dut.expect('example: Reading file', timeout=90)
dut.expect("example: Read from file: 'Hello from FatFS over BDL!'", timeout=90)
dut.expect('example: Unmounting FAT filesystem', timeout=90)
dut.expect('example: Done', timeout=90)

View File

@@ -0,0 +1,4 @@
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