ESP32-S3 上居然能像电脑一样读写文件?SPIFFS 文件系统实战
ESP32-S3 上居然能像电脑一样读写文件?SPIFFS 文件系统实战
前言:NVS 够用了,为什么还需要文件系统?
前面学完 NVS,我一度觉得存储问题已经彻底解决了——键值对嘛,想存什么存什么。
直到有一天我想存一段比较长的 HTML 网页内容,还想存一张小图片,突然发现 NVS 用起来很别扭。NVS 本质上是键值对存储,适合存配置参数、计数器这类结构化的小数据。但如果我要存文件呢?一个 txt、一个 html、一个 json,用 NVS 来管理就太勉强了。
这时候就需要文件系统了。而且让我惊喜的是,ESP-IDF 的文件系统用的竟然是标准 C 的文件操作函数——fopen、fprintf、fgets、fclose——跟在电脑上写 C 语言作业一模一样。
一、ESP-IDF 支持哪些文件系统?
ESP-IDF 目前支持三种文件系统,我先做个横向对比,方便选型:
| 特性 | SPIFFS | FATFS | LittleFS |
|---|---|---|---|
| 设计目标 | SPI Flash 专用,轻量级 | 通用 FAT 格式 | 嵌入式专用,高可靠 |
| 资源占用 | 最少 | 较多 | 适中 |
| 磨损均衡 | 基本支持 | 需额外实现 | 高级算法 |
| 掉电安全 | 一般 | 一般 | 原子写入,崩溃恢复 |
| 大文件支持 | ❌ 不擅长 | ✅ 支持 | ✅ 支持 |
| PC 兼容性 | ❌ | ✅ Windows/Linux 直接识别 | ❌ |
| 适用场景 | 配置文件、小网页 | 多媒体、与 PC 交换数据 | 实时数据、重要配置 |
我的选择:先从 SPIFFS 入手。 它最轻量、API 最简单,而且三种文件系统的使用套路是相通的,学会一个,其他的很容易迁移。
SPIFFS vs NVS 到底有啥区别?
| NVS | SPIFFS | |
|---|---|---|
| 存储方式 | 键值对 | 文件 |
| 适合存什么 | Wi-Fi 密码、计数器、开关状态 | txt、html、json、小图片 |
| 读写方式 | nvs_set_xxx / nvs_get_xxx |
fopen / fprintf / fgets |
| 一句话总结 | 存参数用 NVS | 存文件用 SPIFFS |
二、第一步:创建分区表
上一篇学分区表的时候就说过,任何用到 Flash 的功能都得先在分区表里划地盘。SPIFFS 也不例外。
在工程根目录创建 partitions.csv:
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, , 0x6000,
phy_init, data, phy, , 0x1000,
factory, app, factory, , 1M,
storage, data, spiffs, , 0xF0000,
重点看最后一行:新增了一个名为 storage 的分区,类型是 data,子类型是 spiffs,大小 0xF0000(960KB)。
然后打开 idf.py menuconfig,把分区表切换成自定义的:
Partition Table → Custom partition table CSV
Custom partition CSV file → partitions.csv
三、第二步:注册并挂载文件系统
分区划好了,接下来要告诉系统:“这块分区是 SPIFFS 文件系统,请帮我挂载好。”
esp_vfs_spiffs_conf_t conf = {
.base_path = "/spiffs", // 挂载路径,后面所有文件路径都以这个开头
.partition_label = NULL, // 用默认的 spiffs 分区
.max_files = 5, // 最多同时打开 5 个文件
.format_if_mount_failed = true // 挂载失败就自动格式化
};
esp_err_t ret = esp_vfs_spiffs_register(&conf);
这里有几个关键配置我想单独说说:
base_path:相当于给文件系统指定一个"根目录"。设成 /spiffs 后,你要访问 hello.txt,完整路径就是 /spiffs/hello.txt。
format_if_mount_failed:这个一定要设成 true!第一次使用时,Flash 上还没有文件系统的格式信息,挂载必然失败。设成 true 就会自动格式化,省得手动处理。
partition_label:设成 NULL 就自动找第一个子类型为 spiffs 的分区。如果你有多个 SPIFFS 分区,才需要手动指定名字。
挂载后还需要做错误处理:
if (ret != ESP_OK) {
if (ret == ESP_FAIL) {
ESP_LOGE(TAG, "挂载或格式化失败");
} else if (ret == ESP_ERR_NOT_FOUND) {
ESP_LOGE(TAG, "找不到 SPIFFS 分区"); // 多半是分区表没配对
} else {
ESP_LOGE(TAG, "初始化 SPIFFS 失败 (%s)", esp_err_to_name(ret));
}
return;
}
⚠️ 注意:首次启动时,格式化过程大约需要 10 秒左右,串口会卡住不动,这是正常的,别急着拔线。
四、第三步:获取分区信息
挂载成功后,可以查看一下分区有多大、已经用了多少:
size_t total = 0, used = 0;
ret = esp_spiffs_info(conf.partition_label, &total, &used);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "获取分区信息失败,正在格式化...");
esp_spiffs_format(conf.partition_label);
return;
} else {
ESP_LOGI(TAG, "分区大小 - 总计: %d, 已用: %d", total, used);
}
五、第四步:一致性检查
这一步是个保险措施。如果 ESP32-S3 在文件操作过程中突然断电,SPIFFS 可能会损坏,导致 used > total 这种不合理的情况。这时候就需要调用 esp_spiffs_check 来修复:
if (used > total) {
ESP_LOGW(TAG, "已用空间大于总空间,执行一致性检查...");
ret = esp_spiffs_check(conf.partition_label);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "SPIFFS 检查失败 (%s)", esp_err_to_name(ret));
return;
} else {
ESP_LOGI(TAG, "SPIFFS 检查通过");
}
}
六、第五步:像在电脑上一样读写文件
前面所有步骤都是准备工作,到这里才是真正激动人心的部分——用标准 C 函数操作文件!
写入文件
ESP_LOGI(TAG, "创建并写入文件...");
FILE* f = fopen("/spiffs/hello.txt", "w");
if (f == NULL) {
ESP_LOGE(TAG, "打开文件失败");
return;
}
fprintf(f, "Hello World!\n");
fclose(f);
ESP_LOGI(TAG, "文件写入成功");
读取文件
ESP_LOGI(TAG, "读取文件...");
f = fopen("/spiffs/hello.txt", "r");
if (f == NULL) {
ESP_LOGE(TAG, "打开文件失败");
return;
}
char line[64];
fgets(line, sizeof(line), f);
fclose(f);
// 去掉换行符
char* pos = strchr(line, '\n');
if (pos) {
*pos = '\0';
}
ESP_LOGI(TAG, "读取到的内容: '%s'", line);
用完记得卸载
esp_vfs_spiffs_unregister(conf.partition_label);
ESP_LOGI(TAG, "SPIFFS 已卸载");
七、完整代码
把上面所有步骤串起来,就是一个完整的 Demo:
#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"
static const char *TAG = "SPIFFS_DEMO";
void app_main(void)
{
// ========== 1. 配置并挂载 SPIFFS ==========
ESP_LOGI(TAG, "初始化 SPIFFS...");
esp_vfs_spiffs_conf_t conf = {
.base_path = "/spiffs",
.partition_label = NULL,
.max_files = 5,
.format_if_mount_failed = true
};
esp_err_t ret = esp_vfs_spiffs_register(&conf);
if (ret != ESP_OK) {
if (ret == ESP_FAIL) {
ESP_LOGE(TAG, "挂载或格式化失败");
} else if (ret == ESP_ERR_NOT_FOUND) {
ESP_LOGE(TAG, "找不到 SPIFFS 分区");
} else {
ESP_LOGE(TAG, "初始化失败 (%s)", esp_err_to_name(ret));
}
return;
}
// ========== 2. 获取分区信息 ==========
size_t total = 0, used = 0;
ret = esp_spiffs_info(conf.partition_label, &total, &used);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "获取分区信息失败,格式化中...");
esp_spiffs_format(conf.partition_label);
return;
}
ESP_LOGI(TAG, "分区大小 - 总计: %d, 已用: %d", total, used);
// ========== 3. 一致性检查 ==========
if (used > total) {
ESP_LOGW(TAG, "数据异常,执行一致性检查...");
ret = esp_spiffs_check(conf.partition_label);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "检查失败 (%s)", esp_err_to_name(ret));
return;
}
ESP_LOGI(TAG, "检查通过");
}
// ========== 4. 写入文件 ==========
ESP_LOGI(TAG, "写入文件...");
FILE* f = fopen("/spiffs/hello.txt", "w");
if (f == NULL) {
ESP_LOGE(TAG, "打开文件失败");
return;
}
fprintf(f, "Hello World!\n");
fclose(f);
ESP_LOGI(TAG, "写入成功");
// ========== 5. 读取文件 ==========
ESP_LOGI(TAG, "读取文件...");
f = fopen("/spiffs/hello.txt", "r");
if (f == NULL) {
ESP_LOGE(TAG, "打开文件失败");
return;
}
char line[64];
fgets(line, sizeof(line), f);
fclose(f);
char* pos = strchr(line, '\n');
if (pos) *pos = '\0';
ESP_LOGI(TAG, "读取到: '%s'", line);
// ========== 6. 卸载 ==========
esp_vfs_spiffs_unregister(conf.partition_label);
ESP_LOGI(TAG, "SPIFFS 已卸载");
}
八、进阶:把电脑上的文件直接烧录到板子里
这个功能让我眼前一亮。
假设我电脑上有个 hello1.txt,内容是 this is hello1 txt,我想让它在烧录程序的时候一起烧到 Flash 里,这样程序启动后直接就能读到这个文件,不需要代码里手动创建。
第一步:准备文件
在工程目录下创建一个文件夹,比如 txt,里面放上要烧录的文件:
demo/
├── main/
│ ├── CMakeLists.txt
│ └── main.c
├── txt/
│ └── hello1.txt ← 内容:this is hello1 txt
├── partitions.csv
└── CMakeLists.txt
第二步:修改 CMakeLists.txt
在 demo/main/CMakeLists.txt 中添加一行:
spiffs_create_partition_image(storage "../txt" FLASH_IN_PROJECT)
这句话的意思是:
storage— 对应分区表里的分区名"../txt"— 电脑上要打包的文件夹路径FLASH_IN_PROJECT— 编译时自动打包成 SPIFFS 镜像,烧录时一起写入 Flash
第三步:代码中直接读取
// 不需要先 fopen + fprintf 创建文件了,直接读!
FILE* f = fopen("/spiffs/hello1.txt", "r");
if (f == NULL) {
ESP_LOGE(TAG, "打开文件失败");
return;
}
char line[64];
fgets(line, sizeof(line), f);
fclose(f);
char* pos = strchr(line, '\n');
if (pos) *pos = '\0';
ESP_LOGI(TAG, "读取到: '%s'", line);
编译烧录后,串口输出:
I (xxx) SPIFFS_DEMO: 读取到: 'this is hello1 txt'
文件确实在烧录阶段就写入 Flash 了,代码启动后直接就能读到。
这个功能非常实用! 比如做 Web Server 的时候,可以把 HTML、CSS、JS 文件放在文件夹里,编译时自动打包烧录,不需要在代码里拼接字符串。
九、总结
回顾一下,使用 SPIFFS 的完整流程:
创建分区表 → 配置 menuconfig → 注册挂载 → 获取信息 → 一致性检查 → 读写文件 → 卸载
几个关键点:
- 分区表是前提 — 没有分区,文件系统无处安放
- 首次格式化会卡 10 秒 — 别以为程序挂了
- 用的是标准 C 文件操作 —
fopen、fprintf、fgets,零学习成本 spiffs_create_partition_image是神器 — 可以把电脑上的文件直接烧进去- 三种文件系统套路相同 — 学会 SPIFFS,LittleFS 和 FATFS 很容易迁移
对我来说,学完这个最大的感受是:ESP32-S3 远比我想象中强大。它不只是一个跑跑 GPIO、点点灯的单片机,它有 NVS 键值存储、有文件系统、有分区管理,这些能力组合起来,完全可以做出相当复杂的产品。
不过在 ESP-IDF v5.3+ 中,官方开始更推荐使用 LittleFS 替代 SPIFFS。SPIFFS 目前仍然可用,但未来可能被标记为 deprecated。如果是新项目,可以考虑直接用 LittleFS。
下一篇打算学习 Wi-Fi 相关的内容,敬请期待!如果觉得有帮助,欢迎点赞收藏~
更多推荐
所有评论(0)