diff --git a/components/fatfs/CMakeLists.txt b/components/fatfs/CMakeLists.txt index a66cc419347..3bfcfbc771f 100644 --- a/components/fatfs/CMakeLists.txt +++ b/components/fatfs/CMakeLists.txt @@ -15,6 +15,7 @@ set(requires "wear_levelling" "esp_blockdev") if(${target} STREQUAL "linux") list(APPEND srcs "port/linux/ffsystem.c" "vfs/vfs_fat.c" + "vfs/vfs_fat_spiflash.c" "vfs/vfs_fat_bdl.c") list(APPEND include_dirs "vfs") list(APPEND priv_requires "vfs" "linux") diff --git a/components/fatfs/test_apps/.build-test-rules.yml b/components/fatfs/test_apps/.build-test-rules.yml index 634d58f6777..00b29e79557 100644 --- a/components/fatfs/test_apps/.build-test-rules.yml +++ b/components/fatfs/test_apps/.build-test-rules.yml @@ -38,6 +38,16 @@ components/fatfs/test_apps/flash_wl: - vfs - wear_leveling +components/fatfs/test_apps/flash_wl_readonly: + disable_test: + - if: IDF_TARGET not in ["esp32", "esp32c3"] + reason: only one target per arch needed + depends_components: + - esp_partition + - fatfs + - vfs + - wear_leveling + components/fatfs/test_apps/sdcard: disable: - if: SOC_GPSPI_SUPPORTED != 1 diff --git a/components/fatfs/test_apps/flash_wl_readonly/CMakeLists.txt b/components/fatfs/test_apps/flash_wl_readonly/CMakeLists.txt new file mode 100644 index 00000000000..5c2d50ccef1 --- /dev/null +++ b/components/fatfs/test_apps/flash_wl_readonly/CMakeLists.txt @@ -0,0 +1,8 @@ +cmake_minimum_required(VERSION 3.22) + +set(COMPONENTS main) +set(EXTRA_COMPONENT_DIRS "${CMAKE_CURRENT_LIST_DIR}/../test_fatfs_common") + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) + +project(test_fatfs_flash_wl_readonly) diff --git a/components/fatfs/test_apps/flash_wl_readonly/main/CMakeLists.txt b/components/fatfs/test_apps/flash_wl_readonly/main/CMakeLists.txt new file mode 100644 index 00000000000..a2e2cb8e16d --- /dev/null +++ b/components/fatfs/test_apps/flash_wl_readonly/main/CMakeLists.txt @@ -0,0 +1,4 @@ +idf_component_register(SRCS "test_fatfs_flash_wl_readonly.c" + INCLUDE_DIRS "." + PRIV_REQUIRES unity spi_flash fatfs vfs + WHOLE_ARCHIVE) diff --git a/components/fatfs/test_apps/flash_wl_readonly/main/test_fatfs_flash_wl_readonly.c b/components/fatfs/test_apps/flash_wl_readonly/main/test_fatfs_flash_wl_readonly.c new file mode 100644 index 00000000000..7462f824f17 --- /dev/null +++ b/components/fatfs/test_apps/flash_wl_readonly/main/test_fatfs_flash_wl_readonly.c @@ -0,0 +1,149 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include +#include +#include "unity.h" +#include "esp_vfs.h" +#include "esp_vfs_fat.h" +#include "wear_levelling.h" + +static const char *BASE_PATH = "/spiflash"; +static wl_handle_t s_wl_handle = WL_INVALID_HANDLE; + +static void prepare_filesystem(void) +{ + esp_vfs_fat_mount_config_t cfg = { + .format_if_mount_failed = true, + .max_files = 5, + }; + TEST_ESP_OK(esp_vfs_fat_spiflash_mount_rw_wl(BASE_PATH, NULL, &cfg, &s_wl_handle)); + + FILE *f = fopen("/spiflash/hello.txt", "w"); + TEST_ASSERT_NOT_NULL(f); + fprintf(f, "hello"); + fclose(f); + + TEST_ASSERT_EQUAL(0, mkdir("/spiflash/testdir", 0755)); + + TEST_ESP_OK(esp_vfs_fat_spiflash_unmount_rw_wl(BASE_PATH, s_wl_handle)); + s_wl_handle = WL_INVALID_HANDLE; +} + +static void mount_readonly(void) +{ + esp_vfs_fat_mount_config_t cfg = { + .format_if_mount_failed = false, + .max_files = 5, + .read_only = true, + }; + TEST_ESP_OK(esp_vfs_fat_spiflash_mount_rw_wl(BASE_PATH, NULL, &cfg, &s_wl_handle)); +} + +static void unmount(void) +{ + TEST_ESP_OK(esp_vfs_fat_spiflash_unmount_rw_wl(BASE_PATH, s_wl_handle)); + s_wl_handle = WL_INVALID_HANDLE; +} + +void app_main(void) +{ + prepare_filesystem(); + unity_run_menu(); +} + +TEST_CASE("read_only mount: can read existing file", "[fatfs][readonly]") +{ + mount_readonly(); + + FILE *f = fopen("/spiflash/hello.txt", "r"); + TEST_ASSERT_NOT_NULL(f); + char buf[16] = {}; + TEST_ASSERT_EQUAL(5, fread(buf, 1, sizeof(buf), f)); + TEST_ASSERT_EQUAL_STRING("hello", buf); + fclose(f); + + unmount(); +} + +TEST_CASE("read_only mount: open for write fails with EROFS", "[fatfs][readonly]") +{ + mount_readonly(); + + int fd = open("/spiflash/new.txt", O_CREAT | O_RDWR, 0666); + TEST_ASSERT_EQUAL(-1, fd); + TEST_ASSERT_EQUAL(EROFS, errno); + + fd = open("/spiflash/hello.txt", O_WRONLY); + TEST_ASSERT_EQUAL(-1, fd); + TEST_ASSERT_EQUAL(EROFS, errno); + + unmount(); +} + +TEST_CASE("read_only mount: mkdir fails with EROFS", "[fatfs][readonly]") +{ + mount_readonly(); + + TEST_ASSERT_EQUAL(-1, mkdir("/spiflash/newdir", 0755)); + TEST_ASSERT_EQUAL(EROFS, errno); + + unmount(); +} + +TEST_CASE("read_only mount: unlink fails with EROFS", "[fatfs][readonly]") +{ + mount_readonly(); + + TEST_ASSERT_EQUAL(-1, unlink("/spiflash/hello.txt")); + TEST_ASSERT_EQUAL(EROFS, errno); + + unmount(); +} + +TEST_CASE("read_only mount: rename fails with EROFS", "[fatfs][readonly]") +{ + mount_readonly(); + + TEST_ASSERT_EQUAL(-1, rename("/spiflash/hello.txt", "/spiflash/bye.txt")); + TEST_ASSERT_EQUAL(EROFS, errno); + + unmount(); +} + +TEST_CASE("read_only mount: stat and opendir still work", "[fatfs][readonly]") +{ + mount_readonly(); + + struct stat st; + TEST_ASSERT_EQUAL(0, stat("/spiflash/hello.txt", &st)); + TEST_ASSERT(st.st_mode & S_IFREG); + + TEST_ASSERT_EQUAL(0, stat("/spiflash/testdir", &st)); + TEST_ASSERT(st.st_mode & S_IFDIR); + + DIR *dir = opendir("/spiflash"); + TEST_ASSERT_NOT_NULL(dir); + TEST_ASSERT_EQUAL(0, closedir(dir)); + + unmount(); +} + +TEST_CASE("read_only mount: format_if_mount_failed and read_only are mutually exclusive", "[fatfs][readonly]") +{ + esp_vfs_fat_mount_config_t cfg = { + .format_if_mount_failed = true, + .max_files = 5, + .read_only = true, + }; + wl_handle_t wl_handle = WL_INVALID_HANDLE; + TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, + esp_vfs_fat_spiflash_mount_rw_wl(BASE_PATH, NULL, &cfg, &wl_handle)); +} diff --git a/components/fatfs/test_apps/flash_wl_readonly/partitions.csv b/components/fatfs/test_apps/flash_wl_readonly/partitions.csv new file mode 100644 index 00000000000..a929971141e --- /dev/null +++ b/components/fatfs/test_apps/flash_wl_readonly/partitions.csv @@ -0,0 +1,3 @@ +# Name, Type, SubType, Offset, Size, Flags +factory, app, factory, 0x10000, 1M, +storage, data, fat, , 528k, diff --git a/components/fatfs/test_apps/flash_wl_readonly/pytest_fatfs_flash_wl_readonly.py b/components/fatfs/test_apps/flash_wl_readonly/pytest_fatfs_flash_wl_readonly.py new file mode 100644 index 00000000000..8ca2a44bae3 --- /dev/null +++ b/components/fatfs/test_apps/flash_wl_readonly/pytest_fatfs_flash_wl_readonly.py @@ -0,0 +1,11 @@ +# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: 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', 'esp32c3'], indirect=['target']) +def test_fatfs_flash_wl_readonly(dut: Dut) -> None: + dut.run_all_single_board_cases() diff --git a/components/fatfs/test_apps/flash_wl_readonly/sdkconfig.defaults b/components/fatfs/test_apps/flash_wl_readonly/sdkconfig.defaults new file mode 100644 index 00000000000..c679bc7b4d3 --- /dev/null +++ b/components/fatfs/test_apps/flash_wl_readonly/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 c65196016d3..f1694cee007 100644 --- a/components/fatfs/vfs/esp_vfs_fat.h +++ b/components/fatfs/vfs/esp_vfs_fat.h @@ -136,6 +136,14 @@ typedef struct { * may be different. */ bool use_one_fat; + /** + * Mount the filesystem in read-only mode. + * When set to true, all write operations (open for write, mkdir, unlink, + * rename, etc.) will fail with errno set to EROFS. + * This flag is independent of the underlying storage — it can be used + * with wear-levelled, raw, BDL, or SD card partitions. + */ + bool read_only; } esp_vfs_fat_mount_config_t; #define VFS_FAT_MOUNT_DEFAULT_CONFIG() \ @@ -145,6 +153,7 @@ typedef struct { .allocation_unit_size = 0, \ .disk_status_check_enable = false, \ .use_one_fat = false, \ + .read_only = false, \ } // Compatibility definition diff --git a/components/fatfs/vfs/vfs_fat_bdl.c b/components/fatfs/vfs/vfs_fat_bdl.c index ec60d671dbc..ae7c63db572 100644 --- a/components/fatfs/vfs/vfs_fat_bdl.c +++ b/components/fatfs/vfs/vfs_fat_bdl.c @@ -8,6 +8,7 @@ #include #include "esp_check.h" #include "esp_log.h" +#include "esp_vfs.h" #include "esp_vfs_fat.h" #include "vfs_fat_internal.h" #include "diskio_impl.h" @@ -17,8 +18,6 @@ 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++) { @@ -104,6 +103,8 @@ esp_err_t esp_vfs_fat_bdl_mount(const char *base_path, 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"); + ESP_RETURN_ON_FALSE(!(mount_config->read_only && mount_config->format_if_mount_failed), + ESP_ERR_INVALID_ARG, TAG, "read_only and format_if_mount_failed are mutually exclusive"); BYTE pdrv = 0xFF; if (ff_diskio_get_drive(&pdrv) != ESP_OK) { @@ -165,7 +166,7 @@ esp_err_t esp_vfs_fat_bdl_mount(const char *base_path, assert(ctx_id != FF_VOLUMES); s_bdl_ctx[ctx_id] = ctx; - if (bdl_handle->device_flags.read_only) { + if (bdl_handle->device_flags.read_only || mount_config->read_only) { esp_vfs_set_readonly_flag(base_path); } diff --git a/components/fatfs/vfs/vfs_fat_sdmmc.c b/components/fatfs/vfs/vfs_fat_sdmmc.c index 51b901a3390..ec1c4745cb8 100644 --- a/components/fatfs/vfs/vfs_fat_sdmmc.c +++ b/components/fatfs/vfs/vfs_fat_sdmmc.c @@ -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 */ @@ -415,6 +415,8 @@ esp_err_t esp_vfs_fat_mount_initialized(sdmmc_card_t* card, if (card == NULL || base_path == NULL || mount_config == NULL) { return ESP_ERR_INVALID_ARG; } + ESP_RETURN_ON_FALSE(!(mount_config->read_only && mount_config->format_if_mount_failed), + ESP_ERR_INVALID_ARG, TAG, "read_only and format_if_mount_failed are mutually exclusive"); esp_err_t err; @@ -436,6 +438,10 @@ esp_err_t esp_vfs_fat_mount_initialized(sdmmc_card_t* card, err = esp_vfs_fat_save_ctx(ldrv, mount_config, card, dup_path, fs, flags); CHECK_EXECUTE_RESULT(err, "esp_vfs_fat_save_ctx failed"); + if (mount_config->read_only) { + esp_vfs_set_readonly_flag(base_path); + } + return ESP_OK; cleanup: free(dup_path); diff --git a/components/fatfs/vfs/vfs_fat_spiflash.c b/components/fatfs/vfs/vfs_fat_spiflash.c index 8c8cf099af5..59b4c403f19 100644 --- a/components/fatfs/vfs/vfs_fat_spiflash.c +++ b/components/fatfs/vfs/vfs_fat_spiflash.c @@ -8,6 +8,7 @@ #include #include "esp_check.h" #include "esp_log.h" +#include "esp_vfs.h" #include "esp_vfs_fat.h" #include "vfs_fat_internal.h" #include "diskio_impl.h" @@ -23,8 +24,6 @@ static const char* TAG = "vfs_fat_spiflash"; static vfs_fat_spiflash_ctx_t *s_ctx[FF_VOLUMES] = {}; -extern esp_err_t esp_vfs_set_readonly_flag(const char* base_path); // from vfs/vfs.c to set readonly flag externally - static bool s_get_context_id_by_label(const char *label, uint32_t *out_id) { vfs_fat_spiflash_ctx_t *p_ctx = NULL; @@ -149,6 +148,9 @@ esp_err_t esp_vfs_fat_spiflash_mount_rw_wl(const char* base_path, const esp_vfs_fat_mount_config_t* mount_config, wl_handle_t* wl_handle) { + ESP_RETURN_ON_FALSE(!(mount_config->read_only && mount_config->format_if_mount_failed), + ESP_ERR_INVALID_ARG, TAG, "read_only and format_if_mount_failed are mutually exclusive"); + esp_err_t ret = ESP_OK; vfs_fat_spiflash_ctx_t *ctx = NULL; uint32_t ctx_id = FF_VOLUMES; @@ -208,7 +210,7 @@ esp_err_t esp_vfs_fat_spiflash_mount_rw_wl(const char* base_path, assert(ctx_id != FF_VOLUMES); s_ctx[ctx_id] = ctx; - if (data_partition->readonly) { + if (data_partition->readonly || mount_config->read_only) { esp_vfs_set_readonly_flag(base_path); } @@ -350,6 +352,9 @@ esp_err_t esp_vfs_fat_spiflash_mount_ro(const char* base_path, const char* partition_label, const esp_vfs_fat_mount_config_t* mount_config) { + ESP_RETURN_ON_FALSE(!(mount_config->read_only && mount_config->format_if_mount_failed), + ESP_ERR_INVALID_ARG, TAG, "read_only and format_if_mount_failed are mutually exclusive"); + esp_err_t ret = ESP_OK; const esp_partition_t *data_partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, @@ -388,7 +393,7 @@ esp_err_t esp_vfs_fat_spiflash_mount_ro(const char* base_path, goto fail; } - if (data_partition->readonly) { + if (data_partition->readonly || mount_config->read_only) { esp_vfs_set_readonly_flag(base_path); } diff --git a/components/vfs/include/esp_vfs.h b/components/vfs/include/esp_vfs.h index c9b3a87c11a..69ab3935d4d 100644 --- a/components/vfs/include/esp_vfs.h +++ b/components/vfs/include/esp_vfs.h @@ -512,6 +512,19 @@ void esp_vfs_dump_fds(FILE *fp); */ void esp_vfs_dump_registered_paths(FILE *fp); +/** + * @brief Set the read-only flag for a registered VFS entry + * + * After this call, all write operations (open for write, mkdir, unlink, + * rename, etc.) on the given path will fail with errno set to EROFS. + * + * @param base_path Path prefix of the already-registered VFS entry + * @return + * - ESP_OK on success + * - ESP_ERR_INVALID_STATE if no VFS entry matches base_path + */ +esp_err_t esp_vfs_set_readonly_flag(const char *base_path); + #if !defined(__DOXYGEN__) #pragma pop_macro("deprecated") #endif diff --git a/docs/en/api-reference/storage/fatfs.rst b/docs/en/api-reference/storage/fatfs.rst index a6fa1f27394..ca0b4114315 100644 --- a/docs/en/api-reference/storage/fatfs.rst +++ b/docs/en/api-reference/storage/fatfs.rst @@ -34,6 +34,46 @@ The general mounting workflow is: #. Use standard file APIs (``stdio.h`` or POSIX) on paths under the mount path. #. Close open files and call the matching unmount helper. +.. _fatfs-read-only-mount: + +Read-Only Mount +--------------- + +Any FAT filesystem can be mounted in read-only mode by setting ``esp_vfs_fat_mount_config_t.read_only`` to ``true``. When this flag is set, the VFS layer rejects all write operations (``open`` for write, ``mkdir``, ``unlink``, ``rename``, ``truncate``, etc.) with ``errno`` set to ``EROFS``, regardless of the underlying storage type. + +This is useful for protecting filesystem integrity when write access is not needed — for example, when reading configuration or asset data from a wear-levelled SPI flash partition: + +.. code-block:: c + + esp_vfs_fat_mount_config_t mount_config = { + .format_if_mount_failed = false, + .max_files = 5, + .read_only = true, + }; + wl_handle_t wl_handle; + esp_err_t err = esp_vfs_fat_spiflash_mount_rw_wl("/data", "storage", + &mount_config, &wl_handle); + + // Reading works normally + FILE *f = fopen("/data/config.json", "r"); // OK + + // All write operations are rejected at the VFS level + FILE *w = fopen("/data/new.txt", "w"); // Returns NULL, errno == EROFS + mkdir("/data/subdir", 0755); // Returns -1, errno == EROFS + +The ``read_only`` flag works with all mount helpers: + +- :cpp:func:`esp_vfs_fat_spiflash_mount_rw_wl` — wear-levelled SPI flash (read-only with WL metadata preserved). +- :cpp:func:`esp_vfs_fat_spiflash_mount_ro` — raw read-only SPI flash. +- :cpp:func:`esp_vfs_fat_sdmmc_mount` / :cpp:func:`esp_vfs_fat_sdspi_mount` — SD cards. +- :cpp:func:`esp_vfs_fat_bdl_mount` — Block Device Layer. + +.. note:: + + The ``read_only`` flag is enforced by the VFS layer, not the FatFs library itself. It prevents POSIX/stdio write operations from reaching the filesystem, but does not modify the on-disk filesystem state or metadata. The underlying storage driver (e.g., wear leveling) remains initialized normally. + + Mounting with ``read_only = true`` is distinct from :cpp:func:`esp_vfs_fat_spiflash_mount_ro`, which skips wear leveling entirely and accesses the raw flash partition directly. Use ``read_only = true`` with :cpp:func:`esp_vfs_fat_spiflash_mount_rw_wl` when you need the wear-leveling layer active (e.g., to ensure correct interpretation of a WL-formatted partition) but do not want the application to modify the filesystem. + .. _fatfs-configuration-options: Configuration Options diff --git a/docs/zh_CN/api-reference/storage/fatfs.rst b/docs/zh_CN/api-reference/storage/fatfs.rst index 72b762ebb42..dc38ad143c0 100644 --- a/docs/zh_CN/api-reference/storage/fatfs.rst +++ b/docs/zh_CN/api-reference/storage/fatfs.rst @@ -34,6 +34,46 @@ FatFs 组件提供了便利的封装函数,用于通过 VFS 层挂载文件系 #. 在挂载路径下的路径上使用标准文件 API(``stdio.h`` 或 POSIX)。 #. 关闭打开的文件,并调用相应的卸载辅助函数。 +.. _fatfs-read-only-mount: + +只读挂载 +-------- + +通过将 ``esp_vfs_fat_mount_config_t.read_only`` 设置为 ``true``,可以将任何 FAT 文件系统以只读模式挂载。设置此标志后,VFS 层将拒绝所有写操作,例如 ``open`` (写模式)、``mkdir``、``unlink``、``rename``、``truncate`` 等,并将 ``errno`` 设置为 ``EROFS``,无论底层存储类型如何。 + +当不需要写入访问时,此功能可用于保护文件系统完整性。例如,从带有磨损均衡功能的 SPI flash 分区读取配置或资源数据: + +.. code-block:: c + + esp_vfs_fat_mount_config_t mount_config = { + .format_if_mount_failed = false, + .max_files = 5, + .read_only = true, + }; + wl_handle_t wl_handle; + esp_err_t err = esp_vfs_fat_spiflash_mount_rw_wl("/data", "storage", + &mount_config, &wl_handle); + + // 读取操作正常 + FILE *f = fopen("/data/config.json", "r"); // 正常 + + // 所有写操作在 VFS 层被拒绝 + FILE *w = fopen("/data/new.txt", "w"); // 返回 NULL,errno == EROFS + mkdir("/data/subdir", 0755); // 返回 -1,errno == EROFS + +``read_only`` 标志适用于所有挂载辅助函数: + +- :cpp:func:`esp_vfs_fat_spiflash_mount_rw_wl` — 带有磨损均衡功能的 SPI flash(只读但保留 WL 元数据)。 +- :cpp:func:`esp_vfs_fat_spiflash_mount_ro` — 原始只读 SPI flash。 +- :cpp:func:`esp_vfs_fat_sdmmc_mount` / :cpp:func:`esp_vfs_fat_sdspi_mount` — SD 卡。 +- :cpp:func:`esp_vfs_fat_bdl_mount` — 块设备层。 + +.. note:: + + ``read_only`` 标志由 VFS 层强制执行,而非 FatFs 库本身。它阻止 POSIX/stdio 写操作到达文件系统,但不会修改磁盘上的文件系统状态或元数据。底层存储驱动(如磨损均衡)仍正常初始化。 + + 上述通过 ``read_only`` 标志实现的只读挂载方式与 :cpp:func:`esp_vfs_fat_spiflash_mount_ro` 不同,后者完全跳过磨损均衡并直接访问原始 flash 分区。当需要磨损均衡层处于活动状态(例如确保正确解释 WL 格式的分区)但不希望应用程序修改文件系统时,请在调用 :cpp:func:`esp_vfs_fat_spiflash_mount_rw_wl` 时设置 ``read_only = true``。 + .. _fatfs-configuration-options: 配置选项