2. SPIFFS 文件系统初探:初始化、格式化与基础读写
SPIFFS,全称是 SPI Flash File System。说白了,它就是给 ESP32 那片 SPI Flash 芯片量身定做的一个轻量级文件系统。你想想看,我们做嵌入式项目,总得存点配置参数、网页文件、证书什么的吧?总不能每次都写死在代码里,一改就得重新编译烧录,那多麻烦。
我个人习惯,只要项目里涉及 Web 服务器、OTA 升级或者需要保存用户配置,第一件事就是把 SPIFFS 先搭起来。它就像给 MCU 装了个小硬盘,虽然不大,但足够用了。
2.1 为什么需要 SPIFFS?
在 ESP32 上,程序固件和文件数据是分开存放的。固件烧录在 Flash 的特定分区,而 SPIFFS 则占用另一个独立分区。这样做的好处很明显:
- 解耦数据与代码:修改网页、图片、配置文件,不需要重新编译固件。
- 运行时持久化:设备掉电后,写入 SPIFFS 的数据不会丢失。
- 方便远程更新:配合 OTA,可以直接把新网页文件上传到 SPIFFS,实现功能升级。
核心概念:SPIFFS 是一个 磨损均衡 的文件系统。它会尽量让擦写操作均匀分布在 Flash 的各个区块上,延长 Flash 寿命。但注意,它没有日志恢复功能,突然掉电可能导致文件损坏。
2.2 初始化 SPIFFS
初始化是第一步,也是最容易出问题的一步。我见过不少新手,代码写了一大堆,结果 SPIFFS 没挂载成功,后面所有文件操作都返回错误,排查半天才发现是初始化参数没配对。
ESP-IDF 和 Arduino 框架都提供了 SPIFFS 的 API。这里我以 Arduino 框架为例,因为它更常用,代码也更简洁。
#include <SPIFFS.h>
void setup() {
Serial.begin(115200);
// 初始化 SPIFFS
if (!SPIFFS.begin(true)) { // true 表示如果挂载失败则自动格式化
Serial.println("SPIFFS 挂载失败!");
return;
}
Serial.println("SPIFFS 挂载成功!");
}
看到那个 begin(true) 了吗?这个参数很关键。它告诉 SPIFFS:如果检测到文件系统损坏或者从未格式化过,就自动执行格式化。我个人建议,在产品开发阶段,可以一直开着这个参数。但到了量产阶段,最好改成 begin(false),然后单独做一个格式化按钮或指令,避免意外格式化导致用户数据丢失。
注意:SPIFFS.begin(true) 的自动格式化会 清空整个 SPIFFS 分区。如果你在分区里存了重要数据,一旦触发格式化,数据就全没了。我曾经在一个远程升级项目里吃过这个亏——测试时一切正常,结果某次现场设备意外断电重启,触发了自动格式化,所有配置参数归零,设备直接变砖。后来我改成了手动格式化,才彻底解决这个问题。
2.3 格式化 SPIFFS
除了自动格式化,你也可以手动调用格式化函数。这在调试阶段特别有用,比如你想清空所有文件,重新开始。
// 手动格式化 SPIFFS
if (SPIFFS.format()) {
Serial.println("SPIFFS 格式化成功!");
} else {
Serial.println("SPIFFS 格式化失败!");
}
格式化操作会擦除整个 SPIFFS 分区,速度取决于分区大小。对于 1MB 的分区,通常只需要几百毫秒。嗯,这里要注意:格式化期间不要断电,否则可能损坏 Flash 的坏块管理表。
2.4 文件读写基础操作
SPIFFS 的文件操作接口和标准 C 的 fopen、fread、fwrite 非常相似。如果你写过 PC 上的文件操作,上手会很快。
2.4.1 写入文件
// 写入文件
File file = SPIFFS.open("/config.txt", FILE_WRITE);
if (!file) {
Serial.println("打开文件失败!");
return;
}
// 写入内容
file.println("ssid=MyWiFi");
file.println("password=12345678");
file.println("mode=AP");
// 关闭文件
file.close();
Serial.println("文件写入完成!");
这里有个细节:FILE_WRITE 模式会 覆盖 原有内容。如果你只想追加内容,可以用 FILE_APPEND。我刚开始用的时候,就犯过这个错——每次写入都把之前的配置覆盖了,还以为是 SPIFFS 坏了。
2.4.2 读取文件
// 读取文件
File file = SPIFFS.open("/config.txt", FILE_READ);
if (!file) {
Serial.println("打开文件失败!");
return;
}
// 逐行读取
while (file.available()) {
String line = file.readStringUntil('\n');
line.trim(); // 去掉换行符和空格
Serial.println(line);
}
// 关闭文件
file.close();
读取时,我习惯用 readStringUntil('\n') 逐行处理,这样方便解析键值对。如果你存的是二进制数据,比如图片或固件包,那就用 file.read() 按字节读取。
2.4.3 检查文件是否存在
if (SPIFFS.exists("/config.txt")) {
Serial.println("配置文件存在");
} else {
Serial.println("配置文件不存在,将使用默认配置");
}
这个函数在启动时检查配置文件是否存在,非常实用。比如第一次上电时,配置文件不存在,就自动创建并写入默认值。
2.4.4 删除文件
if (SPIFFS.remove("/config.txt")) {
Serial.println("文件删除成功");
} else {
Serial.println("文件删除失败");
}
删除操作不可逆,用之前最好确认一下。我在做日志轮转功能时,会先检查文件数量,超过阈值就删除最旧的日志文件。
2.5 常用 API 速查表
| 函数 | 功能 | 返回值 |
|---|---|---|
SPIFFS.begin(bool formatOnFail) |
挂载 SPIFFS 文件系统 | true 成功,false 失败 |
SPIFFS.format() |
格式化 SPIFFS 分区 | true 成功,false 失败 |
SPIFFS.open(path, mode) |
打开文件 | File 对象,失败返回空 |
SPIFFS.exists(path) |
检查文件是否存在 | true 存在,false 不存在 |
SPIFFS.remove(path) |
删除文件 | true 成功,false 失败 |
SPIFFS.rename(path1, path2) |
重命名文件 | true 成功,false 失败 |
SPIFFS.info(FSInfo) |
获取分区信息(总大小、已用空间等) | true 成功,false 失败 |
2.6 获取分区信息
有时候你需要知道 SPIFFS 分区还剩多少空间,比如在写入大文件之前做检查。可以用 SPIFFS.info() 来获取。
FSInfo fs_info;
SPIFFS.info(fs_info);
Serial.print("总空间: ");
Serial.print(fs_info.totalBytes);
Serial.println(" bytes");
Serial.print("已用空间: ");
Serial.print(fs_info.usedBytes);
Serial.println(" bytes");
Serial.print("剩余空间: ");
Serial.print(fs_info.totalBytes - fs_info.usedBytes);
Serial.println(" bytes");
这个信息在调试时很有用。我记得有一次,客户反馈设备无法保存配置,我远程一看,SPIFFS 分区已经写满了,日志文件把空间全占了。从那以后,我每个项目都会加一个空间监控,低于阈值就自动清理旧日志。
2.7 SPIFFS 操作流程图
下面这张图总结了 SPIFFS 的典型操作流程,从初始化到文件读写,再到资源释放。你可以把它贴在工位旁边,写代码时对照着看。
2.8 避坑指南
最后,分享几个我踩过的坑,希望能帮你少走弯路。
技巧 1:路径格式——SPIFFS 的路径必须以 / 开头,比如 /config.txt,而不是 config.txt。这个细节很容易忽略,但写错了就是打不开文件。
技巧 2:文件描述符限制——SPIFFS 同时打开的文件数量有限,默认是 4 个。如果你同时打开多个文件,记得及时关闭,否则会耗尽文件描述符,导致后续操作失败。
警告:分区大小——SPIFFS 分区的大小必须在 partitions.csv 中正确配置。如果分区太小,存几个文件就满了;如果分区太大,会浪费 Flash 空间。我一般给 SPIFFS 分配 1MB 到 2MB,具体看项目需求。