跳转至

SPIFFS文件系统实战:ESP32 Flash存储的轻量级解决方案

学习目标

完成本教程后,你将能够:

  • 理解SPIFFS的设计原理和核心特性
  • 掌握SPIFFS在ESP32上的配置和使用
  • 熟练使用SPIFFS的API进行文件操作
  • 理解SPIFFS的磨损均衡机制
  • 掌握SPIFFS的性能优化方法
  • 能够处理SPIFFS的常见问题
  • 完成一个完整的Flash存储项目
  • 了解SPIFFS与其他文件系统的区别

前置要求

在开始学习之前,建议你具备:

知识要求: - 熟悉C/C++编程 - 了解Flash存储器的基本特性 - 理解文件系统的基本概念 - 掌握ESP32开发基础 - 了解Arduino或ESP-IDF框架

技能要求: - 能够编写ESP32程序 - 会使用Arduino IDE或PlatformIO - 熟悉基本的文件操作 - 能够使用串口调试工具 - 了解内存管理

开发环境: - ESP32开发板(ESP32-DevKitC或类似) - Arduino IDE 或 ESP-IDF - USB数据线 - 串口调试工具 - PlatformIO(可选)

SPIFFS概述

什么是SPIFFS

SPIFFS(Serial Peripheral Interface Flash File System)是一个专为嵌入式系统设计的轻量级文件系统,特别适合用于SPI Flash存储器。它由Peter Andersson开发并开源,在ESP8266和ESP32等平台上得到广泛应用。

核心特性

  1. 轻量级设计
  2. 代码量小,资源占用少
  3. 适合资源受限的嵌入式系统
  4. RAM占用可配置
  5. 无需外部依赖

  6. 磨损均衡

  7. 自动分散写入操作
  8. 延长Flash使用寿命
  9. 动态块分配
  10. 智能垃圾回收

  11. 简单易用

  12. API简洁直观
  13. 类POSIX接口
  14. 易于集成
  15. 文档完善

  16. Flash优化

  17. 针对Flash特性优化
  18. 支持坏块管理
  19. 高效的空间利用
  20. 快速文件访问

SPIFFS架构

系统架构图

┌─────────────────────────────────────────────────┐
│    应用层 (Application)                          │
│    文件读写、配置管理                             │
└─────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│    SPIFFS API层                                  │
│    SPIFFS_open, SPIFFS_read, SPIFFS_write...    │
└─────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│    SPIFFS核心层                                  │
│    ┌──────────────┐  ┌──────────────┐          │
│    │ 文件管理     │  │ 页管理       │          │
│    └──────────────┘  └──────────────┘          │
│    ┌──────────────┐  ┌──────────────┐          │
│    │ 块分配器     │  │ 磨损均衡     │          │
│    └──────────────┘  └──────────────┘          │
│    ┌──────────────┐  ┌──────────────┐          │
│    │ 垃圾回收     │  │ 缓存管理     │          │
│    └──────────────┘  └──────────────┘          │
└─────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│    Flash驱动层                                   │
│    SPI Flash读写、擦除操作                       │
└─────────────────────────────────────────────────┘

SPIFFS vs 其他文件系统

特性 SPIFFS LittleFS FAT
掉电安全 ⚠️ 部分支持 ✅ 完全支持 ❌ 不支持
磨损均衡 ✅ 支持 ✅ 自动 ❌ 无
RAM占用 极低 中等
ROM占用
目录支持 ❌ 不支持 ✅ 完整支持 ✅ 支持
文件属性 ⚠️ 有限 ✅ 支持 ✅ 支持
性能 中等 中等
适用场景 小型Flash 嵌入式Flash SD卡、U盘

选择建议: - 简单配置存储 → SPIFFS - 需要掉电保护 → LittleFS - 需要与PC交互 → FAT - 需要目录结构 → LittleFS或FAT

SPIFFS的限制: - 不支持目录(所有文件在根目录) - 不支持文件权限 - 掉电保护有限 - 文件名长度限制(通常32字节)

准备工作

硬件准备

名称 数量 说明 参考型号
ESP32开发板 1 带内置Flash ESP32-DevKitC
USB数据线 1 Type-C或Micro-USB -
电脑 1 开发和调试 -

软件准备

必需软件: - Arduino IDE 1.8.x 或更高版本 - ESP32开发板支持包 - 串口驱动(CP2102或CH340) - 串口调试助手

可选工具: - PlatformIO IDE - ESP-IDF开发框架 - SPIFFS文件上传工具

环境配置

Arduino IDE配置

  1. 安装ESP32支持包

    文件 → 首选项 → 附加开发板管理器网址
    添加:https://dl.espressif.com/dl/package_esp32_index.json
    

  2. 安装开发板

    工具 → 开发板 → 开发板管理器
    搜索"ESP32"并安装
    

  3. 选择开发板

    工具 → 开发板 → ESP32 Dev Module
    

  4. 配置Flash参数

    工具 → Flash Size → 4MB (32Mb)
    工具 → Partition Scheme → Default 4MB with spiffs
    

步骤1:初始化SPIFFS

1.1 基本初始化

创建一个新的Arduino项目,编写初始化代码:

#include "FS.h"
#include "SPIFFS.h"

// 格式化SPIFFS标志
#define FORMAT_SPIFFS_IF_FAILED true

void setup() {
    // 初始化串口
    Serial.begin(115200);
    delay(1000);

    Serial.println("SPIFFS Test Starting...");

    // 初始化SPIFFS
    if (!SPIFFS.begin(FORMAT_SPIFFS_IF_FAILED)) {
        Serial.println("SPIFFS Mount Failed");
        return;
    }

    Serial.println("SPIFFS Mounted Successfully");

    // 获取SPIFFS信息
    printSPIFFSInfo();
}

void loop() {
    // 主循环
}

/**
 * @brief  打印SPIFFS信息
 */
void printSPIFFSInfo() {
    size_t total = SPIFFS.totalBytes();
    size_t used = SPIFFS.usedBytes();

    Serial.println("=== SPIFFS Information ===");
    Serial.printf("Total space: %u bytes\n", total);
    Serial.printf("Used space:  %u bytes\n", used);
    Serial.printf("Free space:  %u bytes\n", total - used);
    Serial.printf("Usage:       %.1f%%\n", (used * 100.0) / total);
    Serial.println("==========================");
}

代码说明: - SPIFFS.begin(): 挂载SPIFFS文件系统 - FORMAT_SPIFFS_IF_FAILED: 如果挂载失败则格式化 - totalBytes(): 获取总空间 - usedBytes(): 获取已用空间

预期输出

SPIFFS Test Starting...
SPIFFS Mounted Successfully
=== SPIFFS Information ===
Total space: 1507328 bytes
Used space:  0 bytes
Free space:  1507328 bytes
Usage:       0.0%
==========================

