diff --git a/components/fatfs/CMakeLists.txt b/components/fatfs/CMakeLists.txt index 67423287af..a66cc41934 100644 --- a/components/fatfs/CMakeLists.txt +++ b/components/fatfs/CMakeLists.txt @@ -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") diff --git a/components/fatfs/diskio/diskio_bdl.c b/components/fatfs/diskio/diskio_bdl.c new file mode 100644 index 0000000000..810249dd89 --- /dev/null +++ b/components/fatfs/diskio/diskio_bdl.c @@ -0,0 +1,274 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#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}; + } + } +} diff --git a/components/fatfs/diskio/diskio_bdl.h b/components/fatfs/diskio/diskio_bdl.h new file mode 100644 index 0000000000..139a23b750 --- /dev/null +++ b/components/fatfs/diskio/diskio_bdl.h @@ -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 diff --git a/components/fatfs/host_test/.build-test-rules.yml b/components/fatfs/host_test/.build-test-rules.yml index 17714e9c87..b71d770d52 100644 --- a/components/fatfs/host_test/.build-test-rules.yml +++ b/components/fatfs/host_test/.build-test-rules.yml @@ -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" diff --git a/components/fatfs/host_test/bdl/CMakeLists.txt b/components/fatfs/host_test/bdl/CMakeLists.txt new file mode 100644 index 0000000000..3805cc2231 --- /dev/null +++ b/components/fatfs/host_test/bdl/CMakeLists.txt @@ -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) diff --git a/components/fatfs/host_test/bdl/main/CMakeLists.txt b/components/fatfs/host_test/bdl/main/CMakeLists.txt new file mode 100644 index 0000000000..0a37cf9c73 --- /dev/null +++ b/components/fatfs/host_test/bdl/main/CMakeLists.txt @@ -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) diff --git a/components/fatfs/host_test/bdl/main/idf_component.yml b/components/fatfs/host_test/bdl/main/idf_component.yml new file mode 100644 index 0000000000..f7982136b9 --- /dev/null +++ b/components/fatfs/host_test/bdl/main/idf_component.yml @@ -0,0 +1,2 @@ +dependencies: + espressif/catch2: "^3.4.0" diff --git a/components/fatfs/host_test/bdl/main/test_fatfs_bdl.cpp b/components/fatfs/host_test/bdl/main/test_fatfs_bdl.cpp new file mode 100644 index 0000000000..f4b5f72ed8 --- /dev/null +++ b/components/fatfs/host_test/bdl/main/test_fatfs_bdl.cpp @@ -0,0 +1,276 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include +#include +#include + +#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 + +/* ===================================================================== */ +/* 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); +} diff --git a/components/fatfs/host_test/bdl/partition_table.csv b/components/fatfs/host_test/bdl/partition_table.csv new file mode 100644 index 0000000000..c885c1e1e4 --- /dev/null +++ b/components/fatfs/host_test/bdl/partition_table.csv @@ -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, diff --git a/components/fatfs/host_test/bdl/pytest_fatfs_bdl_linux.py b/components/fatfs/host_test/bdl/pytest_fatfs_bdl_linux.py new file mode 100644 index 0000000000..d9a2d1a682 --- /dev/null +++ b/components/fatfs/host_test/bdl/pytest_fatfs_bdl_linux.py @@ -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) diff --git a/components/fatfs/host_test/bdl/sdkconfig.defaults b/components/fatfs/host_test/bdl/sdkconfig.defaults new file mode 100644 index 0000000000..53e7687b77 --- /dev/null +++ b/components/fatfs/host_test/bdl/sdkconfig.defaults @@ -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 diff --git a/components/fatfs/test_apps/.build-test-rules.yml b/components/fatfs/test_apps/.build-test-rules.yml index 55db11c8bf..634d58f677 100644 --- a/components/fatfs/test_apps/.build-test-rules.yml +++ b/components/fatfs/test_apps/.build-test-rules.yml @@ -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" diff --git a/components/fatfs/test_apps/README.md b/components/fatfs/test_apps/README.md index 78a9dcd9de..8de5e98787 100644 --- a/components/fatfs/test_apps/README.md +++ b/components/fatfs/test_apps/README.md @@ -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 diff --git a/components/fatfs/test_apps/bdl/CMakeLists.txt b/components/fatfs/test_apps/bdl/CMakeLists.txt new file mode 100644 index 0000000000..03fb0ffdc8 --- /dev/null +++ b/components/fatfs/test_apps/bdl/CMakeLists.txt @@ -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) diff --git a/components/fatfs/test_apps/bdl/main/CMakeLists.txt b/components/fatfs/test_apps/bdl/main/CMakeLists.txt new file mode 100644 index 0000000000..f2c4aac49d --- /dev/null +++ b/components/fatfs/test_apps/bdl/main/CMakeLists.txt @@ -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) diff --git a/components/fatfs/test_apps/bdl/main/test_fatfs_bdl.c b/components/fatfs/test_apps/bdl/main/test_fatfs_bdl.c new file mode 100644 index 0000000000..2cc86df6a1 --- /dev/null +++ b/components/fatfs/test_apps/bdl/main/test_fatfs_bdl.c @@ -0,0 +1,220 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include +#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); +} diff --git a/components/fatfs/test_apps/bdl/partitions.csv b/components/fatfs/test_apps/bdl/partitions.csv new file mode 100644 index 0000000000..d1dcbae61d --- /dev/null +++ b/components/fatfs/test_apps/bdl/partitions.csv @@ -0,0 +1,4 @@ +# Name, Type, SubType, Offset, Size, Flags +factory, app, factory, 0x10000, 768k, +storage, data, fat, , 528k, +storage2, data, fat, , 528k, diff --git a/components/fatfs/test_apps/bdl/pytest_fatfs_bdl.py b/components/fatfs/test_apps/bdl/pytest_fatfs_bdl.py new file mode 100644 index 0000000000..e7b57fdcda --- /dev/null +++ b/components/fatfs/test_apps/bdl/pytest_fatfs_bdl.py @@ -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) diff --git a/components/fatfs/test_apps/bdl/sdkconfig.defaults b/components/fatfs/test_apps/bdl/sdkconfig.defaults new file mode 100644 index 0000000000..c679bc7b4d --- /dev/null +++ b/components/fatfs/test_apps/bdl/sdkconfig.defaults @@ -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" diff --git a/components/fatfs/vfs/esp_vfs_fat.h b/components/fatfs/vfs/esp_vfs_fat.h index c305244f97..76df5ff2be 100644 --- a/components/fatfs/vfs/esp_vfs_fat.h +++ b/components/fatfs/vfs/esp_vfs_fat.h @@ -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 * diff --git a/components/fatfs/vfs/vfs_fat_bdl.c b/components/fatfs/vfs/vfs_fat_bdl.c new file mode 100644 index 0000000000..e3adb837bf --- /dev/null +++ b/components/fatfs/vfs/vfs_fat_bdl.c @@ -0,0 +1,204 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#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; +} diff --git a/components/fatfs/vfs/vfs_fat_internal.h b/components/fatfs/vfs/vfs_fat_internal.h index e2f36a05f7..03c36a3e4f 100644 --- a/components/fatfs/vfs/vfs_fat_internal.h +++ b/components/fatfs/vfs/vfs_fat_internal.h @@ -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 diff --git a/docs/en/api-reference/storage/index.rst b/docs/en/api-reference/storage/index.rst index 24b1b8a2ce..655eca4dda 100644 --- a/docs/en/api-reference/storage/index.rst +++ b/docs/en/api-reference/storage/index.rst @@ -87,6 +87,8 @@ Examples - Demonstrates the capabilities of Python-based tooling for FATFS images available on host computers. * - :example:`ext_flash_fatfs ` - Demonstrates using FATFS over wear leveling on external flash. + * - :example:`bdl_wl ` + - Demonstrates using FATFS over BDL wear-levelling stack on internal flash. * - :example:`wear_leveling ` - Demonstrates using FATFS over wear leveling on internal flash. diff --git a/docs/zh_CN/api-reference/storage/index.rst b/docs/zh_CN/api-reference/storage/index.rst index f36ac271c6..74ffd9a778 100644 --- a/docs/zh_CN/api-reference/storage/index.rst +++ b/docs/zh_CN/api-reference/storage/index.rst @@ -87,6 +87,8 @@ - 演示了在主机上使用 Python 工具生成 FATFS 镜像的相关功能。 * - :example:`ext_flash_fatfs ` - 演示了在外部 flash 上使用带有磨损均衡功能的 FATFS。 + * - :example:`bdl_wl ` + - 演示了在内部 flash 上通过 BDL 磨损均衡堆栈使用 FATFS。 * - :example:`wear_leveling ` - 演示了在内部 flash 上使用带有磨损均衡功能的 FATFS。 diff --git a/examples/storage/fatfs/.build-test-rules.yml b/examples/storage/fatfs/.build-test-rules.yml index 152eee5fae..96e3a53dde 100644 --- a/examples/storage/fatfs/.build-test-rules.yml +++ b/examples/storage/fatfs/.build-test-rules.yml @@ -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 diff --git a/examples/storage/fatfs/bdl_wl/CMakeLists.txt b/examples/storage/fatfs/bdl_wl/CMakeLists.txt new file mode 100644 index 0000000000..382246a706 --- /dev/null +++ b/examples/storage/fatfs/bdl_wl/CMakeLists.txt @@ -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) diff --git a/examples/storage/fatfs/bdl_wl/README.md b/examples/storage/fatfs/bdl_wl/README.md new file mode 100644 index 0000000000..61fc05355b --- /dev/null +++ b/examples/storage/fatfs/bdl_wl/README.md @@ -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 +``` diff --git a/examples/storage/fatfs/bdl_wl/main/CMakeLists.txt b/examples/storage/fatfs/bdl_wl/main/CMakeLists.txt new file mode 100644 index 0000000000..18382c4cc5 --- /dev/null +++ b/examples/storage/fatfs/bdl_wl/main/CMakeLists.txt @@ -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 ".") diff --git a/examples/storage/fatfs/bdl_wl/main/fatfs_bdl_wl_main.c b/examples/storage/fatfs/bdl_wl/main/fatfs_bdl_wl_main.c new file mode 100644 index 0000000000..31178eece4 --- /dev/null +++ b/examples/storage/fatfs/bdl_wl/main/fatfs_bdl_wl_main.c @@ -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 +#include +#include +#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"); +} diff --git a/examples/storage/fatfs/bdl_wl/partitions_example.csv b/examples/storage/fatfs/bdl_wl/partitions_example.csv new file mode 100644 index 0000000000..1c79321a10 --- /dev/null +++ b/examples/storage/fatfs/bdl_wl/partitions_example.csv @@ -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, diff --git a/examples/storage/fatfs/bdl_wl/pytest_fatfs_bdl_wl_example.py b/examples/storage/fatfs/bdl_wl/pytest_fatfs_bdl_wl_example.py new file mode 100644 index 0000000000..f560dee28e --- /dev/null +++ b/examples/storage/fatfs/bdl_wl/pytest_fatfs_bdl_wl_example.py @@ -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) diff --git a/examples/storage/fatfs/bdl_wl/sdkconfig.defaults b/examples/storage/fatfs/bdl_wl/sdkconfig.defaults new file mode 100644 index 0000000000..47363c32d5 --- /dev/null +++ b/examples/storage/fatfs/bdl_wl/sdkconfig.defaults @@ -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