基于ESP32的桌面小屏幕实战[10]:使用文件系统存储文件
源码位置~/esp/esp-idf/examples/storage/spiffs
1. 基础知识
文件系统就是一个用于存储文件的系统模块,可以创建、打开、读写、关闭文件。后面的http service会用到文件系统。用手机连接桌面屏幕的热点,访问它的网页,这个功能需要文件系统。
SPIFFS(SPI Flash File System)是一种用于微控制器(MCU)和嵌入式系统的轻量级文件系统。它专为资源受限的设备设计,能够在 SPI 闪存设备上提供简单且可靠的文件存储方式。SPIFFS 不支持目录结构,因此所有文件都存储在一个扁平的空间中,但它在读写小文件时非常高效。
官方文档中说:“SPIFFS 是一个用于 SPI NOR flash 设备的嵌入式文件系统,支持磨损均衡、文件系统一致性检查等功能。”[1]
下面逐一介绍这句话里的专业名词:
-
SPI(Serial Peripheral Interface,串行外设接口)是一种用于短距离数据传输的高速串行通信协议,常用于在微控制器和外围设备之间(如闪存、传感器和显示器)建立连接。SPI 通信使用以下四条信号线:
- MOSI(主输出、从输入):主设备(如微控制器)发送数据,从设备(如闪存)接收。
- MISO(主输入、从输出):从设备发送数据,主设备接收。
- SCLK(时钟):由主设备生成的时钟信号,用于同步数据传输。
- SS/CS(从设备选择):选择从设备,确保只有被选中的从设备能进行数据传输。
SPI 的特点是全双工通信,数据传输速度快,广泛用于嵌入式设备中,如传感器、存储器和显示屏。
-
SPI NOR Flash 是一种通过 SPI 接口连接的 NOR 闪存。NOR Flash 是一种非易失性存储器,与 NAND Flash 相比,它的特点是:
- 字节级可寻址:可以直接访问特定字节地址,而 NAND Flash 是页级别访问。
- 高读取速度:适合需要频繁读取的应用,例如存储固件或启动代码。
- 较低写入速度和耐用性:通常 NAND Flash 写入速度更快、容量更大、成本更低,但 NOR Flash 更适合存储少量数据且需要快速读取的应用。
SPI NOR Flash 通常用于嵌入式系统和微控制器上存储固件、配置文件和日志数据等。
-
磨损均衡是一种管理闪存存储的技术,用于延长闪存的使用寿命。由于闪存具有有限的擦写次数,某些区域会因频繁擦写而过度磨损,导致数据可靠性下降。磨损均衡通过均匀分布写入操作,使所有区域的擦写次数接近,从而避免单一区域过早损坏。这通常通过以下方式实现:
- 动态磨损均衡:在文件频繁更新时,将文件数据移动到不同区域,均匀分布写入负载。
- 静态磨损均衡:不频繁更新的数据也会偶尔移动,以保证存储空间整体磨损分布均匀。
在 SPIFFS 中,磨损均衡是通过管理页和块的方式实现的。
-
文件系统一致性检查是指在文件系统初始化或异常(如断电、意外重启)后,对文件系统的元数据和存储内容进行检查,确保数据结构的完整性和一致性。例如,当设备断电后重新启动,文件系统会通过一致性检查验证文件结构、文件数据和元数据的正确性,防止损坏或数据丢失。
2. 源码
2.1 包含头文件以及变量定义
包含头文件
#include <stdio.h>
#include <string.h>
#include <sys/unistd.h>
#include <sys/stat.h>
#include "esp_err.h"
#include "esp_log.h"
#include "esp_spiffs.h"
"esp_spiffs.h"头文件定义了 SPIFFS 文件系统在 ESP32 环境中的接口和配置方式,帮助开发者在 SPI Flash 上方便地管理文件数据。
定义一个指向字符串 "example" 的常量指针 TAG,用于日志系统中标识该模块或文件的日志信息来源。
static const char *TAG = "example";
static表示TAG变量的作用域限定在本文件内,其他文件不能直接访问它。const表示TAG是一个常量指针,指向的字符串"example"是只读的,防止代码意外修改其内容。
2.2 主函数
定义 esp_vfs_spiffs_conf_t 类型的结构体变量 conf,用于配置 SPIFFS 文件系统。
void app_main(void)
{
ESP_LOGI(TAG, "Initializing SPIFFS");//打印日志
esp_vfs_spiffs_conf_t conf = {
.base_path = "/spiffs", //挂载点路径
.partition_label = NULL, //不指定具体的分区标签
.max_files = 5, //设置最多同时打开 5 个文件
.format_if_mount_failed = true //果挂载文件系统失败,尝试格式化并重新挂载
};
//... ...
conf是一个esp_vfs_spiffs_conf_t类型的结构体变量,用于配置 SPIFFS 文件系统。esp_vfs_spiffs_conf_t的定义可以在 ESP-IDF 框架的esp_spiffs.h头文件中找到。.base_path = "/spiffs":指定挂载点路径为/spiffs,之后的文件操作将使用这个路径。.partition_label = NULL:不指定具体的分区标签,使用默认的 SPIFFS 分区。.max_files = 5:设置最多同时打开 5 个文件。.format_if_mount_failed = true:如果挂载文件系统失败,尝试格式化并重新挂载。
使用上述配置初始化并挂载 SPIFFS 文件系统
esp_err_t ret = esp_vfs_spiffs_register(&conf);
用 conf 配置来初始化和挂载 SPIFFS 文件系统。如果成功,ret 会返回 ESP_OK;否则返回错误代码。
错误处理
if (ret != ESP_OK) {
if (ret == ESP_FAIL) {
ESP_LOGE(TAG, "Failed to mount or format filesystem");
} else if (ret == ESP_ERR_NOT_FOUND) {
ESP_LOGE(TAG, "Failed to find SPIFFS partition");
} else {
ESP_LOGE(TAG, "Failed to initialize SPIFFS (%s)", esp_err_to_name(ret));
}
return;
}
ESP_FAIL:挂载失败,且文件系统格式化也失败。ESP_ERR_NOT_FOUND:找不到指定的 SPIFFS 分区。- 其他错误:使用
esp_err_to_name(ret)将错误代码转换为字符串并打印错误信息。
检查并获取 SPIFFS 文件系统的状态,包括是否有错误、存储空间的总量和已使用量。
#ifdef CONFIG_EXAMPLE_SPIFFS_CHECK_ON_START
ESP_LOGI(TAG, "Performing SPIFFS_check().");
/* 调用 esp_spiffs_check 函数检查文件系统一致性,传入分区标签 conf.partition_label */
ret = esp_spiffs_check(conf.partition_label);
if (ret != ESP_OK) { // 如果检查结果不是 ESP_OK,则表示检查失败
ESP_LOGE(TAG, "SPIFFS_check() failed (%s)", esp_err_to_name(ret));
return; // 终止当前函数,返回调用者
} else {
ESP_LOGI(TAG, "SPIFFS_check() successful");
}
#endif
/* 定义两个变量 total 和 used,用于存储文件系统的总空间和已使用空间的大小 */
size_t total = 0, used = 0;
/* 获取 SPIFFS 文件系统的分区信息 */
ret = esp_spiffs_info(conf.partition_label, &total, &used);
if (ret != ESP_OK) { // 如果返回值 ret 不是 ESP_OK,则表示获取分区信息失败
ESP_LOGE(TAG, "Failed to get SPIFFS partition information (%s). Formatting...", esp_err_to_name(ret));
/* 调用 esp_spiffs_format 函数格式化 SPIFFS 文件系统,使其恢复到初始状态 */
esp_spiffs_format(conf.partition_label);
return; // 终止当前函数,返回调用者
} else {
ESP_LOGI(TAG, "Partition size: total: %d, used: %d", total, used);
}
//... ...
esp_spiffs_check- 功能:检查 SPIFFS 文件系统的一致性。
- 函数原型:
esp_err_t esp_spiffs_check(const char *partition_label); - 返回值为 ESP_OK 表示检查成功,文件系统一致性良好。返回值为其他错误代码(如 ESP_FAIL)表示文件系统出现了错误。
- 使用场景:通常用于系统启动时,确认 SPIFFS 文件系统没有发生数据损坏或不一致的情况。
esp_spiffs_info-
功能:获取 SPIFFS 文件系统的分区信息,包括总空间和已使用空间。
-
参数:
partition_label:要获取信息的 SPIFFS 分区标签。可以为 NULL 表示默认分区。total_bytes:用于存储文件系统的总空间(字节数)。used_bytes:用于存储文件系统的已使用空间(字节数)。
-
返回值:
ESP_OK表示成功获取分区信息。- 其他错误代码表示无法获取分区信息,可能是文件系统未挂载或指定的分区不存在。
-
使用场景:可以用来监测文件系统的空间使用情况,为文件读写提供参考。
-
esp_spiffs_format- 功能:格式化 SPIFFS 文件系统,将其清空并恢复为初始状态。
- 函数原型:
esp_err_t esp_spiffs_format(const char *partition_label); - 返回值:
- ESP_OK 表示格式化成功。
- 其他错误代码表示格式化失败。
- 使用场景:当文件系统出现严重错误或数据无法读取时,可以使用该函数进行格式化,以清除错误数据,恢复文件系统的正常状态。
检查已使用空间是否超过总空间
if (used > total) { //如果 used(已使用空间)大于 total(总空间),表明存在文件系统不一致的情况,继续执行检查。
ESP_LOGW(TAG, "Number of used bytes cannot be larger than total. Performing SPIFFS_check().");
ret = esp_spiffs_check(conf.partition_label); //检查文件系统一致性
// Could be also used to mend broken files, to clean unreferenced pages, etc.
// More info at https://github.com/pellepl/spiffs/wiki/FAQ#powerlosses-contd-when-should-i-run-spiffs_check
if (ret != ESP_OK) {
ESP_LOGE(TAG, "SPIFFS_check() failed (%s)", esp_err_to_name(ret));
return;
} else {
ESP_LOGI(TAG, "SPIFFS_check() successful");
}
}
使用标准的 C 库函数来在 SPIFFS 文件系统上创建和写入一个文件
ESP_LOGI(TAG, "Opening file");//日志记录
FILE* f = fopen("/spiffs/hello.txt", "w");//打开文件进行写入
/* 检查文件是否成功打开 */
if (f == NULL) {
ESP_LOGE(TAG, "Failed to open file for writing");
return;
}
fprintf(f, "Hello World!\n");//使用 fprintf 函数将 "Hello World!\n" 字符串写入文件 f 中
fclose(f);//关闭文件
ESP_LOGI(TAG, "File written");//日志记录:文件写入成功
FILE* f定义了一个指向FILE类型的指针变量f,用于在 C 语言中处理文件操作。FILE类型:FILE是一个结构体类型,通常用于表示打开的文件。它包含文件的状态信息、当前位置、读写模式等,由标准库定义。FILE*指针:FILE*是一个指向FILE结构的指针,表示文件的引用,用来在代码中访问和操作该文件。f变量:在这里,f是指向FILE的指针,用来保存fopen函数返回的文件指针。
在重命名文件前检查目标文件是否已存在。如果存在,则将其删除。
struct stat st;//定义一个stat类型的结构体变量st,用于存储文件信息。
if (stat("/spiffs/foo.txt", &st) == 0) {//使用 stat 函数检查 /spiffs/foo.txt 文件是否存在。stat 函数会将文件的信息存储到 st 中。
// Delete it if it exists
unlink("/spiffs/foo.txt");//如果文件存在,调用 unlink 函数删除该文件。
}
stat是 POSIX 标准的结构体类型,用于保存文件的元数据。unlink是 POSIX 标准中的文件删除函数,它会从文件系统中移除指定文件。
将文件重命名
ESP_LOGI(TAG, "Renaming file");//输出一条信息日志
if (rename("/spiffs/hello.txt", "/spiffs/foo.txt") != 0) {//重命名
ESP_LOGE(TAG, "Rename failed");
return;
}
打开并读取重命名后的文件的内容,并将其打印到日志中
ESP_LOGI(TAG, "Reading file");//输出日志,表示开始读取文件
f = fopen("/spiffs/foo.txt", "r");//以只读模式打开文件,将文件指针赋给f
if (f == NULL) {//如果 f 为 NULL,表示文件打开失败
ESP_LOGE(TAG, "Failed to open file for reading");
return;
}
char line[64];//定义字符数组,存储读取内容
fgets(line, sizeof(line), f);//使用 fgets 读取文件内容到 line,只读取第一行
fclose(f);//关闭文件,释放资源
// strip newline
char* pos = strchr(line, '\n');//查找 line 中的换行符(\n)位置
if (pos) {//如果找到
*pos = '\0';//将换行符替换为字符串终止符 '\0',去掉换行。
}
ESP_LOGI(TAG, "Read from file: '%s'", line);//打印文件内容
卸载 SPIFFS 文件系统并释放其占用的资源
esp_vfs_spiffs_unregister(conf.partition_label);
ESP_LOGI(TAG, "SPIFFS unmounted");//打印日志信息,确认 SPIFFS 文件系统已成功卸载
- 调用
esp_vfs_spiffs_unregister函数,可以卸载指定分区(即conf.partition_label指定的分区)上的 SPIFFS 文件系统。此函数会清理与 SPIFFS 相关的资源,将文件系统与虚拟文件系统 (VFS) 解除绑定,禁用 SPIFFS。
3. 编译工程
在编译工程之前,首先要做spiffs文件系统移植
在终端中使用
idf.py menuconfig
打开项目的配置界面
找到下面的Custom partition table CSV,并选择它

然后连上板子,在终端输入
idf.py -p /dev/ttyUSB0 flash monitor
下载程序。
打印出的日志如下


浙公网安备 33010602011771号