1.2 手动格式化

如果需要手动格式化SPIFFS:

/**
 * @brief  格式化SPIFFS
 */
void formatSPIFFS() {
    Serial.println("Formatting SPIFFS...");

    if (SPIFFS.format()) {
        Serial.println("SPIFFS formatted successfully");
    } else {
        Serial.println("SPIFFS format failed");
    }
}

void setup() {
    Serial.begin(115200);
    delay(1000);

    // 挂载SPIFFS
    if (!SPIFFS.begin(false)) {
        Serial.println("SPIFFS Mount Failed, formatting...");
        formatSPIFFS();

        // 重新挂载
        if (!SPIFFS.begin(false)) {
            Serial.println("SPIFFS Mount Failed after format");
            return;
        }
    }

    Serial.println("SPIFFS Ready");
}

步骤2:文件操作实战

2.1 创建和写入文件

/**
 * @brief  写入文件示例
 */
void writeFile(const char *path, const char *message) {
    Serial.printf("Writing file: %s\n", path);

    // 打开文件(写入模式)
    File file = SPIFFS.open(path, FILE_WRITE);
    if (!file) {
        Serial.println("Failed to open file for writing");
        return;
    }

    // 写入数据
    if (file.print(message)) {
        Serial.println("File written successfully");
    } else {
        Serial.println("Write failed");
    }

    // 关闭文件
    file.close();
}

void setup() {
    Serial.begin(115200);
    SPIFFS.begin(true);

    // 写入文本文件
    writeFile("/hello.txt", "Hello, SPIFFS!\n");

    // 写入配置文件
    writeFile("/config.txt", "wifi_ssid=MyWiFi\nwifi_pass=12345678\n");
}

文件打开模式: - FILE_READ: 只读模式 - FILE_WRITE: 写入模式(覆盖) - FILE_APPEND: 追加模式

2.2 读取文件

/**
 * @brief  读取文件示例
 */
void readFile(const char *path) {
    Serial.printf("Reading file: %s\n", path);

    // 打开文件(只读模式)
    File file = SPIFFS.open(path, FILE_READ);
    if (!file) {
        Serial.println("Failed to open file for reading");
        return;
    }

    Serial.println("File content:");
    Serial.println("---");

    // 读取并打印文件内容
    while (file.available()) {
        Serial.write(file.read());
    }

    Serial.println("---");

    // 关闭文件
    file.close();
}

void setup() {
    Serial.begin(115200);
    SPIFFS.begin(true);

    // 读取文件
    readFile("/hello.txt");
}

预期输出

Reading file: /hello.txt
File content:
---
Hello, SPIFFS!
---

2.3 追加写入

/**
 * @brief  追加写入示例
 */
void appendFile(const char *path, const char *message) {
    Serial.printf("Appending to file: %s\n", path);

    // 打开文件(追加模式)
    File file = SPIFFS.open(path, FILE_APPEND);
    if (!file) {
        Serial.println("Failed to open file for appending");
        return;
    }

    // 追加数据
    if (file.print(message)) {
        Serial.println("Message appended");
    } else {
        Serial.println("Append failed");
    }

    file.close();
}

void setup() {
    Serial.begin(115200);
    SPIFFS.begin(true);

    // 创建日志文件
    writeFile("/log.txt", "=== System Log ===\n");

    // 追加日志条目
    appendFile("/log.txt", "[INFO] System started\n");
    appendFile("/log.txt", "[INFO] WiFi connecting...\n");
    appendFile("/log.txt", "[INFO] WiFi connected\n");

    // 读取日志
    readFile("/log.txt");
}

2.4 删除文件

/**
 * @brief  删除文件示例
 */
void deleteFile(const char *path) {
    Serial.printf("Deleting file: %s\n", path);

    if (SPIFFS.remove(path)) {
        Serial.println("File deleted");
    } else {
        Serial.println("Delete failed");
    }
}

void setup() {
    Serial.begin(115200);
    SPIFFS.begin(true);

    // 创建测试文件
    writeFile("/test.txt", "This is a test file");

    // 删除文件
    deleteFile("/test.txt");

    // 尝试读取(应该失败)
    readFile("/test.txt");
}

2.5 重命名文件

/**
 * @brief  重命名文件示例
 */
void renameFile(const char *path1, const char *path2) {
    Serial.printf("Renaming file %s to %s\n", path1, path2);

    if (SPIFFS.rename(path1, path2)) {
        Serial.println("File renamed");
    } else {
        Serial.println("Rename failed");
    }
}

void setup() {
    Serial.begin(115200);
    SPIFFS.begin(true);

    // 创建文件
    writeFile("/old_name.txt", "Test content");

    // 重命名
    renameFile("/old_name.txt", "/new_name.txt");

    // 读取新文件
    readFile("/new_name.txt");
}

2.6 检查文件是否存在

/**
 * @brief  检查文件是否存在
 */
bool fileExists(const char *path) {
    return SPIFFS.exists(path);
}

void setup() {
    Serial.begin(115200);
    SPIFFS.begin(true);

    // 检查文件
    if (fileExists("/config.txt")) {
        Serial.println("Config file exists");
        readFile("/config.txt");
    } else {
        Serial.println("Config file not found, creating...");
        writeFile("/config.txt", "default_config=true\n");
    }
}

步骤3:文件列表和信息

3.1 列出所有文件

/**
 * @brief  列出SPIFFS中的所有文件
 */
void listFiles() {
    Serial.println("=== File List ===");

    // 打开根目录
    File root = SPIFFS.open("/");
    if (!root) {
        Serial.println("Failed to open directory");
        return;
    }

    if (!root.isDirectory()) {
        Serial.println("Not a directory");
        return;
    }

    // 遍历文件
    File file = root.openNextFile();
    while (file) {
        if (file.isDirectory()) {
            Serial.printf("  DIR : %s\n", file.name());
        } else {
            Serial.printf("  FILE: %-20s  SIZE: %6d bytes\n", 
                         file.name(), file.size());
        }
        file = root.openNextFile();
    }

    Serial.println("=================");
}

void setup() {
    Serial.begin(115200);
    SPIFFS.begin(true);

    // 创建一些测试文件
    writeFile("/file1.txt", "Content 1");
    writeFile("/file2.txt", "Content 2");
    writeFile("/config.json", "{\"key\":\"value\"}");

    // 列出所有文件
    listFiles();
}

预期输出

=== File List ===
  FILE: /file1.txt          SIZE:     10 bytes
  FILE: /file2.txt          SIZE:     10 bytes
  FILE: /config.json        SIZE:     17 bytes
=================

3.2 获取文件信息

