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 的典型操作流程,从初始化到文件读写,再到资源释放。你可以把它贴在工位旁边,写代码时对照着看。

SPIFFS 操作流程图 SPIFFS 典型操作流程 开始 SPIFFS.begin(true) 挂载成功? 打印错误并返回 挂载成功 文件读写操作 file.close() 结束 ⚠️ 重要提醒 • 每次操作后检查返回值 • 用完文件必须 close() • 避免频繁擦写同一区域

2.8 避坑指南

最后,分享几个我踩过的坑,希望能帮你少走弯路。

技巧 1:路径格式——SPIFFS 的路径必须以 / 开头,比如 /config.txt,而不是 config.txt。这个细节很容易忽略,但写错了就是打不开文件。

技巧 2:文件描述符限制——SPIFFS 同时打开的文件数量有限,默认是 4 个。如果你同时打开多个文件,记得及时关闭,否则会耗尽文件描述符,导致后续操作失败。

警告:分区大小——SPIFFS 分区的大小必须在 partitions.csv 中正确配置。如果分区太小,存几个文件就满了;如果分区太大,会浪费 Flash 空间。我一般给 SPIFFS 分配 1MB 到 2MB,具体看项目需求。