2. SPIFFS 文件操作基础:打开、读写、追加、删除与重命名

SPIFFS 是 ESP32 上最常用的文件系统之一。说白了,它就是把 Flash 里的一块区域模拟成一个小硬盘。你可以在上面存网页、存配置、存日志。我最早接触它的时候,觉得不就是文件读写嘛,结果踩了不少坑。今天咱们就把这些基础操作一个一个捋清楚。

本章核心:掌握 SPIFFS 的 6 个基本操作——打开、关闭、写入、读取、追加、删除与重命名。这些是后续所有 Web 服务器文件操作的基础。

2.1 SPIFFS 文件系统初始化

操作文件之前,必须先挂载文件系统。就像你插 U 盘之前,系统得先识别它。

#include <SPIFFS.h>

void setup() {
  Serial.begin(115200);
  
  if (!SPIFFS.begin(true)) {  // true 表示如果挂载失败则格式化
    Serial.println("SPIFFS 挂载失败!");
    return;
  }
  Serial.println("SPIFFS 挂载成功");
}

这里有个细节:SPIFFS.begin(true) 里的 true 参数。我个人习惯在开发阶段用 true,这样万一文件系统坏了,它会自动格式化。但产品发布时,我建议改成 false,避免意外格式化把用户数据清掉。

注意:SPIFFS 格式化会擦除整个 SPIFFS 分区。我曾经在调试时不小心反复格式化,把 Flash 的擦写寿命消耗了不少。量产设备上,非必要不要开自动格式化。

2.2 文件打开与关闭

SPIFFS 的文件操作和标准 C 语言很像。打开文件用 SPIFFS.open(),关闭用 file.close()。但有个坑——你打开的文件必须记得关,否则会泄漏文件句柄。

File file = SPIFFS.open("/config.txt", "r");  // 只读模式
if (!file) {
  Serial.println("文件打开失败");
  return;
}
// 读文件操作...
file.close();  // 一定要关!

打开模式有几种:

模式 含义 文件不存在时
"r" 只读 返回 NULL
"w" 写入(覆盖) 创建新文件
"a" 追加 创建新文件
"r+" 读写 返回 NULL
"w+" 读写(覆盖) 创建新文件
"a+" 读写(追加) 创建新文件

经验之谈:我习惯用 "r" 和 "w" 就够了。读写混合模式 "r+" 容易搞混文件指针位置,新手慎用。

2.3 写入数据

写入数据用 file.print()、file.println() 或 file.write()。我最常用的是 println(),因为它会自动加换行,读日志时方便。

File file = SPIFFS.open("/log.txt", "w");
if (!file) {
  Serial.println("打开文件失败");
  return;
}

file.println("传感器数据:温度 25.3°C");
file.println("传感器数据:湿度 68.2%");
file.printf("时间戳:%lu\n", millis());

file.close();
Serial.println("数据写入完成");

你想想看,如果每次写入都打开关闭文件,效率其实不高。但 SPIFFS 的设计就是这样——它适合小文件、低频写入。高频写入的话,我建议用缓冲区攒一批再写。

避坑:SPIFFS 不支持并发写入。如果你在多个任务里同时写同一个文件,数据会乱。我曾经在项目中用 FreeRTOS 的两个任务同时写日志,结果文件内容全是乱码。后来加了个互斥锁才解决。

2.4 读取数据

读取数据的方法也很多。你可以按字符读、按行读、或者一次性全读进来。

File file = SPIFFS.open("/log.txt", "r");
if (!file) {
  Serial.println("文件打开失败");
  return;
}

// 方法1:按行读取
while (file.available()) {
  String line = file.readStringUntil('\n');
  Serial.println(line);
}

// 方法2:一次性读取全部内容
// file.seek(0);  // 回到文件开头
// String content = file.readString();
// Serial.println(content);

file.close();

我个人更推荐按行读取。为什么呢?因为 SPIFFS 的 Flash 读取虽然快,但一次性读大文件会占用大量 RAM。ESP32 的 RAM 本来就紧张,你想想看,一个 100KB 的 HTML 文件读进来,堆空间就去掉一大块。

小技巧:用 file.size() 可以提前知道文件大小。如果文件太大,就分段读取,别一口气吞下去。

2.5 文件追加模式