/**
 * @brief  获取文件详细信息
 */
void getFileInfo(const char *path) {
    File file = SPIFFS.open(path, FILE_READ);
    if (!file) {
        Serial.println("Failed to open file");
        return;
    }

    Serial.println("=== File Information ===");
    Serial.printf("Name: %s\n", file.name());
    Serial.printf("Size: %d bytes\n", file.size());
    Serial.printf("Position: %d\n", file.position());
    Serial.println("========================");

    file.close();
}

void setup() {
    Serial.begin(115200);
    SPIFFS.begin(true);

    writeFile("/test.txt", "Hello, World!");
    getFileInfo("/test.txt");
}

步骤4:二进制文件操作

4.1 写入二进制数据

/**
 * @brief  写入二进制数据示例
 */
void writeBinaryFile(const char *path) {
    Serial.printf("Writing binary file: %s\n", path);

    File file = SPIFFS.open(path, FILE_WRITE);
    if (!file) {
        Serial.println("Failed to open file");
        return;
    }

    // 定义数据结构
    struct SensorData {
        uint32_t timestamp;
        float temperature;
        float humidity;
        uint16_t pressure;
    };

    // 创建数据
    SensorData data;
    data.timestamp = millis();
    data.temperature = 25.5;
    data.humidity = 60.2;
    data.pressure = 1013;

    // 写入二进制数据
    size_t written = file.write((uint8_t*)&data, sizeof(data));
    Serial.printf("Written %d bytes\n", written);

    file.close();
}

void setup() {
    Serial.begin(115200);
    SPIFFS.begin(true);

    writeBinaryFile("/sensor.dat");
}

4.2 读取二进制数据

/**
 * @brief  读取二进制数据示例
 */
void readBinaryFile(const char *path) {
    Serial.printf("Reading binary file: %s\n", path);

    File file = SPIFFS.open(path, FILE_READ);
    if (!file) {
        Serial.println("Failed to open file");
        return;
    }

    // 定义数据结构
    struct SensorData {
        uint32_t timestamp;
        float temperature;
        float humidity;
        uint16_t pressure;
    };

    // 读取二进制数据
    SensorData data;
    size_t read_size = file.read((uint8_t*)&data, sizeof(data));

    if (read_size == sizeof(data)) {
        Serial.println("=== Sensor Data ===");
        Serial.printf("Timestamp:   %u\n", data.timestamp);
        Serial.printf("Temperature: %.1f°C\n", data.temperature);
        Serial.printf("Humidity:    %.1f%%\n", data.humidity);
        Serial.printf("Pressure:    %u hPa\n", data.pressure);
        Serial.println("===================");
    } else {
        Serial.println("Failed to read data");
    }

    file.close();
}

void setup() {
    Serial.begin(115200);
    SPIFFS.begin(true);

    writeBinaryFile("/sensor.dat");
    readBinaryFile("/sensor.dat");
}

步骤5:配置文件管理

5.1 JSON配置文件

#include <ArduinoJson.h>

/**
 * @brief  保存JSON配置
 */
void saveConfig() {
    Serial.println("Saving config...");

    // 创建JSON文档
    StaticJsonDocument<200> doc;
    doc["wifi_ssid"] = "MyWiFi";
    doc["wifi_password"] = "12345678";
    doc["mqtt_server"] = "192.168.1.100";
    doc["mqtt_port"] = 1883;
    doc["device_name"] = "ESP32_Device";

    // 打开文件
    File file = SPIFFS.open("/config.json", FILE_WRITE);
    if (!file) {
        Serial.println("Failed to open config file");
        return;
    }

    // 序列化JSON到文件
    if (serializeJson(doc, file) == 0) {
        Serial.println("Failed to write config");
    } else {
        Serial.println("Config saved");
    }

    file.close();
}

/**
 * @brief  加载JSON配置
 */
void loadConfig() {
    Serial.println("Loading config...");

    // 打开文件
    File file = SPIFFS.open("/config.json", FILE_READ);
    if (!file) {
        Serial.println("Config file not found");
        return;
    }

    // 解析JSON
    StaticJsonDocument<200> doc;
    DeserializationError error = deserializeJson(doc, file);

    if (error) {
        Serial.println("Failed to parse config");
        file.close();
        return;
    }

    // 读取配置
    const char* wifi_ssid = doc["wifi_ssid"];
    const char* wifi_password = doc["wifi_password"];
    const char* mqtt_server = doc["mqtt_server"];
    int mqtt_port = doc["mqtt_port"];
    const char* device_name = doc["device_name"];

    Serial.println("=== Configuration ===");
    Serial.printf("WiFi SSID:    %s\n", wifi_ssid);
    Serial.printf("WiFi Password: %s\n", wifi_password);
    Serial.printf("MQTT Server:  %s\n", mqtt_server);
    Serial.printf("MQTT Port:    %d\n", mqtt_port);
    Serial.printf("Device Name:  %s\n", device_name);
    Serial.println("=====================");

    file.close();
}

void setup() {
    Serial.begin(115200);
    SPIFFS.begin(true);

    // 保存配置
    saveConfig();

    // 加载配置
    loadConfig();
}

5.2 简单键值对配置

/**
 * @brief  保存键值对配置
 */
void saveKeyValue(const char *key, const char *value) {
    String filename = String("/") + key + ".txt";
    File file = SPIFFS.open(filename.c_str(), FILE_WRITE);
    if (file) {
        file.print(value);
        file.close();
        Serial.printf("Saved: %s = %s\n", key, value);
    }
}

/**
 * @brief  读取键值对配置
 */
String loadKeyValue(const char *key) {
    String filename = String("/") + key + ".txt";
    File file = SPIFFS.open(filename.c_str(), FILE_READ);
    if (!file) {
        return "";
    }

    String value = file.readString();
    file.close();
    return value;
}

void setup() {
    Serial.begin(115200);
    SPIFFS.begin(true);

    // 保存配置
    saveKeyValue("wifi_ssid", "MyWiFi");
    saveKeyValue("wifi_pass", "12345678");
    saveKeyValue("device_id", "ESP32_001");

    // 读取配置
    String ssid = loadKeyValue("wifi_ssid");
    String pass = loadKeyValue("wifi_pass");
    String device_id = loadKeyValue("device_id");

    Serial.println("=== Configuration ===");
    Serial.printf("WiFi SSID: %s\n", ssid.c_str());
    Serial.printf("WiFi Pass: %s\n", pass.c_str());
    Serial.printf("Device ID: %s\n", device_id.c_str());
    Serial.println("=====================");
}

步骤6:数据记录应用

6.1 传感器数据记录器

/**
 * @brief  传感器数据记录器
 */
