本帖最后由 正点原子运营 于 2026-1-21 09:22 编辑
第三十九章 SPIFFS文件系统
1)实验平台:正点原子DNESP32P4开发板
2)章节摘自【正点原子】ESP32-P4开发指南— V1.0
3)购买链接:https://detail.tmall.com/item.htm?id=873309579825
4)全套实验源码+手册+视频下载地址:http://www.openedv.com/docs/boards/esp32/ATK-DNESP32P4.html
5)正点原子官方B站:https://space.bilibili.com/394620890
6)正点原子DNESP32S3开发板技术交流群:132780729
在现代嵌入式系统中,存储管理是一个至关重要的方面,尤其是在资源有限的环境中。SPIFFS(SPI Flash File System)作为一种专为SPI NOR Flash设备设计的文件系统,提供了一种高效的方式来管理和存储数据。其特性如磨损均衡和文件系统一致性检查,使得SPIFFS成为许多嵌入式应用的理想选择。本章将深入探讨SPIFFS文件系统的结构、性能特性以及使用方法。我们将讨论其扁平文件结构的实现、垃圾收集机制的工作原理以及如何通过工具生成和管理SPIFFS镜像。这些内容将为开发者在使用ESP32-P4等嵌入式平台时提供必要的知识,帮助他们有效地利用SPIFFS文件系统来实现数据存储需求。希望读者能够通过本章的学习,掌握SPIFFS的基本概念及其在实际项目中的应用
39.1 SPIFFS简介
39.2 硬件设计
39.3 程序设计
39.4 下载验证
39.1 SPIFFS简介
SPIFFS(SPI Flash File System)是专为SPI NOR Flash设计的嵌入式文件系统,支持磨损均衡和文件系统一致性检查等功能。用户可通过Posix接口或VFS操作SPIFFS的大部分功能(见下图所示)。与FAT文件系统相比,SPIFFS在RAM资源占用上更为高效,适用于容量小于128 MB的Flash芯片。在第七章中,笔者介绍了分区表的管理与使用,其中storage分区表的存储类型为SPIFFS,允许通过相关文件系统操作函数对自定义的SPIFFS存储区域进行数据存储和数据操作。
图39.1.1 VFS虚拟文件系统和真实文件系统的关系
上图中,ESP32-P4支持VFS(虚拟文件系统),通过一个统一接口使应用层可以便捷地访问底层文件系统,如SPIFFS和FAT。VFS为开发者提供了标准的POSIX风格文件操作接口,无需针对不同文件系统编写特定代码。SPIFFS文件系统适合小容量的SPI NOR Flash存储,具备磨损均衡和一致性检查功能,而FAT文件系统则适用于容量较大的存储设备,如SD卡。在ESP32-P4中,VFS实现了文件系统的抽象化,提升了代码的通用性和灵活性。
下面笔者着重讲解SPIFFS文件系统结构和性能与限制等相关知识。
1,SPIFFS文件系统结构
SPIFFS目前不支持目录结构,采用扁平结构。这意味着在挂载于/spiffs时,创建文件的路径如/spiffs/tmp/myfile.txt实际上会生成名为/tmp/myfile.txt的文件,而不是在/spiffs/tmp下创建。
2,SPIFFS性能与限制
SPIFFS并不是实时操作系统,因此写入操作的时间并不固定,这取决于写入数据的大小和文件系统的状态。此外,SPIFFS在稳定使用时,仅建议使用约75%的分区容量。一旦空间不足,垃圾收集器会多次尝试回收可用空间。需要注意的是,如果在文件系统操作期间发生断电,可能会导致SPIFFS损坏,但可以使用esp_spiffs_check函数进行恢复。
3,SPIFFS垃圾收集
垃圾收集器会在每次扫描中尝试释放可用数据块。默认情况下,垃圾收集的最大运行次数为10次,可以通过SPIFFS_GC_MAX_RUNS选项进行配置。写入数据的大小不得超过最大运行次数乘以数据块大小,否则将返回错误。
39.2 硬件设计
39.2.1 程序功能
在storage分区区域新建holle.txt文件,然后对这文件进行读写操作。
39.2.2 硬件资源
1)LED灯
LED 0 - IO51
2)LCD
4.3/7 寸 ALIENTEK RGBLCD屏
5/10.1寸ALIENTEK MIPI屏
3 SPIFFS
39.2.3 原理图
SPIFFS器件相关原理图,如下图所示。
图39.2.3.1 Flash硬件原理图
在上图中,我们将16M Flash划分为五个子分区,其中 storage 分区用于作为SPIFFS文件系统的存储区域。本章的实验将在该分区内进行操作。
39.3 程序设计
39.3.1 SPIFFS的IDF驱动
SPIFFS外设驱动位于ESP-IDF的components\spiffs目录。该目录中的include文件夹存放SPIFFS相关的头文件,声明了SPIFFS函数和结构体等;而esp_spiffs.c文件是实现SPIFFS操作函数。要使用SPIFFS功能,必须先导入以下头文件。
接下来,作者将介绍一些常用的SPIFFS函数,这些函数的描述及其作用如下:
1,注册和挂载SPIFFS esp_vfs_spiffs_register
该函数用于将SPIFFS注册并挂载到VFS中,其函数原型如下:
- esp_err_t esp_vfs_spiffs_register(const esp_vfs_spiffs_conf_t * conf);
复制代码 函数形参:
表39.3.1.1 esp_vfs_spiffs_register函数形参描述
返回值:
ESP_OK表示成功。
ESP_ERR_NO_MEM表示内存不足。
ESP_ERR_INVALID_STATE表示已挂载或分区加密。
ESP_ERR_NOT_FOUND表示未找到SPIFFS分区。
ESP_FAIL表示挂载或格式化失败。
conf为指向SPIFFS配置结构体的指针。接下来,笔者将详细介绍esp_vfs_spiffs_conf_t结构体中的各个成员变量,如下代码所示:
- typedef struct {
- const char* base_path; /* 与文件系统关联的文件路径前缀。 */
- /* 可选,使用的SPIFFS分区标签。
- 如果设置为NULL,将使用第一个子类型为spiffs的分区。 */
- const char* partition_label;
- size_t max_files; /* 同时可以打开的最大文件数。 */
- /* 如果为true,当文件系统挂载失败时将格式化文件系统。 */
- bool format_if_mount_failed;
- } esp_vfs_spiffs_conf_t;
复制代码 结构体用于配置SPIFFS文件系统参数,以下对各个成员做简单介绍。
1)base_path
用于设置文件系统的路径前缀,一般我们设置“0:/”。
2)partition_label
用于指向操作那个分区名称,这个字段是根据项目工程的分区表的子分区名称而定的。
3)max_files
用于配置最大打开文件数量,一般我们配置为5。
4)format_if_mount_failed
若设置为true,则文件系统挂载失败后会格式化子分区。
2,卸载和注销SPIFFS esp_vfs_spiffs_unregister
该函数用于从VFS中卸载并注销SPIFFS,其函数原型如下:
- esp_err_t esp_vfs_spiffs_unregister(const char* partition_label);
复制代码 函数形参:
表39.3.1.3 esp_vfs_spiffs_unregister函数形参描述
返回值:
ESP_OK表示成功。
ESP_ERR_INVALID_STATE表示已卸载。
3,检查SPIFFS挂载状态esp_spiffs_mounted
该函数用于检查指定分区是否挂载,其函数原型如下:
- bool esp_spiffs_mounted(const char* partition_label);
复制代码 函数形参:
表39.3.1.4 esp_spiffs_mounted函数形参描述
返回值:
true表示已挂载。
false表示未挂载。
4,格式化SPIFFS esp_spiffs_format
该函数用于格式化指定分区,其函数原型如下:
- esp_err_t esp_spiffs_format(const char* partition_label);
复制代码 函数形参:
表39.3.1.5 esp_spiffs_format函数形参描述
返回值:
ESP_OK表示成功。
ESP_FAIL表示失败。
5,获取SPIFFS信息esp_spiffs_info
该函数用于获取文件系统的大小和已用字节数,其函数原型如下:
- esp_err_t esp_spiffs_info(const char* partition_label,
- size_t *total_bytes, size_t *used_bytes);
复制代码 函数形参:
表39.3.1.6 esp_spiffs_info函数形参描述
返回值:
ESP_OK表示成功。
ESP_ERR_INVALID_STATE表示未挂载。
6,SPIFFS完整性检查esp_spiffs_check
该函数用于检查文件系统的完整性,其函数原型如下:
- esp_err_t esp_spiffs_check(const char* partition_label);
复制代码 函数形参:
表39.3.1.7 esp_spiffs_check函数形参描述
返回值:
ESP_OK表示成功。
ESP_ERR_INVALID_STATE表示未挂载。
ESP_FAIL表示出错。
7,垃圾回收esp_spiffs_gc
该函数用于在SPIFFS分区中执行垃圾回收,至少释放指定字节的空间,其函数原型如下:
- esp_err_t esp_spiffs_gc(const char* partition_label, size_t size_to_gc);
复制代码 函数形参:
表39.3.1.8 esp_spiffs_gc函数形参描述
返回值:
ESP_OK表示成功。
ESP_ERR_NOT_FINISHED表示无法回收指定大小。
ESP_ERR_INVALID_STATE表示分区未挂载。
ESP_FAIL表示其他错误。
39.3.2 程序流程图
图39.3.2.1 SPIFFS实验程序流程图
39.3.3 程序解析
在30_spiffs例程中,作者在30_spiffs\components\BSP路径下新建SPIFFS文件夹,并且需要更改CMakeLists.txt内容,以便在其他文件上调用。
1,SPIFFS驱动代码
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。SPIFFS驱动源码包括两个文件:my_spiffs.c和my_spiffs.h。
下面先解析my_spiffs.h的程序。对SPIFFS文件数量与磁盘路径做了定义。
- <font size="3">#define DEFAULT_FD_NUM 5 /* 默认最大可打开文件数量 */</font>
- <font size="3">#define DEFAULT_MOUNT_POINT "/spiffs" /* 文件系统名称 */</font>
复制代码 下面我们再解析my_spiffs.c的程序,看一下初始化函数spiffs_init,代码如下:
- <font size="3">/**</font>
- <font size="3"> * @brief spiffs初始化</font>
- <font size="3"> * [url=home.php?mod=space&uid=271674]@param[/url] partition_label:分区表的分区名称</font>
- <font size="3"> * @param mount_point:文件系统关联的文件路径前缀</font>
- <font size="3"> * @param max_files:可以同时打开的最大文件数</font>
- <font size="3"> * @retval ESP_OK:成功; ESP_FAIL:失败</font>
- <font size="3"> */</font>
- <font size="3">esp_err_t spiffs_init(char *partition_label, </font>
- <font size="3">char *mount_point, </font>
- <font size="3">size_t max_files)</font>
- <font size="3">{</font>
- <font size="3"> size_t total = 0; /* SPIFFS总容量 */</font>
- <font size="3"> size_t used = 0; /* SPIFFS已使用的容量 */</font>
- <font size="3"> </font>
- <font size="3"> esp_vfs_spiffs_conf_t spiffs_conf = { /* 配置spiffs文件系统的参数 */</font>
- <font size="3"> .base_path = mount_point, /* 磁盘路径,比如"0:","1:" */</font>
- <font size="3"> .partition_label = partition_label, /* 分区表的分区名称 */</font>
- <font size="3"> .max_files = max_files, /* 最大可同时打开的文件数 */</font>
- <font size="3"> .format_if_mount_failed = true, /* 挂载失败则格式化文件系统 */</font>
- <font size="3"> };</font>
- <font size="3">/* 初始化和挂载SPIFFS分区 */</font>
- <font size="3">esp_err_t ret = esp_vfs_spiffs_register(&spiffs_conf); </font>
- <font size="3"> if (ret != ESP_OK)</font>
- <font size="3"> {</font>
- <font size="3"> if (ret == ESP_FAIL)</font>
- <font size="3"> {</font>
- <font size="3"> ESP_LOGE(spiffs_tag, "Failed to mount or format filesystem");</font>
- <font size="3"> }</font>
- <font size="3"> else if (ret == ESP_ERR_NOT_FOUND)</font>
- <font size="3"> {</font>
- <font size="3"> ESP_LOGE(spiffs_tag, "Failed to find SPIFFS partition");</font>
- <font size="3"> }</font>
- <font size="3"> else</font>
- <font size="3"> {</font>
- <font size="3"> ESP_LOGE(spiffs_tag, "Failed to initialize SPIFFS (%s)", </font>
- <font size="3">esp_err_to_name(ret));</font>
- <font size="3"> }</font>
- <font size="3"> return ESP_FAIL;</font>
- <font size="3"> }</font>
- <font size="3">/* 获取SPIFFS的总容量和已使用的容量 */</font>
- <font size="3">ret = esp_spiffs_info(spiffs_conf.partition_label, &total, &used); </font>
- <font size="3"> if (ret != ESP_OK)</font>
- <font size="3"> {</font>
- <font size="3"> ESP_LOGI(spiffs_tag, "Failed to get SPIFFS partition information (%s)", </font>
- <font size="3">esp_err_to_name(ret));</font>
- <font size="3"> }</font>
- <font size="3"> else</font>
- <font size="3"> {</font>
- <font size="3"> ESP_LOGI(spiffs_tag, "Partition size: total: %d Bytes, used: %d Bytes", </font>
- <font size="3">total, used);</font>
- <font size="3"> }</font>
- <font size="3"> return ret;</font>
- <font size="3">}</font>
复制代码 spiffs_init函数负责初始化和挂载SPIFFS分区,接收分区名称、文件系统路径前缀以及最大可同时打开的文件数作为参数。通过设置配置结构体并调用esp_vfs_spiffs_register来挂载分区,并在成功后获取和记录分区的总容量和已使用容量。如果挂载失败,会输出相应的错误信息。
注销分区挂载函数如下所示。
- <font size="3">/**</font>
- <font size="3"> * @brief 注销spiffs</font>
- <font size="3"> * @param partition_label:分区表的分区名称</font>
- <font size="3"> * @retval ESP_OK:注销成功; 其他:失败</font>
- <font size="3"> */</font>
- <font size="3">esp_err_t spiffs_deinit(char *partition_label)</font>
- <font size="3">{</font>
- <font size="3"> return esp_vfs_spiffs_unregister(partition_label);</font>
- <font size="3">}</font>
复制代码 spiffs_deinit函数用于注销已挂载的SPIFFS分区,通过esp_vfs_spiffs_unregister来释放资源。
2,CMakeLists.txt文件
本例程的功能实现主要依靠SPIFFS驱动。要在main函数中,成功调用SPIFFS文件中的内容,就得需要修改BSP文件夹下的CMakeLists.txt文件(重点看红色内容),修改如下:
- <font size="3">set(src_dirs</font>
- <font size="3"> LED</font>
- <font size="3"> LCD</font>
- <font size="3"> SPIFFS</font>
- <font size="3"> )</font>
- <font size="3">set(include_dirs</font>
- <font size="3"> LED</font>
- <font size="3"> LCD</font>
- <font size="3"> SPIFFS)</font>
- <font size="3">set(requires</font>
- <font size="3"> driver</font>
- <font size="3"> esp_lcd</font>
- <font size="3"> esp_common</font>
- <font size="3"> spiffs)</font>
- <font size="3">idf_component_register(SRC_DIRS ${src_dirs} INCLUDE_DIRS ${include_dirs}</font>
- <font size="3"> REQUIRES ${requires})</font>
- <font size="3">component_compile_options(-ffast-math -O3 -Wno-error=format=-Wno-format)</font>
复制代码 3,main.c驱动代码
在main.c里面编写如下代码。
- <font size="3">#define WRITE_DATA "ALIENTEK ESP32-P4" /* 写入数据 */</font>
- <font size="3">void spiffs_test(void)</font>
- <font size="3">{</font>
- <font size="3"> ESP_LOGI("spiffs_test", "Opening file");</font>
- <font size="3"> /* 建立一个名为/spiffs/hello.txt的只写文件 */</font>
- <font size="3"> FILE *file_obj = fopen("/spiffs/hello.txt", "w"); </font>
- <font size="3"> if (file_obj == NULL)</font>
- <font size="3"> {</font>
- <font size="3"> ESP_LOGE("spiffs_test", "Failed to open file for writing");</font>
- <font size="3"> }</font>
- <font size="3"> fprintf(file_obj, WRITE_DATA); /* 写入字符 */</font>
- <font size="3"> fclose(file_obj); /* 关闭文件 */</font>
- <font size="3"> ESP_LOGI("spiffs_test", "File written");</font>
- <font size="3"> /* 重命名之前检查目标文件是否存在 */</font>
- <font size="3"> struct stat st;</font>
- <font size="3"> if (stat("/spiffs/foo.txt", &st) == 0) /* 获取文件信息,获取成功返回0 */</font>
- <font size="3"> { /* 从文件系统中删除一个名称。</font>
- <font size="3"> 如果名称是文件的最后一个连接,并且没有其它进程将文件打开,</font>
- <font size="3"> 名称对应的文件会实际被删除。 */</font>
- <font size="3"> unlink("/spiffs/foo.txt");</font>
- <font size="3"> }</font>
-
- <font size="3"> /* 重命名创建的文件 */</font>
- <font size="3"> ESP_LOGI("spiffs_test", "Renaming file");</font>
- <font size="3"> if (rename("/spiffs/hello.txt", "/spiffs/foo.txt") != 0)</font>
- <font size="3"> {</font>
- <font size="3"> ESP_LOGE("spiffs_test", "Rename failed");</font>
- <font size="3"> }</font>
-
- <font size="3"> /* 打开重命名的文件并读取 */</font>
- <font size="3"> ESP_LOGI("spiffs_test", "Reading file");</font>
- <font size="3"> file_obj = fopen("/spiffs/foo.txt", "r");</font>
- <font size="3"> if (file_obj == NULL)</font>
- <font size="3"> {</font>
- <font size="3"> ESP_LOGE("spiffs_test", "Failed to open file for reading");</font>
- <font size="3"> }</font>
- <font size="3"> char line[64];</font>
- <font size="3"> fgets(line, sizeof(line), file_obj); /* 从指定的流中读取数据 */</font>
- <font size="3"> fclose(file_obj);</font>
- <font size="3"> </font>
- <font size="3"> char *pos = strchr(line, '\n'); /* 指针pos指向第一个找到‘\n’ */</font>
- <font size="3"> if (pos)</font>
- <font size="3"> {</font>
- <font size="3"> *pos = '\0'; /* 将‘\n’替换为‘\0’ */</font>
- <font size="3"> }</font>
- <font size="3"> ESP_LOGI("spiffs_test", "Read from file: '%s'", line);</font>
- <font size="3"> lcd_show_string(110, 130, 200, 16, 16, line, BLUE);</font>
- <font size="3">}</font>
- <font size="3">void app_main(void)</font>
- <font size="3">{</font>
- <font size="3"> esp_err_t ret;</font>
- <font size="3"> ret = nvs_flash_init(); /* 初始化NVS */</font>
- <font size="3"> if(ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND)</font>
- <font size="3"> {</font>
- <font size="3"> ESP_ERROR_CHECK(nvs_flash_erase());</font>
- <font size="3"> ESP_ERROR_CHECK(nvs_flash_init());</font>
- <font size="3"> }</font>
- <font size="3"> led_init(); /* LED初始化 */</font>
- <font size="3"> lcd_init(); /* LCD屏初始化 */</font>
- <font size="3">ESP_ERROR_CHECK(spiffs_init("storage", DEFAULT_MOUNT_POINT, </font>
- <font size="3">DEFAULT_FD_NUM)); /* SPIFFS初始化 */</font>
- <font size="3"> lcd_show_string(30, 50, 200, 16, 16, "ESP32-P4", RED);</font>
- <font size="3"> lcd_show_string(30, 70, 200, 16, 16, "SPIFFS TEST", RED);</font>
- <font size="3"> lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);</font>
- <font size="3"> lcd_show_string(30, 130, 200, 16, 16, "Read Text:", RED);</font>
- <font size="3"> spiffs_test(); /* SPIFFS测试 */</font>
- <font size="3"> while (1)</font>
- <font size="3"> {</font>
- <font size="3"> LED0_TOGGLE();</font>
- <font size="3"> vTaskDelay(pdMS_TO_TICKS(500));</font>
- <font size="3"> }</font>
- <font size="3">}</font>
复制代码 上述代码实现了一个对SPIFFS(SPI Flash File System)的基本测试程序,包含文件的创建、写入、重命名和读取功能。spiffs_test函数首先创建一个名为/spiffs/hello.txt的文件,并写入预定义的数据。如果目标文件/spiffs/foo.txt已存在,它会在重命名之前删除该文件。接着,它将hello.txt重命名为foo.txt,并读取该文件的内容,处理后显示在LCD屏幕上。
主函数app_main初始化非易失性存储(NVS)、LED和LCD屏幕,随后调用spiffs_init初始化SPIFFS文件系统,最后通过调用spiffs_test函数进行文件操作测试,并在一个无限循环中切换LED状态。这段代码展示了如何在ESP32-P4上使用SPIFFS进行基本的文件操作,并通过LCD实时显示结果。
39.4 下载验证
在程序下载完成后,如果SPIFFS文件系统挂载失败,则会自动执行擦除操作,擦除对象为storage分区。此时,屏幕会显示白屏。当擦除操作成功完成后,便可以对storage分区进行正常操作。下图中,笔者向storage分区的foo.txt文件写入“ALIENTEK ESP32-P4”字符串,写入成功后读取该文件的内容。
图39.4.1 向storage分区读取foo.txt文件内容 |