追加模式 "a" 和写入模式 "w" 的区别,说白了就是一个保留原有内容,一个清空重写。追加模式特别适合写日志。

File file = SPIFFS.open("/log.txt", "a");  // 追加模式
if (!file) {
  Serial.println("打开文件失败");
  return;
}

file.println("新记录:系统重启");
file.close();

嗯,这里要注意:追加模式下,文件指针自动定位到末尾。你不需要手动 seek()。但如果你用 "a+" 模式,读取时文件指针也在末尾,得先 seek(0) 才能读到内容。

2.6 文件删除与重命名

删除文件用 SPIFFS.remove(),重命名用 SPIFFS.rename()。这两个操作都很直接,但有个细节——删除前最好确认文件存在。

// 删除文件
if (SPIFFS.exists("/old_config.txt")) {
  if (SPIFFS.remove("/old_config.txt")) {
    Serial.println("文件删除成功");
  } else {
    Serial.println("文件删除失败");
  }
}

// 重命名文件
if (SPIFFS.rename("/temp.txt", "/final.txt")) {
  Serial.println("文件重命名成功");
} else {
  Serial.println("文件重命名失败");
}

我曾经犯过一个错误:在删除文件后,没有检查返回值就直接进行下一步操作。结果文件没删掉,后续写入就出问题了。所以,永远检查返回值。

重要提醒:SPIFFS 不支持目录操作。所有文件都在根目录下,路径里不能有子目录。比如 "/data/log.txt" 这种路径,SPIFFS 会当成一个名字叫 data/log.txt 的文件,而不是目录下的文件。这一点和 SD 卡完全不同。

2.7 本章知识结构图

下面这张图把 SPIFFS 文件操作的流程串起来了。你可以把它当作操作手册来用。

SPIFFS 文件操作流程 SPIFFS.begin() 挂载成功? 打印错误并返回 选择文件操作 写入 / 追加 读取 删除 / 重命名 file.close() 操作完成 ⚠ 每次操作后检查返回值,确保成功

2.8 综合示例:一个简单的配置管理器

把上面的操作串起来,写一个简单的配置读写工具。这个工具我在好几个项目里都用过,改改就能用。

#include <SPIFFS.h>

const char* configFile = "/config.txt";

// 保存配置
bool saveConfig(String key, String value) {
  File file = SPIFFS.open(configFile, "a");  // 追加模式
  if (!file) return false;
  
  file.println(key + "=" + value);
  file.close();
  return true;
}

// 读取配置
String readConfig(String key) {
  File file = SPIFFS.open(configFile, "r");
  if (!file) return "";
  
  while (file.available()) {
    String line = file.readStringUntil('\n');
    int eqPos = line.indexOf('=');
    if (eqPos > 0) {
      String k = line.substring(0, eqPos);
      k.trim();
      if (k == key) {
        String v = line.substring(eqPos + 1);
        v.trim();
        file.close();
        return v;
      }
    }
  }
  file.close();
  return "";
}

// 删除配置(重写文件,跳过指定key)
bool deleteConfig(String key) {
  File readFile = SPIFFS.open(configFile, "r");
  if (!readFile) return false;
  
  // 先读到内存
  String allContent = "";
  while (readFile.available()) {
    String line = readFile.readStringUntil('\n');
    int eqPos = line.indexOf('=');
    if (eqPos > 0) {
      String k = line.substring(0, eqPos);
      k.trim();
      if (k == key) continue;  // 跳过要删除的
    }
    allContent += line + "\n";
  }
  readFile.close();
  
  // 重新写入
  File writeFile = SPIFFS.open(configFile, "w");
  if (!writeFile) return false;
  writeFile.print(allContent);
  writeFile.close();
  return true;
}

void setup() {
  Serial.begin(115200);
  SPIFFS.begin(true);
  
  // 测试
  saveConfig("ssid", "MyWiFi");
  saveConfig("password", "12345678");
  
  Serial.println("读取 ssid: " + readConfig("ssid"));
  
  deleteConfig("password");
  Serial.println("删除后读取 password: " + readConfig("password"));
}

实用建议:这个配置管理器虽然简单,但够用。如果你需要更复杂的配置管理,可以考虑用 JSON 格式。不过对于大多数 IoT 项目,key=value 的格式已经足够了。