class DataLogger {
private:
    File logFile;
    String currentLogFile;
    int maxLogSize;
    int logCount;

public:
    DataLogger(int maxSize = 10000) : maxLogSize(maxSize), logCount(0) {}

    /**
     * @brief  开始记录
     */
    bool begin() {
        // 生成日志文件名
        currentLogFile = "/log_" + String(millis()) + ".csv";

        // 打开文件
        logFile = SPIFFS.open(currentLogFile.c_str(), FILE_WRITE);
        if (!logFile) {
            Serial.println("Failed to create log file");
            return false;
        }

        // 写入CSV头
        logFile.println("Timestamp,Temperature,Humidity,Pressure");
        Serial.printf("Log file created: %s\n", currentLogFile.c_str());

        return true;
    }

    /**
     * @brief  记录数据
     */
    void log(float temperature, float humidity, int pressure) {
        if (!logFile) {
            Serial.println("Log file not open");
            return;
        }

        // 检查文件大小
        if (logFile.size() >= maxLogSize) {
            Serial.println("Log file full, creating new file");
            logFile.close();
            begin();
        }

        // 写入数据
        logFile.printf("%lu,%.2f,%.2f,%d\n", 
                      millis(), temperature, humidity, pressure);
        logCount++;

        // 定期刷新
        if (logCount % 10 == 0) {
            logFile.flush();
        }
    }

    /**
     * @brief  结束记录
     */
    void end() {
        if (logFile) {
            logFile.close();
            Serial.printf("Log closed. Total entries: %d\n", logCount);
        }
    }

    /**
     * @brief  读取日志
     */
    void readLog(const char *filename) {
        File file = SPIFFS.open(filename, FILE_READ);
        if (!file) {
            Serial.println("Failed to open log file");
            return;
        }

        Serial.println("=== Log Content ===");
        while (file.available()) {
            Serial.write(file.read());
        }
        Serial.println("===================");

        file.close();
    }
};

DataLogger logger;

void setup() {
    Serial.begin(115200);
    SPIFFS.begin(true);

    // 开始记录
    logger.begin();

    // 模拟传感器数据记录
    for (int i = 0; i < 20; i++) {
        float temp = 20.0 + random(0, 100) / 10.0;
        float humid = 50.0 + random(0, 200) / 10.0;
        int press = 1000 + random(0, 50);

        logger.log(temp, humid, press);
        Serial.printf("Logged: T=%.1f H=%.1f P=%d\n", temp, humid, press);

        delay(1000);
    }

    // 结束记录
    logger.end();

    // 列出所有日志文件
    listFiles();
}

void loop() {
    // 空循环
}

步骤7:Web服务器文件托管

7.1 托管HTML文件

#include <WiFi.h>
#include <WebServer.h>

const char* ssid = "YourWiFi";
const char* password = "YourPassword";

WebServer server(80);

/**
 * @brief  处理根路径请求
 */
void handleRoot() {
    File file = SPIFFS.open("/index.html", FILE_READ);
    if (!file) {
        server.send(404, "text/plain", "File not found");
        return;
    }

    server.streamFile(file, "text/html");
    file.close();
}

/**
 * @brief  处理CSS文件请求
 */
void handleCSS() {
    File file = SPIFFS.open("/style.css", FILE_READ);
    if (!file) {
        server.send(404, "text/plain", "File not found");
        return;
    }

    server.streamFile(file, "text/css");
    file.close();
}

/**
 * @brief  处理JavaScript文件请求
 */
void handleJS() {
    File file = SPIFFS.open("/script.js", FILE_READ);
    if (!file) {
        server.send(404, "text/plain", "File not found");
        return;
    }

    server.streamFile(file, "application/javascript");
    file.close();
}

void setup() {
    Serial.begin(115200);
    SPIFFS.begin(true);

    // 创建示例HTML文件
    File file = SPIFFS.open("/index.html", FILE_WRITE);
    if (file) {
        file.println("<!DOCTYPE html>");
        file.println("<html>");
        file.println("<head>");
        file.println("  <title>ESP32 SPIFFS Demo</title>");
        file.println("  <link rel='stylesheet' href='/style.css'>");
        file.println("</head>");
        file.println("<body>");
        file.println("  <h1>Hello from SPIFFS!</h1>");
        file.println("  <p>This page is served from ESP32 Flash memory.</p>");
        file.println("  <script src='/script.js'></script>");
        file.println("</body>");
        file.println("</html>");
        file.close();
    }

    // 创建CSS文件
    file = SPIFFS.open("/style.css", FILE_WRITE);
    if (file) {
        file.println("body { font-family: Arial; margin: 20px; }");
        file.println("h1 { color: #333; }");
        file.close();
    }

    // 创建JS文件
    file = SPIFFS.open("/script.js", FILE_WRITE);
    if (file) {
        file.println("console.log('Hello from SPIFFS!');");
        file.close();
    }

    // 连接WiFi
    WiFi.begin(ssid, password);
    Serial.print("Connecting to WiFi");
    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
    }
    Serial.println();
    Serial.print("Connected! IP: ");
    Serial.println(WiFi.localIP());

    // 配置Web服务器
    server.on("/", handleRoot);
    server.on("/style.css", handleCSS);
    server.on("/script.js", handleJS);

    // 启动服务器
    server.begin();
    Serial.println("HTTP server started");
}

void loop() {
    server.handleClient();
}

7.2 文件上传功能

/**
 * @brief  处理文件上传
 */
void handleFileUpload() {
    HTTPUpload& upload = server.upload();

    if (upload.status == UPLOAD_FILE_START) {
        String filename = "/" + upload.filename;
        Serial.printf("Upload Start: %s\n", filename.c_str());

        // 打开文件准备写入
        File file = SPIFFS.open(filename, FILE_WRITE);
        if (!file) {
            Serial.println("Failed to open file for writing");
            return;
        }
    } else if (upload.status == UPLOAD_FILE_WRITE) {
        // 写入数据
        File file = SPIFFS.open("/" + upload.filename, FILE_APPEND);
        if (file) {
            file.write(upload.buf, upload.currentSize);
            file.close();
        }
    } else if (upload.status == UPLOAD_FILE_END) {
        Serial.printf("Upload End: %s (%u bytes)\n", 
                     upload.filename.c_str(), upload.totalSize);
    }
}

void setup() {
    // ... 前面的初始化代码 ...

    // 添加上传处理
    server.on("/upload", HTTP_POST, []() {
        server.send(200, "text/plain", "Upload complete");
    }, handleFileUpload);

    server.begin();
}

步骤8:性能优化

8.1 缓存优化

/**
 * @brief  使用缓存读取文件
 */
