简介
在嵌入式应用中,将文件(如配置文件、网页资源或固件数据)存储在 Flash 中是一种非常常见的需求。基于原始 SPIFFS 项目,ESP-IDF 中的 SPIFFS 组件为 SPI NOR Flash 提供了一个轻量级文件系统:它支持磨损均衡、一致性检查,并与 ESP-IDF 生态深度集成,使开发者可以直接使用熟悉的 C 语言和 POSIX 文件 API。
本文将介绍 SPIFFS 组件的基本概念、它与 VFS(虚拟文件系统)的协作方式,以及如何配合 SPIFFSgen 等工具使用,并提供一个可运行的示例供参考。
使用时,只需通过 VFS 挂载一个 SPIFFS 分区,即可通过 fopen、fprintf、fread 等标准函数进行文件读写。
关键特性
- 标准文件 I/O 接口 支持常见的 C 与 POSIX 文件操作接口,如 fopen、fread、fwrite、fprintf、stat、unlink、rename 等。挂载SPIFFS 后,可直接使用这些接口读写文件,无需额外学习专用 API。
- 基于分区管理 SPIFFS 依赖分区表进行配置。开发者在分区表中定义一个 SPIFFS 分区后,所有文件数据都存储于该分区中。
- 磨损均衡 (Wear Levelling) 通过在分区范围内分散写入操作,有效延长 Flash 的使用寿命。
- 文件系统一致性检查 可通过 esp_spiffs_check() 函数执行完整性检查及修复(详见后文“一致性检查”部分)。
- 挂载失败自动格式化 通过配置 format_if_mount_failed,在挂载失败时(例如首次启动或 Flash 被擦除后)自动格式化分区,简化开发流程。
- 需要注意的是,SPIFFS 采用扁平结构,并不支持真正的目录层级——路径中的“目录”实际上是文件名的一部分。通常情况下,SPIFFS 可稳定使用约 75% 的分区空间。在剩余空间较少时,垃圾回收过程可能会变得耗时,具体行为可参考 SPIFFS 官方文档。
应用场景
SPIFFS 常见的应用包括:
- Web 服务器资源:存储 ESP32 Web Server 所需的 HTML、CSS、JavaScript 文件;
- 配置与校准数据:保存设备配置或校准参数,并支持在不重新烧录整个应用的情况下更新;
- 脚本与运行时数据:如 Lua 脚本、JSON 配置文件或小型数据集;
- OTA 与资源管理:独立于主应用程序进行更新的固件元数据或资源包。
安全提示:SPIFFS 本身不支持数据加密。如需保护敏感数据,建议使用 ESP-IDF 提供的其他存储组件(如 FATFS、LittleFS、NVS),这些组件支持数据加密功能。更多对比可参考 ESP-IDF 文档中的文件系统注意事项页面。
基本示例:挂载、写入与读取
ESP-IDF 中的 storage/spiffs 示例展示了一个最基础的使用流程:注册 SPIFFS、写入文件、重命名文件,然后再读取文件内容。该示例无需任何特殊硬件,只要分区表中包含 SPIFFS 分区即可运行。
示例执行流程
- 在编译阶段定义包含 SPIFFS 分区(标签为 storage)的分区表;
- 调用 esp_vfs_spiffs_register() 初始化 SPIFFS,并通过 VFS 挂载到指定路径(如 /spiffs);
- 使用 fopen 创建文件,并通过 fprintf 写入内容;
- 对文件进行重命名(使用 stat 检查目标文件是否存在,必要时通过 unlink 删除);
- 打开重命名后的文件,使用 fgets 读取内容并输出到控制台;
- 最后调用 esp_vfs_spiffs_unregister() 注销 SPIFFS;
分区大小与标签定义在 partitions_example.csv 中。可通过 idf.py -p PORT flash monitor 运行示例。如需清空SPIFFS 分区,可在重新烧录前执行 idf.py erase-flash。
前提条件
项目需包含带有 SPIFFS 分区的分区表(参考示例中的 partitions_example.csv)
无需特殊硬件,支持所有 ESP-IDF 目标芯片(如 ESP32、ESP32-C3 等)。
步骤 1:编译时准备分区表
在代码中挂载 SPIFFS 之前,请确保构建配置使用包含 SPIFFS 分区的分区表,且该分区标签与代码中一致(此处为 storage)。
在 idf.py menuconfig 中:
- 进入 Partition Table
- 设置 Partition Table 为 Custom partition table CSV
- 设置 Custom partition CSV file 为你的文件(例如 partitions_example.csv)
示例中的 partitions_example.csv 如下:
# 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, spiffs, , 0xF0000,SPIFFS 关键行:
storage, data, spiffs, , 0xF0000,在构建时,ESP-IDF 会将该 CSV 编译为分区表二进制文件,并与应用程序一同烧录。
步骤 2:包含头文件并配置
引入 SPIFFS/VFS 头文件,并设置分区标签与挂载路径:
#include "esp_vfs_spiffs.h" static const char *TAG = "example"; #define PARTITION_LABEL "storage" #define BASE_PATH "/spiffs"步骤 3:通过 VFS 注册 SPIFFS
在 app_main()(或 NVS/其他初始化之后)中:
esp_vfs_spiffs_conf_t conf = { .base_path = BASE_PATH, .partition_label = PARTITION_LABEL, .max_files = 5, .format_if_mount_failed = true }; esp_err_t ret = esp_vfs_spiffs_register(&conf); if (ret != ESP_OK) { ESP_LOGE(TAG, "Failed to initialize SPIFFS (%s)", esp_err_to_name(ret)); return; }将 format_if_mount_failed 设为 true,可在首次使用或挂载失败时自动格式化分区,这在开发阶段非常方便。
步骤 4:使用标准文件 API
挂载完成后,可使用 C 标准库函数操作 BASE_PATH 下的文件:
FILE *f = fopen("/spiffs/hello.txt", "w"); if (f != NULL) { fprintf(f, "Hello World!\n"); fclose(f); }所有操作都会通过 VFS 转发至 SPIFFS 驱动,无需使用 SPIFFS 专用 API。
步骤 5:完成后注销
esp_vfs_spiffs_unregister(PARTITION_LABEL);
示例还展示了如何使用 esp_spiffs_info() 获取分区信息,以及(可选)使用 esp_spiffs_check() 进行完整性检查。
完整源码
完整示例位于:
esp-idf/examples/storage/spiffs
使用 SPIFFSgen 在构建过程中嵌入文件:
esp-idf/examples/storage/spiffsgen
讨论:示例中的选项与替代方案
上述示例仅使用了 SPIFFS 和 VFS 的一小部分配置选项。本节将逐一说明这些选择及组件提供的替代方案,帮助你根据项目需求进行灵活调整。
注册参数:base_path 与 partition_label
示例中通过 base_path = "/spiffs" 和 partition_label = "storage" 注册 SPIFFS。这两个参数分别回答两个不同的问题:文件数据存储在 Flash 的哪里,以及应用程序通过什么路径访问这些数据。
partition_label ——“使用哪一块 Flash 分区?”(基于 esp_partition)
分区表会将 Flash 划分为多个具名区域,每个区域都有一个标签(例如 storage 或 spiffs)。当你将 partition_label 设置为 "storage" 时,SPIFFS 组件会在底层调用 esp_partition API:根据该标签查找对应分区,并获取该区域在 Flash 中的偏移和大小。
随后,SPIFFS 会独占使用该分区来存储所有文件数据。因此,partition_label 实际上是连接“软件配置”与“物理Flash 布局”的关键桥梁。
如果你的系统中存在多个 SPIFFS 分区(例如 storage 和 assets),则需要分别使用不同的 label 注册,每个实例都会对应独立的 Flash 区域。
替代方式:
可以将 partition_label 设置为 NULL,此时组件会使用分区表中第一个 SPIFFS 分区(示例代码也支持这种方式;这里使用 "storage" 只是为了更清晰)。这种方式适用于只有一个 SPIFFS 分区、且不关心名称的场景。
base_path ——“代码中如何访问文件?”(基于 VFS)
base_path(例如 /spiffs)是 VFS 层中的挂载点。VFS 会根据路径前缀,将文件操作路由到对应的文件系统驱动。
例如,当你调用:
fopen("/spiffs/hello.txt", "r");
VFS 会将该请求转发给注册在 /spiffs 路径下的 SPIFFS 驱动。驱动只处理相对路径部分 (hello.txt),并使用此前通过 partition_label 已确定的分区进行实际读写。
因此,base_path 本质上是一种命名约定:它定义了该 SPIFFS 实例在代码中的“访问路径前缀”。 它不会影响数据在 Flash 中的存储位置。
替代方式:
可以将 base_path 设置为任意不与其他挂载点冲突的路径(例如 /data、/assets)。示例中使用 /spiffs 只是约定俗成。
两者如何协同工作
- partition_label:决定数据存储在哪个 Flash 分区(通过 esp_partition)
- base_path:决定通过哪个路径前缀访问这些数据(通过 VFS)
二者在注册时同时指定,VFS 才能正确路由请求,而驱动才能访问正确的存储区域。
format_if_mount_failed 与 max_files
示例中设置:
format_if_mount_failed = true max_files = 5format_if_mount_failed
当挂载失败(例如分区未格式化或已损坏)时,组件会自动格式化该分区并重新尝试挂载。这在开发阶段或首次启动时非常方便。 在生产环境中,可以将其设置为 false,由应用自行处理挂载失败(例如调用 esp_spiffs_format() 或 esp_spiffs_check() 后重试)。
max_files
表示允许同时打开的文件数量上限。示例中设置为 5;如果你的应用需要并发打开更多文件,应相应提高该值。
需要注意,esp_vfs_spiffs_conf_t 结构体仅包含这四个字段:base_path、partition_label、max_files、format_if_mount_failed,没有其他配置项。
一致性检查 (Integrity check)
在写操作过程中,如果发生断电,SPIFFS 文件系统可能会损坏。该组件不会在挂载时自动执行一致性检查,而是提供一个函数供开发者按需调用。
示例中的处理方式
storage/spiffs 示例在以下两种情况下会调用:
esp_spiffs_check(partition_label)1. 启动时可选检查
在示例的 menuconfig(“SPIFFS Example”菜单)中,有一个选项:
Perform SPIFFS consistency check on start(CONFIG_EXAMPLE_SPIFFS_CHECK_ON_START)
启用后,示例会在成功挂载后、执行任何文件操作之前,调用一次:
esp_spiffs_check(conf.partition_label)2. 信息异常时的恢复机制
当 esp_spiffs_info() 返回的 used > total 时,示例会调用 esp_spiffs_check() 作为恢复手段(该逻辑独立于menuconfig 配置)。
esp_spiffs_check(partition_label) 该函数必须在分区已挂载的前提下手动调用。其作用包括:
- 扫描文件系统
- 修复损坏的文件
- 清理未引用的数据页(例如断电导致的残留数据)
返回值可能为:
- ESP_OK
- ESP_ERR_INVALID_STATE(未挂载)
- ESP_FAIL
需要注意,该检查开销较大,需要多次完整扫描文件系统。在大容量分区上,可能会带来明显的延迟。
此外,与某些文件系统不同,esp_vfs_spiffs_conf_t 不支持“挂载时自动检查”选项,检查时机完全由开发者控制。
替代策略
在实际项目中,你可以根据需求选择不同策略:
- 仅在启动时执行检查(类似示例)
- 仅在检测到异常时执行(如 used > total 或检测到断电)
- 作为周期性维护任务定期执行
需要权衡:更高的数据恢复能力 vs 更长的启动阻塞时间(尤其是在大分区下)
关于何时执行检查的更多细节,可参考 SPIFFS FAQ。
分区预填充:SPIFFSgen
示例中,文件是在运行时创建的(例如通过 fprintf)。如果你希望在编译阶段将主机上的文件(HTML、配置文件、资源等)打包进 SPIFFS 分区,并随应用一起烧录,推荐使用工具 SPIFFSgen (spiffsgen.py)。
spiffsgen.py 是一个 write-only 的 Python 工具,用于从主机目录生成 SPIFFS 镜像。它属于 SPIFFS 组件的一部分,可独立使用,也可集成到构建系统中。
独立使用方式
python spiffsgen.py <image_size> <base_dir> <output_file>生成的镜像可以通过 esptool 或 parttool.py 烧录。 可运行 python spiffsgen.py --help 查看可选参数(如页大小、块大小等)。
构建系统集成
可通过 CMake 调用,使镜像在构建过程中自动生成(并可选自动烧录):
spiffs_create_partition_image(<partition> <base_dir> [FLASH_IN_PROJECT] [DEPENDS dep dep ...])说明:
- <partition> 使用分区表中的分区名称
- 镜像大小自动从分区表读取
- 启用 FLASH_IN_PROJECT 后,执行 idf.py flash 时会同时烧录 SPIFFS 镜像
具体流程可参考 storage/spiffsgen示例。
其他工具:mkspiffs 也可用于创建和解包 SPIFFS 镜像,适用于以下场景:
- 需要解包已有镜像
- 无法使用 Python 环境
需要注意:ESP-IDF 构建系统未内置 mkspiffs 集成支持。关于 SPIFFSgen 与 mkspiffs 的使用选择,可参考 SPIFFS官方文档。
总结
SPIFFS 组件为 SPI NOR Flash 提供了一个简洁、基于分区的文件系统方案,具备磨损均衡与一致性检查能力。通过与 VFS 集成,开发者可以使用标准 C / POSIX 文件接口完成所有文件操作。本文示例展示了基础使用流程,并对关键配置项与扩展方案进行了说明,方便你根据实际项目需求进行调整。