4. SPIFFS 高级操作:文件流、二进制读写与 JSON 解析
SPIFFS 的基本文件操作(打开、关闭、读写字符串)其实不难。但真正到了项目里,你会发现光会这些远远不够。我最早用 SPIFFS 存传感器数据时,就踩过二进制读写的坑——存进去的数据读出来全是乱码。后来才明白,文件流操作和二进制处理才是 SPIFFS 的硬核技能。
这一章,咱们就专门啃这些硬骨头。我会把文件流、二进制读写、JSON 解析和配置文件实战串起来讲。你跟着走一遍,以后 SPIFFS 相关的需求基本都能搞定。
4.1 文件流操作:大文件不慌
先问个问题:如果你要往 SPIFFS 里写一个 100KB 的日志文件,你会怎么做?
新手可能会这样:
File file = SPIFFS.open("/log.txt", "w");
file.print(bigString); // 一次性写入大字符串
file.close();
这写法在 ESP32 上很容易崩。为什么?因为 print() 内部会先把整个字符串塞进缓冲区,内存不够就直接重启。我遇到过好几次,排查了半天才发现是缓冲区溢出。
正确的做法是用文件流——分块读写,每次只处理一小段数据。
核心思路:文件流操作的本质是“边读边处理”或“边写边生成”,不依赖一次性加载全部数据。
看个例子,用流的方式逐行读取文件:
File file = SPIFFS.open("/config.txt", "r");
if (!file) {
Serial.println("文件打开失败");
return;
}
while (file.available()) {
String line = file.readStringUntil('\n');
line.trim(); // 去掉换行和首尾空格
if (line.length() > 0) {
Serial.println("读到一行: " + line);
}
}
file.close();
这里用了 readStringUntil('\n'),每次只读一行。内存占用只有一行的大小,哪怕文件有 1MB 也不怕。
写文件也可以用流的方式:
File file = SPIFFS.open("/data.log", "a"); // 追加模式
if (!file) return;
for (int i = 0; i < 1000; i++) {
String line = "记录 #" + String(i) + ": " + String(millis()) + "\n";
file.print(line); // 逐行写入,不攒缓冲区
}
file.close();
我个人习惯在写日志时用追加模式 + 逐行写入。这样即使中途断电,已写入的数据也不会丢。
| 操作方式 | 适用场景 | 内存占用 | 可靠性 |
|---|---|---|---|
| 一次性读写 | 小文件(< 4KB) | 高 | 低(易丢数据) |
| 流式读写 | 大文件、日志、配置 | 低 | 高(逐行持久化) |
4.2 二进制文件读写:存啥都行
文本文件虽然方便,但存数值型数据时效率很低。比如你要存一个 float 类型的传感器读数,转成字符串再存,既占空间又费时间。二进制文件直接存原始字节,又快又省。
写二进制文件用 write() 方法,它接受 uint8_t* 指针和长度:
struct SensorData {
float temperature;
float humidity;
uint32_t timestamp;
};
SensorData data = {25.6, 68.3, millis()};
File file = SPIFFS.open("/sensor.bin", "w");
if (file) {
file.write((uint8_t*)&data, sizeof(data));
file.close();
Serial.println("二进制数据写入成功");
}
读回来也很直接:
SensorData readData;
File file = SPIFFS.open("/sensor.bin", "r");
if (file) {
file.read((uint8_t*)&readData, sizeof(readData));
file.close();
Serial.printf("温度: %.1f, 湿度: %.1f, 时间戳: %u\n",
readData.temperature, readData.humidity, readData.timestamp);
}
注意:二进制文件在不同平台间移植时,要注意结构体对齐和字节序问题。ESP32 是小端模式,如果文件要在 PC 上解析,可能需要做字节序转换。
我曾经在项目里用二进制格式存储 1000 个传感器采样点。如果用文本存,文件大小约 50KB;用二进制存,只有 12KB。读取速度也从 200ms 降到了 30ms。差距很明显。
4.3 JSON 文件解析:配置管理的标配
二进制虽然高效,但可读性差。配置文件这种东西,还是 JSON 最合适。ESP32 上解析 JSON 我推荐用 ArduinoJson 库,稳定且功能全。
先看一个典型的 JSON 配置文件:
{
"wifi": {
"ssid": "MyHomeWiFi",
"password": "12345678"
},
"mqtt": {
"broker": "mqtt.example.com",
"port": 1883,
"topic": "sensor/data"
},
"sensor": {
"interval": 5000,
"threshold": 30.5
}
}
解析这个文件,我一般分三步走:
- 读取文件到字符串——用流的方式读入内存
- 反序列化——用
deserializeJson()解析 - 提取数据——按路径取值
代码实现:
#include <ArduinoJson.h>
// 从 SPIFFS 读取并解析 JSON 配置文件
bool loadConfig(const char* path) {
File file = SPIFFS.open(path, "r");
if (!file) {
Serial.println("配置文件不存在");
return false;
}
// 第一步:读取文件内容
String content = file.readString();
file.close();
// 第二步:反序列化
DynamicJsonDocument doc(2048);
DeserializationError error = deserializeJson(doc, content);
if (error) {
Serial.print("JSON 解析失败: ");
Serial.println(error.c_str());
return false;
}
// 第三步:提取数据
String ssid = doc["wifi"]["ssid"].as<String>();
String password = doc["wifi"]["password"].as<String>();
int port = doc["mqtt"]["port"].as<int>();
float threshold = doc["sensor"]["threshold"].as<float>();
Serial.printf("WiFi: %s, MQTT 端口: %d, 阈值: %.1f\n",
ssid.c_str(), port, threshold);
return true;
}
小技巧:DynamicJsonDocument 的大小要预估准确。我一般按 JSON 文件最大可能大小的 2 倍来分配。如果解析失败,先检查是不是缓冲区太小了。
4.4 配置文件读写实战:一个完整的例子
理论说完了,咱们来一个完整的实战。假设你要做一个温湿度监测设备,配置参数存在 SPIFFS 里,支持读取和更新。
先定义配置结构体:
struct AppConfig {
char ssid[32];
char password[32];
char mqttBroker[64];
uint16_t mqttPort;
uint32_t sampleInterval; // 采样间隔,单位 ms
float alertThreshold; // 告警阈值
};
保存配置到 JSON 文件:
bool saveConfig(const AppConfig& config) {
DynamicJsonDocument doc(1024);
doc["wifi"]["ssid"] = config.ssid;
doc["wifi"]["password"] = config.password;
doc["mqtt"]["broker"] = config.mqttBroker;
doc["mqtt"]["port"] = config.mqttPort;
doc["sensor"]["interval"] = config.sampleInterval;
doc["sensor"]["threshold"] = config.alertThreshold;
File file = SPIFFS.open("/config.json", "w");
if (!file) return false;
serializeJson(doc, file);
file.close();
return true;
}
读取配置:
bool loadConfig(AppConfig& config) {
File file = SPIFFS.open("/config.json", "r");
if (!file) return false;
DynamicJsonDocument doc(1024);
DeserializationError err = deserializeJson(doc, file);
file.close();
if (err) return false;
strlcpy(config.ssid, doc["wifi"]["ssid"] | "default", sizeof(config.ssid));
strlcpy(config.password, doc["wifi"]["password"] | "", sizeof(config.password));
strlcpy(config.mqttBroker, doc["mqtt"]["broker"] | "mqtt.local", sizeof(config.mqttBroker));
config.mqttPort = doc["mqtt"]["port"] | 1883;
config.sampleInterval = doc["sensor"]["interval"] | 5000;
config.alertThreshold = doc["sensor"]["threshold"] | 30.0;
return true;
}
你看,用 | 运算符可以设置默认值。这样即使 JSON 文件里缺了某个字段,程序也不会崩溃。
实战建议:每次修改配置后,先调用 saveConfig() 写入 SPIFFS,再调用 loadConfig() 读回来验证一遍。我吃过亏——写入时没检查返回值,结果配置丢了,设备连不上网,排查了一整天。
4.5 本章知识结构图
下面这张图把 SPIFFS 高级操作的核心脉络画出来了。你可以对照着回顾一下:
从图里能看出来,文件流是基础,二进制和 JSON 是两种不同的存储策略,而配置文件实战是把它们串起来用的完整方案。你掌握了这些,SPIFFS 这块基本就通了。
我的经验:在实际项目中,我通常把“不变的数据”用二进制存(比如校准参数、固件版本),把“用户可配置的数据”用 JSON 存(比如 WiFi 设置、告警阈值)。这样既保证了效率,又保留了可读性。