class CachedFileReader {
private:
    File file;
    uint8_t buffer[512];
    int bufferSize;
    int bufferPos;

public:
    bool open(const char *path) {
        file = SPIFFS.open(path, FILE_READ);
        if (!file) {
            return false;
        }
        bufferSize = 0;
        bufferPos = 0;
        return true;
    }

    int read() {
        // 如果缓冲区为空,填充缓冲区
        if (bufferPos >= bufferSize) {
            bufferSize = file.read(buffer, sizeof(buffer));
            bufferPos = 0;

            if (bufferSize == 0) {
                return -1;  // EOF
            }
        }

        return buffer[bufferPos++];
    }

    void close() {
        if (file) {
            file.close();
        }
    }
};

void testCachedRead() {
    CachedFileReader reader;

    unsigned long start = millis();

    if (reader.open("/large_file.txt")) {
        int count = 0;
        while (reader.read() != -1) {
            count++;
        }
        reader.close();

        unsigned long elapsed = millis() - start;
        Serial.printf("Read %d bytes in %lu ms\n", count, elapsed);
    }
}

8.2 批量写入优化

/**
 * @brief  批量写入优化
 */
class BufferedFileWriter {
private:
    File file;
    uint8_t buffer[512];
    int bufferPos;

public:
    bool open(const char *path) {
        file = SPIFFS.open(path, FILE_WRITE);
        if (!file) {
            return false;
        }
        bufferPos = 0;
        return true;
    }

    void write(uint8_t data) {
        buffer[bufferPos++] = data;

        // 缓冲区满时写入文件
        if (bufferPos >= sizeof(buffer)) {
            flush();
        }
    }

    void flush() {
        if (bufferPos > 0 && file) {
            file.write(buffer, bufferPos);
            bufferPos = 0;
        }
    }

    void close() {
        flush();
        if (file) {
            file.close();
        }
    }
};

void testBufferedWrite() {
    BufferedFileWriter writer;

    unsigned long start = millis();

    if (writer.open("/test_write.txt")) {
        for (int i = 0; i < 10000; i++) {
            writer.write('A' + (i % 26));
        }
        writer.close();

        unsigned long elapsed = millis() - start;
        Serial.printf("Wrote 10000 bytes in %lu ms\n", elapsed);
    }
}

8.3 性能测试

/**
 * @brief  性能测试
 */
void performanceTest() {
    const int TEST_SIZE = 10000;
    uint8_t testData[TEST_SIZE];

    // 准备测试数据
    for (int i = 0; i < TEST_SIZE; i++) {
        testData[i] = i % 256;
    }

    Serial.println("=== Performance Test ===");

    // 写入测试
    unsigned long start = millis();
    File file = SPIFFS.open("/perf_test.dat", FILE_WRITE);
    if (file) {
        file.write(testData, TEST_SIZE);
        file.close();
    }
    unsigned long writeTime = millis() - start;

    // 读取测试
    start = millis();
    file = SPIFFS.open("/perf_test.dat", FILE_READ);
    if (file) {
        file.read(testData, TEST_SIZE);
        file.close();
    }
    unsigned long readTime = millis() - start;

    // 计算速度
    float writeSpeed = (TEST_SIZE / 1024.0) / (writeTime / 1000.0);
    float readSpeed = (TEST_SIZE / 1024.0) / (readTime / 1000.0);

    Serial.printf("Write: %lu ms (%.2f KB/s)\n", writeTime, writeSpeed);
    Serial.printf("Read:  %lu ms (%.2f KB/s)\n", readTime, readSpeed);
    Serial.println("========================");
}

步骤9:故障排除

问题1:挂载失败

现象

SPIFFS Mount Failed

可能原因: - SPIFFS未格式化 - Flash分区配置错误 - Flash硬件故障

解决方法

  1. 尝试格式化SPIFFS

    if (!SPIFFS.begin(true)) {  // true = 格式化
        Serial.println("Still failed after format");
    }
    

  2. 检查分区表配置

    工具 → Partition Scheme → Default 4MB with spiffs
    

  3. 手动格式化

    SPIFFS.format();
    

问题2:文件写入失败

现象

Failed to open file for writing
Write failed

可能原因: - 空间不足 - 文件名非法 - 权限问题

解决方法

  1. 检查可用空间

    size_t total = SPIFFS.totalBytes();
    size_t used = SPIFFS.usedBytes();
    Serial.printf("Free: %u bytes\n", total - used);
    

  2. 删除不需要的文件

    SPIFFS.remove("/old_file.txt");
    

  3. 检查文件名

    // 文件名必须以 / 开头
    SPIFFS.open("/file.txt", FILE_WRITE);  // ✅ 正确
    SPIFFS.open("file.txt", FILE_WRITE);   // ❌ 错误
    

问题3:文件读取为空

现象: - 文件存在但读取不到内容 - file.available() 返回0

可能原因: - 文件未正确关闭 - 文件指针位置错误 - 文件确实为空

解决方法

  1. 确保文件正确关闭

    File file = SPIFFS.open("/test.txt", FILE_WRITE);
    file.print("Hello");
    file.close();  // 必须关闭
    

  2. 重置文件指针

    File file = SPIFFS.open("/test.txt", FILE_READ);
    file.seek(0);  // 回到开头
    

  3. 检查文件大小

    File file = SPIFFS.open("/test.txt", FILE_READ);
    Serial.printf("File size: %d\n", file.size());
    

问题4:空间不足

现象

SPIFFS: mount failed, -10025

可能原因: - 文件过多 - 碎片过多 - 分区太小

解决方法

  1. 清理旧文件

    void cleanupOldFiles() {
        File root = SPIFFS.open("/");
        File file = root.openNextFile();
    
        while (file) {
            // 删除超过7天的文件
            if (isOlderThan(file.name(), 7)) {
                SPIFFS.remove(file.name());
            }
            file = root.openNextFile();
        }
    }
    

  2. 重新格式化

    SPIFFS.format();
    SPIFFS.begin();
    

  3. 增大SPIFFS分区

    工具 → Partition Scheme → 选择更大的SPIFFS分区
    

深入理解

SPIFFS工作原理

页和块结构

SPIFFS将Flash划分为逻辑页和物理块:

Flash存储器
┌─────────────────────────────────────┐
│ Block 0                             │
│  ┌─────┬─────┬─────┬─────┬─────┐   │
│  │Page0│Page1│Page2│Page3│Page4│   │
│  └─────┴─────┴─────┴─────┴─────┘   │
├─────────────────────────────────────┤
│ Block 1                             │
│  ┌─────┬─────┬─────┬─────┬─────┐   │
│  │Page5│Page6│Page7│Page8│Page9│   │
│  └─────┴─────┴─────┴─────┴─────┘   │
├─────────────────────────────────────┤
│ ...                                 │
└─────────────────────────────────────┘

