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
  }
}

解析这个文件,我一般分三步走:

  1. 读取文件到字符串——用流的方式读入内存
  2. 反序列化——用 deserializeJson() 解析
  3. 提取数据——按路径取值

代码实现:

#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 高级操作的核心脉络画出来了。你可以对照着回顾一下:

SPIFFS 高级操作知识结构 SPIFFS 高级操作 文件流操作 逐行读写 / 追加模式 大文件分块处理 二进制文件读写 write() / read() 原始字节 结构体直接存取 JSON 文件解析 ArduinoJson 库 deserializeJson / serializeJson 配置文件读写实战 结构体 + JSON 双向转换 默认值保护 / 写入验证 目标:高效、可靠、可维护的存储方案

从图里能看出来,文件流是基础,二进制和 JSON 是两种不同的存储策略,而配置文件实战是把它们串起来用的完整方案。你掌握了这些,SPIFFS 这块基本就通了。

我的经验:在实际项目中,我通常把“不变的数据”用二进制存(比如校准参数、固件版本),把“用户可配置的数据”用 JSON 存(比如 WiFi 设置、告警阈值)。这样既保证了效率,又保留了可读性。