4、文件系统与存储:SPIFFS / LittleFS 文件系统介绍、读写配置文件、固件分区表设计
做嵌入式OTA升级,说白了就是跟存储打交道。你想想看,新固件下载下来放哪儿?配置参数存哪儿?掉电了怎么恢复?这些问题都绕不开文件系统和分区表。今天我就把这块内容掰开了讲清楚。
4.1 为什么需要文件系统?
很多刚入行的朋友会问:我直接操作Flash地址读写不就行了?嗯,理论上可以。但实际项目中你会发现,直接操作地址有几个痛点:
- 地址管理麻烦:今天存个WiFi密码,明天存个传感器阈值,后天又要存个校准参数。每次都得算地址偏移,一不小心就覆盖了。
- 数据碎片化:Flash擦写有寿命限制,频繁擦除同一块区域会加速老化。
- 移植性差:换个Flash芯片,地址映射全得重写。
文件系统就是来解决这些问题的。它帮你管理存储空间,提供类似PC上的文件操作接口——打开、读写、关闭、删除。你只需要关心文件名,不用管底层地址。
核心观点:在嵌入式OTA中,文件系统不是必需品,但用了它,你的代码会干净很多,维护成本也低。
4.2 SPIFFS vs LittleFS:怎么选?
目前ESP32、ESP8266这类芯片上,主流就两个文件系统:SPIFFS和LittleFS。我两个都用过,说说我的感受。
| 特性 | SPIFFS | LittleFS |
|---|---|---|
| 诞生时间 | 较早(2015年左右) | 较新(2018年左右) |
| 掉电安全 | 一般,可能丢文件 | 强,有日志机制 |
| 磨损均衡 | 有,但较弱 | 较好 |
| 目录支持 | 扁平结构,无目录 | 支持目录 |
| 内存占用 | 较小 | 稍大 |
| 社区活跃度 | 逐渐减少 | 越来越主流 |
我的建议:新项目直接用LittleFS。为什么?我在一个灌溉控制器项目里吃过SPIFFS的亏——设备突然断电,配置文件损坏了,导致第二天灌溉时间全乱套。换成LittleFS后,再没出过这种问题。
小技巧:如果你还在用ESP8266,Flash只有2MB,SPIFFS可能更省空间。但ESP32起步4MB,LittleFS完全够用。
4.3 读写配置文件实战
配置文件是OTA升级里最常用的功能。比如设备ID、服务器地址、升级间隔这些参数,都得存下来。我习惯用JSON格式,可读性好,解析也方便。
下面是一个完整的读写示例,基于LittleFS和ArduinoJson库:
#include <LittleFS.h>
#include <ArduinoJson.h>
// 配置文件路径
const char* configPath = "/config.json";
// 配置结构体
struct Config {
char deviceId[32];
char serverUrl[128];
int updateInterval; // 单位:小时
float latitude;
float longitude;
};
// 保存配置
bool saveConfig(const Config& cfg) {
// 我习惯先检查文件系统是否挂载
if (!LittleFS.begin()) {
Serial.println("文件系统挂载失败!");
return false;
}
File file = LittleFS.open(configPath, "w");
if (!file) {
Serial.println("打开配置文件失败");
return false;
}
// 构建JSON文档
StaticJsonDocument<256> doc;
doc["deviceId"] = cfg.deviceId;
doc["serverUrl"] = cfg.serverUrl;
doc["updateInterval"] = cfg.updateInterval;
doc["latitude"] = cfg.latitude;
doc["longitude"] = cfg.longitude;
// 序列化并写入
if (serializeJson(doc, file) == 0) {
Serial.println("JSON写入失败");
file.close();
return false;
}
file.close();
Serial.println("配置保存成功");
return true;
}
// 读取配置
bool loadConfig(Config& cfg) {
if (!LittleFS.begin()) {
Serial.println("文件系统挂载失败!");
return false;
}
if (!LittleFS.exists(configPath)) {
Serial.println("配置文件不存在,使用默认值");
return false;
}
File file = LittleFS.open(configPath, "r");
if (!file) {
Serial.println("打开配置文件失败");
return false;
}
StaticJsonDocument<256> doc;
DeserializationError error = deserializeJson(doc, file);
if (error) {
Serial.print("JSON解析失败:");
Serial.println(error.c_str());
file.close();
return false;
}
// 提取数据
strlcpy(cfg.deviceId, doc["deviceId"] | "default_id", sizeof(cfg.deviceId));
strlcpy(cfg.serverUrl, doc["serverUrl"] | "http://192.168.1.100", sizeof(cfg.serverUrl));
cfg.updateInterval = doc["updateInterval"] | 24;
cfg.latitude = doc["latitude"] | 0.0;
cfg.longitude = doc["longitude"] | 0.0;
file.close();
return true;
}
注意:JSON文档大小要提前算好。StaticJsonDocument是静态分配,太大容易栈溢出。如果配置项多,改用DynamicJsonDocument。
实际项目中,我还会加一个版本号字段。这样以后配置结构变了,可以自动迁移。比如旧版本没有经纬度字段,读取时给个默认值就行。
4.4 固件分区表设计
分区表是OTA升级的骨架。没有合理的分区设计,升级过程很容易出问题。我见过有人把固件和配置文件放在同一个分区,结果升级时把配置也覆盖了——这种坑踩一次就够了。
以ESP32为例,典型的OTA分区表长这样:
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xe000, 0x2000,
app0, app, ota_0, 0x10000, 0x1F0000,
app1, app, ota_1, 0x200000,0x1F0000,
spiffs, data, spiffs, 0x3F0000,0x10000,
coredump, data, coredump,0x400000,0x10000,
我来解释一下每个分区的作用:
- nvs:非易失性存储,存WiFi密码、校准参数等小数据。我一般不用它存配置文件,因为大小有限。
- otadata:OTA启动信息,记录当前该从哪个app分区启动。这个分区千万别动,动了就变砖。
- app0 / app1:两个固件分区,大小一样。一个跑当前固件,一个用来放新固件。这就是OTA的核心——双区备份。
- spiffs:文件系统分区,存配置文件、日志、临时文件等。
- coredump:崩溃转储,调试用。量产时可以去掉,省点空间。
设计原则:app0和app1大小必须一致。我曾经因为app1比app0小了1KB,升级后启动失败,查了两天才发现是分区表配错了。
分区大小怎么定?我一般这样算:
- 固件编译后多大?比如800KB,那每个app分区至少1MB,留点余量。
- 文件系统需要多大?配置文件一般几KB,日志如果存得多,可以给256KB。
- 总Flash多大?比如4MB,减去bootloader和otadata,剩下的合理分配。
举个例子,一个4MB Flash的ESP32:
| 分区 | 大小 | 说明 |
|---|---|---|
| bootloader | 64KB | 启动引导 |
| nvs | 20KB | 系统参数 |
| otadata | 8KB | OTA状态 |
| app0 | 1.5MB | 当前固件 |
| app1 | 1.5MB | 备份/升级用 |
| spiffs | ~900KB | 文件系统 |
这样分配,固件有1.5MB空间,一般够用。文件系统有900KB,存配置文件绰绰有余,还能存点日志。
经验之谈:分区表文件(partitions.csv)要放在项目根目录,并且在编译时指定。我习惯在platformio.ini里加一行:board_build.partitions = partitions.csv
4.5 避坑指南
最后分享几个我踩过的坑:
- 文件系统挂载失败:有时候Flash没初始化好,或者分区表没烧录。解决办法是上电后延迟几百毫秒再挂载。
- 配置文件写一半掉电:LittleFS虽然比SPIFFS好,但也不是100%安全。我习惯写完后读回来校验一下。
- 分区表烧录错误:用esptool.py烧录时,记得加上--erase-region参数,把旧分区表擦干净。
嗯,文件系统和分区表这块,说白了就是给固件找个安稳的家。设计好了,OTA升级就成功了一半。下一章我们聊聊升级流程的具体实现。