关键概念: - 逻辑页(Logical Page): 通常256字节,存储数据的基本单位 - 物理块(Physical Block): 通常4KB,擦除的基本单位 - 对象索引(Object Index): 记录文件的元数据和页索引

磨损均衡机制

SPIFFS通过以下方式实现磨损均衡:

  1. 动态块分配
  2. 写入时选择擦除次数最少的块
  3. 避免某些块过度使用

  4. 垃圾回收

  5. 定期整理碎片
  6. 回收已删除文件的空间
  7. 重新分配数据

  8. 块轮换

  9. 定期移动数据到不同的块
  10. 平衡各块的使用次数

性能考虑

读写性能

影响因素: - 页大小配置 - 缓存大小 - Flash芯片速度 - 文件碎片程度

优化建议: 1. 使用缓冲区批量读写 2. 避免频繁的小文件操作 3. 定期进行垃圾回收 4. 合理配置页大小

空间利用率

空间开销: - 元数据开销:约10-15% - 磨损均衡开销:约5-10% - 实际可用空间:约75-85%

优化方法: - 删除不需要的文件 - 压缩数据后存储 - 使用二进制格式而非文本

最佳实践

文件命名规范

// ✅ 推荐的文件名
"/config.json"
"/data/sensor_001.dat"
"/logs/2024_01_15.log"

// ❌ 避免的文件名
"config.json"           // 缺少前导斜杠
"/very_long_filename_that_exceeds_limit.txt"  // 太长
"/file with spaces.txt" // 包含空格

错误处理

/**
 * @brief  安全的文件操作
 */
bool safeWriteFile(const char *path, const char *data) {
    // 检查空间
    size_t dataSize = strlen(data);
    size_t freeSpace = SPIFFS.totalBytes() - SPIFFS.usedBytes();

    if (dataSize > freeSpace) {
        Serial.println("Not enough space");
        return false;
    }

    // 打开文件
    File file = SPIFFS.open(path, FILE_WRITE);
    if (!file) {
        Serial.println("Failed to open file");
        return false;
    }

    // 写入数据
    size_t written = file.print(data);
    file.close();

    if (written != dataSize) {
        Serial.println("Write incomplete");
        SPIFFS.remove(path);  // 删除不完整的文件
        return false;
    }

    return true;
}

定期维护

/**
 * @brief  文件系统维护
 */
void maintainFileSystem() {
    // 1. 检查空间使用
    size_t total = SPIFFS.totalBytes();
    size_t used = SPIFFS.usedBytes();
    float usage = (used * 100.0) / total;

    Serial.printf("SPIFFS usage: %.1f%%\n", usage);

    // 2. 如果使用率超过80%,清理旧文件
    if (usage > 80.0) {
        Serial.println("Cleaning up old files...");
        cleanupOldFiles();
    }

    // 3. 如果使用率超过95%,警告
    if (usage > 95.0) {
        Serial.println("WARNING: SPIFFS almost full!");
    }
}

完整示例项目

项目:IoT数据记录系统

实现一个完整的IoT数据记录系统,包含传感器数据采集、存储、Web查看和数据导出功能。

#include <WiFi.h>
#include <WebServer.h>
#include <ArduinoJson.h>
#include "FS.h"
#include "SPIFFS.h"

// WiFi配置
const char* ssid = "YourWiFi";
const char* password = "YourPassword";

// Web服务器
WebServer server(80);

// 数据记录器类
class IoTDataLogger {
private:
    String currentLogFile;
    int maxLogSize;
    int logCount;

public:
    IoTDataLogger(int maxSize = 50000) : maxLogSize(maxSize), logCount(0) {}

    /**
     * @brief  初始化记录器
     */
    bool begin() {
        if (!SPIFFS.begin(true)) {
            Serial.println("SPIFFS Mount Failed");
            return false;
        }

        // 创建新日志文件
        createNewLogFile();
        return true;
    }

    /**
     * @brief  创建新日志文件
     */
    void createNewLogFile() {
        // 生成文件名:log_timestamp.csv
        currentLogFile = "/log_" + String(millis()) + ".csv";

        File file = SPIFFS.open(currentLogFile.c_str(), FILE_WRITE);
        if (file) {
            // 写入CSV头
            file.println("Timestamp,Temperature,Humidity,Pressure,Light");
            file.close();
            Serial.printf("Created log file: %s\n", currentLogFile.c_str());
        }
    }

    /**
     * @brief  记录传感器数据
     */
    void logData(float temp, float humid, int pressure, int light) {
        File file = SPIFFS.open(currentLogFile.c_str(), FILE_APPEND);
        if (!file) {
            Serial.println("Failed to open log file");
            return;
        }

        // 检查文件大小
        if (file.size() >= maxLogSize) {
            file.close();
            Serial.println("Log file full, creating new file");
            createNewLogFile();
            file = SPIFFS.open(currentLogFile.c_str(), FILE_APPEND);
        }

        // 写入数据
        file.printf("%lu,%.2f,%.2f,%d,%d\n", 
                   millis(), temp, humid, pressure, light);
        file.close();

        logCount++;

        if (logCount % 10 == 0) {
            Serial.printf("Logged %d entries\n", logCount);
        }
    }

    /**
     * @brief  获取所有日志文件列表
     */
    String getLogFileList() {
        StaticJsonDocument<1024> doc;
        JsonArray files = doc.createNestedArray("files");

        File root = SPIFFS.open("/");
        File file = root.openNextFile();

        while (file) {
            if (strstr(file.name(), "log_") != NULL) {
                JsonObject fileObj = files.createNestedObject();
                fileObj["name"] = String(file.name());
                fileObj["size"] = file.size();
            }
            file = root.openNextFile();
        }

        String output;
        serializeJson(doc, output);
        return output;
    }

    /**
     * @brief  读取日志文件
     */
    String readLogFile(const char *filename) {
        File file = SPIFFS.open(filename, FILE_READ);
        if (!file) {
            return "File not found";
        }

        String content = file.readString();
        file.close();
        return content;
    }

    /**
     * @brief  删除日志文件
     */
    bool deleteLogFile(const char *filename) {
        return SPIFFS.remove(filename);
    }

    /**
     * @brief  获取系统信息
     */
    String getSystemInfo() {
        StaticJsonDocument<200> doc;

        doc["total_space"] = SPIFFS.totalBytes();
        doc["used_space"] = SPIFFS.usedBytes();
        doc["free_space"] = SPIFFS.totalBytes() - SPIFFS.usedBytes();
        doc["log_count"] = logCount;
        doc["current_log"] = currentLogFile;

        String output;
        serializeJson(doc, output);
        return output;
    }
};

IoTDataLogger logger;

/**
 * @brief  模拟传感器读取
 */
