第三十八章 ESP32S3 SPIFFS 实验
上一章实验中已经成功驱动 SD 卡,并可对 SD 卡进行读写操作,但读写 SD 卡时都是直接读出或写入二进制数据,这样使用起来显得十分不方便,因此本章将介绍 SPIFFS, SPIFFS是一个用于 SPI NOR flash 设备的嵌入式文件系统,支持磨损均衡以及文件系统一致性检查等功能。通过本章的学习,将学习到 SPIFFS 的基本使用。
本章分为如下几个小节:
38.1 SPIFFS 简介
38.2 硬件设计
38.3 程序设计
38.4 下载验证
38.1 SPIFFS 介绍
SPIFFS 是一个专为嵌入式系统和物联网(IoT)设备设计的开源文件系统。它的名字是 SPI Flash File System 的缩写,顾名思义,它主要用于在通过 SPI 接口连接的 NOR Flash 存储器上存储文件。SPIFFS 的设计遵循了嵌入式环境的特定约束,其核心特点包括:
特性 | 描述 |
---|---|
轻量级 | 代码量极小(通常只有几千字节),对 RAM 和 CPU 的资源占用极低,非常适合资源受限的 MCU。 |
掉电安全 | 在设计上考虑了意外断电的情况,通过机制确保文件系统结构不会轻易损坏。 |
动态磨损均衡 | 自动将写操作均匀分布到整个 Flash 存储区,避免某些扇区过早磨损而失效,显著延长 Flash 寿命。 |
基于页管理 | 其内部存储管理基于 Flash 的物理页(Page)和扇区(Sector)结构,效率更高。 |
支持随机访问 | 可以随机读取和写入文件中的任何位置。 |
表38.1.1 SPIFFS核心特点
工作原理与结构:
SPIFFS 将 Flash 存储器视为一个线性的、可寻址的空间,并将其逻辑上组织为以下结构:
- 页 (Page):
这是最基本的读写单元,大小通常与 Flash 芯片的物理页大小对齐(例如 256 字节)。每个页包含数据区和元数据头。元数据头记录了该页的状态(有效、无效、已删除)以及它属于哪个文件。
- 扇区 (Sector):
由多个页组成,是擦除(Erase)操作的基本单元。Flash 存储器只能将位从 1 改为 0(写入),而将 0 改回 1 必须通过扇区擦除。擦除操作非常耗时。SPIFFS 的核心工作之一就是通过垃圾回收(Garbage Collection) 机制,将包含无效数据的扇区进行擦除,使其变为可用状态。
- 文件索引:
SPIFFS 没有传统的目录结构(如 FATFS),所有文件都位于根目录下。它使用一种自定义的线性索引来跟踪文件及其数据页的位置,而不是像 FAT 表那样的结构。
SPIFFS 内部结构示意图:文件数据被分散存储在多个物理页中,通过元数据链接起来。
与常见文件系统的对比:
特性 | SPIFFS | FATFS (用于SD卡) | LittleFS (SPIFFS的替代者) |
---|---|---|---|
设计目标 | 极简,小文件,NOR Flash | 通用,兼容性,块设备 | 更可靠,更高性能,NOR Flash |
目录支持 | 不支持(只有根目录) | 支持(多级目录) | 支持(有限的多级目录) |
功耗优化 | 一般 | 一般 | 更优(更少的写放大) |
动态磨损均衡 | 支持 | 不支持(需手动管理) | 支持(更优的算法) |
掉电安全性 | 较好 | 一般(易产生碎片错误) | 更好(一致性更强) |
适用介质 | NOR Flash (SPI接口) | SD卡、MMC (块设备) | NOR Flash (SPI接口) |
表38.1.2 SPIFFS与常见文件系统的对比
SPIFFS 是嵌入式领域一个经典的、轻量级的解决方案,特别适合在片外 NOR Flash 上存储配置文件、网页资源、小数据日志等。
然而,由于其已知的缺点和已停止维护的状态,对于新项目,强烈建议考虑其现代替代品—LittleFS。LittleFS 提供了更好的性能、更高的可靠性以及更友好的目录支持,正在成为嵌入式Flash 文件系统的新标准。在 ESP-IDF 中,LittleFS已经得到了很好的支持,其使用方式与SPIFFS 非常相似。
38.2 硬件设计
38.2.1 例程功能
1.在 nor flash 指定区域新建 holle.txt 文件,然后对这文件进行读写操作;
2. LED 闪烁,指示程序正在运行。
38.2.2 硬件资源
1. LED 灯
LED -IO0
2. XL9555
IIC_SDA-IO41
IIC_SCL-IO42
3. SPILCD
CS-IO21
SCK-IO12
SDA-IO11
DC-IO40(在 P5 端口,使用跳线帽将 IO_SET 和 LCD_DC 相连)
PWR- IO1_3(XL9555)
RST- IO1_2(XL9555)
4. SPIFFS
38.2.3 原理图
本章实验使用的 SPIFFS 为软件库,因此没有对应的连接原理图。
38.3 程序设计
38.3.1 程序流程图
本实验的程序流程图:
图 38.3.1.1 IIC_EXIO 实验程序流程图
38.3.2 SPIFFS 函数解析
在 ESP-IDF 环境中,使用 SPIFFS 通常通过 虚拟文件系统 (VFS) 层进行挂载,然后使用标准的 POSIX 函数(如 open
, read
, write
, close
)或 C 库函数(fopen
, fprintf
, fscanf
)进行操作。
(1)注册装载 SPIFFS
该函数使用给定的路径前缀将 SPIFFS 注册并装载到 VFS,其函数原型如下所示:
esp_err_t esp_vfs_spiffs_register(const esp_vfs_spiffs_conf_t * conf);
该函数的形参描述,如下表所示:
形参 | 描述 |
---|---|
conf | 指向 esp_vfs_spiffs_conf_t 配置结构的指针 |
表 38.3.2.1 函数 esp_vfs_spiffs_register ()形参描述
该函数的返回值描述,如下表所示:
返回值 | 描述 |
---|---|
ESP_OK | 返回: 0,配置成功 |
ESP_ERR_NO_MEM | 如果无法分配对象 |
ESP_ERR_INVALID_STATE | 如果已安装或分区已加密 |
ESP_ERR_NOT_FOUND | 如果找不到 SPIFFS 的分区 |
ESP_FAIL | 如果装载或格式化失败 |
表 38.3.2.2 函数 esp_vfs_spiffs_register ()返回值描述
该函数使用 esp_vfs_spiffs_conf_t 类型的结构体变量传入,该结构体的定义如下所示:
结构体 | 成员变量 | 可选参数 |
---|---|---|
esp_vfs_spiffs_conf_t | base_path | 与文件系统关联的文件路径前缀。 |
partition_label | 可选,要使用的 SPIFFS 分区的标签。如果设置为 NULL,则 f | |
max_files | 可以同时打开的最大文件数。 | |
format_if_mount_failed | 如果为 true,则在装载失败时将格式化文件系统。 |
表 38.3.2.3 esp_vfs_spiffs_conf_t 结构体参数值描述
完成上述结构体参数配置之后,可以将结构传递给esp_vfs_spiffs_register 函数,用以实例化
SPIFFS。
(2)获取 SPIFFS 的信息
该函数用于获取 SPIFFS 的信息,其函数原型如下所示:
esp_err_t esp_spiffs_info(const char* partition_label,size_t *total_bytes,size_t *used_bytes);
该函数的形参描述,如下表所示:
形参 | 描述 |
---|---|
param partition_label | 指向分区标签的指针,分区表名称 |
total_bytes | 文件系统的大小 |
used_bytes | 文件系统中当前使用的字节数 |
表 38.3.2.4 函数 esp_spiffs_info ()形参描述
该函数的返回值描述,如下表所示:
返回值 | 描述 |
---|---|
ESP_OK | 返回: 0,配置成功 |
ESP_FAIL | 如果装载失败 |
表 38.3.2.5 函数 esp_spiffs_info ()返回值描述
(3)注销和卸载 SPIFFS
该函数从 VFS 注销和卸载 SPIFFS,其函数原型如下所示:
esp_err_t esp_vfs_spiffs_unregister(const char* partition_label);
该函数的形参描述,如下表所示:
形参 | 描述 |
---|---|
param partition_label | 指向分区表的指针,分区表名称 |
表 38.3.2.6 函数 esp_vfs_spiffs_unregister ()形参描述
该函数的返回值描述,如下表所示:
返回值 | 描述 |
---|---|
ESP_OK | 返回: 0,配置成功 |
ESP_ERR_INVALID_STATE | 已注销 |
表 38.3.2.7 函数 esp_vfs_spiffs_unregister ()返回值描述
38.3.3 SPIFFS 驱动解析
在IDF版的StandardExampleIDF(v5.3.x)\27_spiffs例程中,在分区表中添加 了 SPIFFS 的内容 , 27_spiffs\components\BSP 路径下并无新的驱动文件增加。分区表内容如下:
Name | Type | SubType | Offset | Size | Flags |
---|---|---|---|---|---|
|
|
|
|
| |
|
|
|
|
| |
|
|
|
|
| |
|
|
|
|
| |
|
|
|
|
|
(1)my_spiffs.h
#define DEFAULT_FD_NUM 5 /* 默认最大可打开文件数量 */
#define DEFAULT_MOUNT_POINT "/spiffs" /* 文件系统名称 *//* 函数声明 */
esp_err_t spiffs_init(char *partition_label, char *mount_point, size_t max_files); /* spiffs初始化 */
(2)my_spiffs.c
static const char *spiffs_tag = "spiffs";/*** @brief spiffs初始化* @param partition_label:分区表的分区名称* @param mount_point:文件系统关联的文件路径前缀* @param max_files:可以同时打开的最大文件数* @retval ESP_OK:成功; ESP_FAIL:失败*/
esp_err_t spiffs_init(char *partition_label, char *mount_point, size_t max_files)
{size_t total = 0; /* SPIFFS总容量 */size_t used = 0; /* SPIFFS已使用的容量 */esp_vfs_spiffs_conf_t spiffs_conf = { /* 配置spiffs文件系统的参数 */.base_path = mount_point, /* 磁盘路径,比如"0:","1:" */.partition_label = partition_label, /* 分区表的分区名称 */.max_files = max_files, /* 最大可同时打开的文件数 */.format_if_mount_failed = true, /* 挂载失败则格式化文件系统 */};esp_err_t ret = esp_vfs_spiffs_register(&spiffs_conf); /* 初始化和挂载SPIFFS分区 */if (ret != ESP_OK){if (ret == ESP_FAIL){ESP_LOGE(spiffs_tag, "Failed to mount or format filesystem");}else if (ret == ESP_ERR_NOT_FOUND){ESP_LOGE(spiffs_tag, "Failed to find SPIFFS partition");}else{ESP_LOGE(spiffs_tag, "Failed to initialize SPIFFS (%s)", esp_err_to_name(ret));}return ESP_FAIL;}ret = esp_spiffs_info(spiffs_conf.partition_label, &total, &used); /* 获取SPIFFS的总容量和已使用的容量 */if (ret != ESP_OK){ESP_LOGI(spiffs_tag, "Failed to get SPIFFS partition information (%s)", esp_err_to_name(ret));}else{ESP_LOGI(spiffs_tag, "Partition size: total: %d Bytes, used: %d Bytes", total, used);}return ret;
}/*** @brief 注销spiffs* @param partition_label:分区表的分区名称* @retval ESP_OK:注销成功; 其他:失败*/
esp_err_t spiffs_deinit(char *partition_label)
{return esp_vfs_spiffs_unregister(partition_label);
}
38.3.4 CMakeLists.txt 文件
打开本实验 BSP 下的 CMakeLists.txt 文件,其内容如下所示:
set(src_dirsLEDMYIICXL9555MYSPISPILCDSPIFFS)set(include_dirsLEDMYIICXL9555MYSPISPILCDSPIFFS)set(requiresdriveresp_lcdspiffs)idf_component_register(SRC_DIRS ${src_dirs} INCLUDE_DIRS ${include_dirs} REQUIRES ${requires})component_compile_options(-ffast-math -O3 -Wno-error=format=-Wno-format)
38.3.5 实验应用代码
打开 main/main.c 文件,该文件定义了工程入口函数,名为 app_main。该函数代码如下。
#define WRITE_DATA "ALIENTEK ESP32-S3" /* 写入数据 *//*** @brief 测试spiffs* @param 无* @retval 无*/
void spiffs_test(void)
{ESP_LOGI("spiffs_test", "Opening file");FILE *file_obj = fopen("/spiffs/hello.txt", "w"); /* 建立一个名为/spiffs/hello.txt的只写文件 */if (file_obj == NULL){ESP_LOGE("spiffs_test", "Failed to open file for writing");}fprintf(file_obj, WRITE_DATA); /* 写入字符 */fclose(file_obj); /* 关闭文件 */ESP_LOGI("spiffs_test", "File written");/* 重命名之前检查目标文件是否存在 */struct stat st;if (stat("/spiffs/foo.txt", &st) == 0) /* 获取文件信息,获取成功返回0 */{/* 从文件系统中删除一个名称。如果名称是文件的最后一个连接,并且没有其它进程将文件打开,名称对应的文件会实际被删除。 */unlink("/spiffs/foo.txt");}/* 重命名创建的文件 */ESP_LOGI("spiffs_test", "Renaming file");if (rename("/spiffs/hello.txt", "/spiffs/foo.txt") != 0){ESP_LOGE("spiffs_test", "Rename failed");}/* 打开重命名的文件并读取 */ESP_LOGI("spiffs_test", "Reading file");file_obj = fopen("/spiffs/foo.txt", "r");if (file_obj == NULL){ESP_LOGE("spiffs_test", "Failed to open file for reading");}char line[64];fgets(line, sizeof(line), file_obj); /* 从指定的流中读取数据 */fclose(file_obj);char *pos = strchr(line, '\n'); /* 指针pos指向第一个找到‘\n’ */if (pos){*pos = '\0'; /* 将‘\n’替换为‘\0’ */}ESP_LOGI("spiffs_test", "Read from file: '%s'", line);spilcd_show_string(110, 130, 200, 16, 16, line, BLUE);
}/*** @brief 程序入口* @param 无* @retval 无*/
void app_main(void)
{esp_err_t ret;ret = nvs_flash_init(); /* 初始化NVS */if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND){ESP_ERROR_CHECK(nvs_flash_erase());ESP_ERROR_CHECK(nvs_flash_init());}led_init(); /* LED初始化 */my_spi_init(); /* SPI初始化 */myiic_init(); /* MYIIC初始化 */xl9555_init(); /* XL9555初始化 */spilcd_init(); /* SPILCD初始化 */ESP_ERROR_CHECK(spiffs_init("storage", DEFAULT_MOUNT_POINT, DEFAULT_FD_NUM)); /* SPIFFS初始化 */spilcd_show_string(30, 50, 200, 16, 16, "ESP32-S3", RED);spilcd_show_string(30, 70, 200, 16, 16, "SPIFFS TEST", RED);spilcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);spilcd_show_string(30, 130, 200, 16, 16, "Read Text:", RED);spiffs_test(); /* SPIFFS测试 */while (1){LED0_TOGGLE();vTaskDelay(pdMS_TO_TICKS(500));}
}
在 SPIFFS 驱动中,首先初始化并挂载了一个 SPIFFS 分区,然后使用 POSIX 和 C 库 API 写入和读取数据。
可以看到,本实验的应用代码中,在一系列初始化之后,配置 spiffs文件系统各个参数,再
建立一个名为/spiffs/hello.txt 的只写文件, LED 闪烁表明程序正在运行。
38.4 下载验证
在完成编译和烧录操作后,在指定区域新建 hello.txt 文件,然后对这文件进行读写操作。
图 38.4.1 程序运行效果图