4、SPIFFS文件系统:从原理到实战

文件系统,说白了就是让单片机也能像电脑一样管理文件。ESP32 跑 SPIFFS,我用了好几年,踩过坑也填过坑。今天咱们把这套东西彻底讲透。

4.1 SPIFFS 原理与特性

SPIFFS 全称是 SPI Flash File System,专门为嵌入式设备设计。它跑在 SPI Flash 上,不是 SD 卡,也不是 EEPROM。

它的核心特性,我列个表,你一看就明白:

特性 说明 我的评价
轻量级 代码体积小,RAM 占用低 适合资源受限场景
掉电安全 写入时掉电不会损坏已有文件 这个很关键,我吃过亏
磨损均衡 自动分散写入位置,延长 Flash 寿命 不是完美均衡,但够用
目录支持 支持多级目录,但扁平化效率更高 别建太深,三层以内
最大文件数 默认约 32 个文件句柄 够用,但别滥用

为什么会选 SPIFFS 而不是 FAT?因为 FAT 需要额外缓冲区,而且对 Flash 的磨损均衡做得不好。SPIFFS 是专门为 Flash 优化的,说白了就是更懂 Flash 的脾气。

核心原理一句话:SPIFFS 把 Flash 分成多个逻辑块,每个块又分成页。写入时先找空闲页,如果块满了就擦除整个块。这种设计让写入和擦除次数尽量平均。

4.2 SPIFFS 分区配置

分区配置是第一步,也是最容易出错的一步。我记得第一次用 SPIFFS 时,分区表写错了,折腾了一下午才找到原因。

ESP32 的分区表在 partitions.csv 里定义。一个典型配置长这样:

# Name,   Type, SubType, Offset,  Size,   Flags
nvs,      data, nvs,     0x9000,  0x5000,
otadata,  data, ota,     0xe000,  0x2000,
app0,     app,  ota_0,   0x10000, 0x1E0000,
spiffs,   data, spiffs,  0x1F0000,0x100000,

注意几个关键点:

  • Type 必须是 data,SubType 必须是 spiffs
  • Offset 和 Size 要对齐 Flash 的扇区大小(通常是 4KB)
  • Size 不要超过 Flash 剩余空间,否则编译报错

我个人习惯给 SPIFFS 分配 1MB 空间。太小了存不了几个网页文件,太大了又浪费。你想想看,一个 HTML 页面加几个 CSS 和 JS,500KB 左右就够用了。

避坑指南:我曾经把 SPIFFS 分区设得太大,结果 OTA 分区不够用,导致无法远程升级。建议先算好总 Flash 大小,再分配各分区。

4.3 文件读写操作

读写文件是基本功。ESP32 的 SPIFFS API 和标准 C 的 fopen/fread/fwrite 很像,但有几个坑要注意。

先看一个完整的读写示例:

#include <SPIFFS.h>

void setup() {
  Serial.begin(115200);
  
  // 挂载 SPIFFS
  if(!SPIFFS.begin(true)){
    Serial.println("SPIFFS 挂载失败");
    return;
  }
  
  // 写文件
  File file = SPIFFS.open("/config.txt", FILE_WRITE);
  if(!file){
    Serial.println("打开文件失败");
    return;
  }
  file.println("ssid=MyWiFi");
  file.println("password=12345678");
  file.close();
  
  // 读文件
  file = SPIFFS.open("/config.txt", FILE_READ);
  if(!file){
    Serial.println("打开文件失败");
    return;
  }
  while(file.available()){
    Serial.write(file.read());
  }
  file.close();
}

这里有几个细节,我特别想强调:

  • SPIFFS.begin(true) 里的 true 表示自动格式化。第一次用或者分区损坏时,这个参数能救命。
  • 文件路径必须以 / 开头,比如 /config.txt,不是 config.txt。
  • 写完一定要 close(),否则数据可能没刷进 Flash。

小技巧:如果你要频繁写小数据,比如传感器读数,建议用 NVS 而不是 SPIFFS。SPIFFS 适合存大文件,比如网页、图片、配置文件。

4.4 目录管理

SPIFFS 支持目录,但别指望它能像 Windows 那样高效。我建议目录层级不超过三层,否则遍历起来很慢。

常用的目录操作:

// 创建目录
SPIFFS.mkdir("/www");

// 创建子目录
SPIFFS.mkdir("/www/css");
SPIFFS.mkdir("/www/js");

// 列出目录内容
File root = SPIFFS.open("/www");
File file = root.openNextFile();
while(file){
  Serial.print("文件名: ");
  Serial.println(file.name());
  Serial.print("大小: ");
  Serial.println(file.size());
  file = root.openNextFile();
}

// 删除目录
SPIFFS.rmdir("/www/css");

嗯,这里要注意:rmdir() 只能删除空目录。如果目录里有文件,得先删文件再删目录。我曾经写了个脚本,忘了先删文件,结果目录删不掉,排查了半天。

下面这张图展示了 SPIFFS 的目录结构和文件操作流程:

SPIFFS 目录结构与操作流程 / (根目录) /www /config /data /www/css /www/js style.css app.js wifi.conf mkdir() openNextFile() rmdir() remove()

从图里能看出来,SPIFFS 的目录结构其实很直观。操作流程就是:先挂载,再打开,读写完记得关闭。

4.5 避坑与最佳实践

最后,我总结几个实战中容易踩的坑:

  • 不要频繁写入:Flash 有擦写寿命,大概 10 万次。频繁写日志会快速消耗寿命。
  • 文件句柄有限:默认 32 个,用完就报错。记得及时 close()。
  • 路径大小写敏感:/Config.txt 和 /config.txt 是两个文件。
  • 不要存二进制大文件:SPIFFS 适合小文件,超过 1MB 的文件建议用 SD 卡。

我的习惯:每次写完文件后,调用 SPIFFS.info() 检查剩余空间。这样能提前发现空间不足的问题,避免写入失败。

好了,SPIFFS 的核心内容就这些。记住:挂载、读写、关闭,三步走。别偷懒,别省那几行代码。