struct SensorData {
    float temperature;
    float humidity;
    int pressure;
    int light;
};

SensorData readSensors() {
    SensorData data;
    data.temperature = 20.0 + random(0, 150) / 10.0;
    data.humidity = 40.0 + random(0, 400) / 10.0;
    data.pressure = 980 + random(0, 60);
    data.light = random(0, 1024);
    return data;
}

/**
 * @brief  Web服务器处理函数
 */

// 主页
void handleRoot() {
    String html = R"(
<!DOCTYPE html>
<html>
<head>
    <title>IoT Data Logger</title>
    <meta charset='utf-8'>
    <style>
        body { font-family: Arial; margin: 20px; background: #f0f0f0; }
        .container { max-width: 800px; margin: 0 auto; background: white; padding: 20px; border-radius: 8px; }
        h1 { color: #333; }
        .info-box { background: #e8f4f8; padding: 15px; border-radius: 5px; margin: 10px 0; }
        .file-list { list-style: none; padding: 0; }
        .file-item { background: #f9f9f9; padding: 10px; margin: 5px 0; border-radius: 3px; }
        button { background: #4CAF50; color: white; padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; }
        button:hover { background: #45a049; }
        .delete-btn { background: #f44336; }
        .delete-btn:hover { background: #da190b; }
    </style>
</head>
<body>
    <div class='container'>
        <h1>IoT Data Logger</h1>
        <div class='info-box' id='info'>Loading...</div>
        <h2>Log Files</h2>
        <div id='files'>Loading...</div>
        <button onclick='refreshData()'>Refresh</button>
    </div>
    <script>
        function refreshData() {
            fetch('/api/info').then(r => r.json()).then(data => {
                document.getElementById('info').innerHTML = 
                    `<strong>Total Space:</strong> ${data.total_space} bytes<br>
                     <strong>Used Space:</strong> ${data.used_space} bytes<br>
                     <strong>Free Space:</strong> ${data.free_space} bytes<br>
                     <strong>Log Count:</strong> ${data.log_count}<br>
                     <strong>Current Log:</strong> ${data.current_log}`;
            });

            fetch('/api/files').then(r => r.json()).then(data => {
                let html = '<ul class="file-list">';
                data.files.forEach(file => {
                    html += `<li class="file-item">
                        ${file.name} (${file.size} bytes)
                        <button onclick="viewFile('${file.name}')">View</button>
                        <button class="delete-btn" onclick="deleteFile('${file.name}')">Delete</button>
                    </li>`;
                });
                html += '</ul>';
                document.getElementById('files').innerHTML = html;
            });
        }

        function viewFile(filename) {
            window.open('/api/view?file=' + filename, '_blank');
        }

        function deleteFile(filename) {
            if (confirm('Delete ' + filename + '?')) {
                fetch('/api/delete?file=' + filename).then(() => refreshData());
            }
        }

        refreshData();
        setInterval(refreshData, 5000);
    </script>
</body>
</html>
    )";

    server.send(200, "text/html", html);
}

// API: 获取系统信息
void handleInfo() {
    server.send(200, "application/json", logger.getSystemInfo());
}

// API: 获取文件列表
void handleFiles() {
    server.send(200, "application/json", logger.getLogFileList());
}

// API: 查看文件
void handleView() {
    if (server.hasArg("file")) {
        String filename = server.arg("file");
        String content = logger.readLogFile(filename.c_str());
        server.send(200, "text/plain", content);
    } else {
        server.send(400, "text/plain", "Missing file parameter");
    }
}

// API: 删除文件
void handleDelete() {
    if (server.hasArg("file")) {
        String filename = server.arg("file");
        if (logger.deleteLogFile(filename.c_str())) {
            server.send(200, "text/plain", "File deleted");
        } else {
            server.send(500, "text/plain", "Delete failed");
        }
    } else {
        server.send(400, "text/plain", "Missing file parameter");
    }
}

void setup() {
    Serial.begin(115200);
    delay(1000);

    Serial.println("IoT Data Logger Starting...");

    // 初始化记录器
    if (!logger.begin()) {
        Serial.println("Logger initialization failed");
        return;
    }

    // 连接WiFi
    WiFi.begin(ssid, password);
    Serial.print("Connecting to WiFi");
    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
    }
    Serial.println();
    Serial.print("Connected! IP: ");
    Serial.println(WiFi.localIP());

    // 配置Web服务器
    server.on("/", handleRoot);
    server.on("/api/info", handleInfo);
    server.on("/api/files", handleFiles);
    server.on("/api/view", handleView);
    server.on("/api/delete", handleDelete);

    server.begin();
    Serial.println("HTTP server started");
    Serial.println("Open http://" + WiFi.localIP().toString() + " in browser");
}

void loop() {
    // 处理Web请求
    server.handleClient();

    // 每10秒记录一次数据
    static unsigned long lastLog = 0;
    if (millis() - lastLog >= 10000) {
        SensorData data = readSensors();
        logger.logData(data.temperature, data.humidity, 
                      data.pressure, data.light);

        Serial.printf("Logged: T=%.1f H=%.1f P=%d L=%d\n",
                     data.temperature, data.humidity, 
                     data.pressure, data.light);

        lastLog = millis();
    }
}

项目功能: - 自动采集传感器数据 - 数据存储到SPIFFS - Web界面查看系统信息 - 在线查看日志文件 - 删除旧日志文件 - 自动创建新日志文件

使用方法: 1. 修改WiFi配置 2. 上传代码到ESP32 3. 打开串口监视器查看IP地址 4. 在浏览器中访问该IP地址 5. 查看和管理日志文件

总结

通过本教程,你学习了:

  • ✅ SPIFFS的设计原理和核心特性
  • ✅ 如何在ESP32上配置和使用SPIFFS
  • ✅ 文件的创建、读取、写入和删除操作
  • ✅ 二进制文件和配置文件的管理
  • ✅ SPIFFS的磨损均衡机制
  • ✅ 性能优化的技巧和方法
  • ✅ Web服务器文件托管
  • ✅ 完整的IoT数据记录系统实现

关键要点

  1. 简单易用
  2. API简洁直观
  3. 类POSIX接口
  4. 易于集成
  5. 适合快速开发

  6. Flash优化

  7. 针对Flash特性设计
  8. 自动磨损均衡
  9. 高效空间利用
  10. 智能垃圾回收

  11. 性能优化

  12. 使用缓冲区批量操作
  13. 避免频繁小文件操作
  14. 定期清理旧文件
  15. 合理配置参数

  16. 限制和注意事项

  17. 不支持目录结构
  18. 掉电保护有限
  19. 文件名长度限制
  20. 需要定期维护

进阶挑战

尝试以下挑战来巩固学习:

  1. 挑战1:实现文件压缩
  2. 使用压缩算法(如zlib)
  3. 压缩日志文件
  4. 节省存储空间
  5. 实现透明压缩/解压

  6. 挑战2:添加文件加密

  7. 使用AES加密
  8. 保护敏感配置
  9. 密钥管理
  10. 透明加密/解密

  11. 挑战3:实现OTA更新

  12. 从SPIFFS加载固件
  13. 实现固件更新
  14. 版本管理
  15. 回滚机制

  16. 挑战4:数据同步到云端

  17. 定期上传日志
  18. 断点续传
  19. 数据压缩
  20. 错误重试

  21. 挑战5:实现数据库功能

  22. 简单的键值数据库
  23. 索引支持
  24. 查询功能
  25. 事务支持

下一步

建议继续学习:

参考资料

官方资源: 1. SPIFFS GitHub - 官方仓库 2. ESP32 SPIFFS文档 - ESP-IDF文档 3. Arduino ESP32 SPIFFS - Arduino库文档

技术文章: 1. ESP32官方文档 - SPIFFS使用指南 2. Arduino ESP32教程 - 文件系统操作 3. SPIFFS设计文档 - 技术原理

相关标准: 1. Flash存储器规范 2. 嵌入式文件系统最佳实践 3. 磨损均衡算法

开发工具: 1. ESP32 Sketch Data Upload - 文件上传工具 2. SPIFFS Image Tool - 镜像创建工具 3. ESP32 Flash Download Tool - Flash编程工具

社区资源: 1. ESP32论坛 2. Arduino社区 3. Stack Overflow相关问题

常见问题

Q1: SPIFFS和LittleFS应该选择哪个?

A: 选择建议: - SPIFFS: 简单应用、不需要目录、快速开发 - LittleFS: 需要掉电保护、需要目录结构、关键数据存储

LittleFS是SPIFFS的改进版本,提供更好的掉电保护和目录支持,推荐新项目使用LittleFS。

Q2: SPIFFS的实际可用空间是多少?

A: SPIFFS的实际可用空间约为分区大小的75-85%,因为: - 元数据开销:10-15% - 磨损均衡开销:5-10% - 垃圾回收预留:5%

例如,1.5MB的SPIFFS分区,实际可用约1.1-1.3MB。

Q3: 如何提高SPIFFS的性能?

A: 性能优化方法: 1. 使用缓冲区批量读写 2. 减少文件打开/关闭次数 3. 避免频繁的小文件操作 4. 定期进行垃圾回收 5. 使用二进制格式而非文本 6. 合理配置页大小

Q4: SPIFFS支持多线程访问吗?

A: SPIFFS本身不是线程安全的。如果需要多线程访问: 1. 使用互斥锁保护文件操作 2. 确保同一时间只有一个线程访问 3. 或者使用消息队列序列化访问

SemaphoreHandle_t spiffsMutex;

void setup() {
    spiffsMutex = xSemaphoreCreateMutex();
}

void safeFileOperation() {
    if (xSemaphoreTake(spiffsMutex, portMAX_DELAY)) {
        // 文件操作
        File file = SPIFFS.open("/file.txt", FILE_WRITE);
        // ...
        file.close();

        xSemaphoreGive(spiffsMutex);
    }
}

Q5: 如何备份SPIFFS数据?

A: 备份方法: 1. 通过串口导出:读取文件并通过串口发送 2. 通过WiFi导出:实现HTTP文件下载 3. 使用esptool:直接读取Flash分区 4. SD卡备份:复制文件到SD卡

// 通过HTTP导出文件
void handleDownload() {
    if (server.hasArg("file")) {
        String filename = server.arg("file");
        File file = SPIFFS.open(filename, FILE_READ);
        if (file) {
            server.streamFile(file, "application/octet-stream");
            file.close();
        }
    }
}

附录A:完整API参考

文件系统操作

// 挂载SPIFFS
bool SPIFFS.begin(bool formatOnFail = false);

// 卸载SPIFFS
void SPIFFS.end();

// 格式化SPIFFS
bool SPIFFS.format();

// 获取总空间
size_t SPIFFS.totalBytes();

// 获取已用空间
size_t SPIFFS.usedBytes();

文件操作

// 打开文件
File SPIFFS.open(const char* path, const char* mode);
// mode: FILE_READ, FILE_WRITE, FILE_APPEND

// 检查文件是否存在
bool SPIFFS.exists(const char* path);

// 删除文件
bool SPIFFS.remove(const char* path);

// 重命名文件
bool SPIFFS.rename(const char* pathFrom, const char* pathTo);

File对象方法

// 读取数据
size_t file.read(uint8_t* buf, size_t size);
int file.read();

// 写入数据
size_t file.write(const uint8_t* buf, size_t size);
size_t file.write(uint8_t data);
size_t file.print(const char* data);
size_t file.println(const char* data);

// 文件定位
bool file.seek(uint32_t pos);
size_t file.position();
size_t file.size();

// 检查状态
int file.available();
bool file.isDirectory();

// 关闭文件
void file.close();

// 刷新缓冲区
void file.flush();

// 读取字符串
String file.readString();
String file.readStringUntil(char terminator);

目录操作

// 打开目录
File SPIFFS.open(const char* path);

// 读取下一个文件
File file.openNextFile();

// 获取文件名
const char* file.name();

附录B:错误处理

常见错误码

错误 说明 解决方法
Mount Failed 挂载失败 格式化SPIFFS
File not found 文件不存在 检查文件名和路径
Write failed 写入失败 检查空间和权限
Out of space 空间不足 删除旧文件或格式化

错误处理示例

/**
 * @brief  带错误处理的文件操作
 */
bool safeFileWrite(const char *path, const char *data) {
    // 检查SPIFFS是否挂载
    if (!SPIFFS.begin()) {
        Serial.println("SPIFFS not mounted");
        return false;
    }

    // 检查空间
    size_t dataSize = strlen(data);
    size_t freeSpace = SPIFFS.totalBytes() - SPIFFS.usedBytes();
    if (dataSize > freeSpace) {
        Serial.println("Not enough space");
        return false;
    }

    // 打开文件
    File file = SPIFFS.open(path, FILE_WRITE);
    if (!file) {
        Serial.println("Failed to open file");
        return false;
    }

    // 写入数据
    size_t written = file.print(data);
    file.close();

    if (written != dataSize) {
        Serial.println("Write incomplete");
        SPIFFS.remove(path);
        return false;
    }

    return true;
}

反馈:如果你在学习过程中遇到问题,欢迎在评论区留言或提交Issue!

贡献:发现错误或有改进建议?欢迎提交Pull Request!

许可:本教程采用 CC BY-SA 4.0 许可协议。