Chapter 08

数据存储与文件系统

掌握 NVS 键值存储、Flash 文件系统和 SD 卡,实现数据掉电保存

Flash 分区表

ESP32 的 Flash(通常 4MB)通过分区表划分不同用途区域。分区表位于 Flash 的固定地址(0x8000),记录每个分区的类型、子类型、起始地址和大小。

ESP32 典型 Flash 分区布局(4MB): 偏移地址 大小 类型 名称 ───────────────────────────────────────────────────────── 0x000000 32KB bootloader 引导程序(只读) 0x008000 3KB partition 分区表本身 0x008C00 16KB nvs NVS 存储(Wi-Fi 校准、用户配置) 0x00c000 8KB phy_init PHY 初始化数据(RF 校准) 0x010000 1.9MB factory 出厂应用固件 0x1f0000 1.9MB ota_0 OTA 固件 A(双 OTA 时使用) 0x3d0000 192KB spiffs SPIFFS/LittleFS 文件系统 ───────────────────────────────────────────────────────── 自定义分区表(partitions.csv): # Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x6000, ota_data, data, ota, 0xf000, 0x2000, app0, app, ota_0, 0x10000, 0x1E0000, app1, app, ota_1, 0x1F0000, 0x1E0000, littlefs, data, spiffs, 0x3D0000, 0x30000,

NVS — 非易失性存储

NVS(Non-Volatile Storage)是 ESP-IDF 提供的键值对存储系统,数据存储在 Flash 的专用分区,断电后保留。它是存储 Wi-Fi 密码、设备配置、校准参数等小量数据的最佳方式。

NVS 核心概念

Namespace(命名空间)
类似文件夹,隔离不同模块的键值对。键名在同一命名空间内唯一,不同命名空间可有同名键。命名空间名最长 15 字节。
键(Key)
字符串标识符,最长 15 字节。支持存储类型:uint8/16/32/64、int8/16/32/64、float、double、字符串、二进制数据(Blob)。
磨损均衡
NVS 内部实现了 Flash 磨损均衡算法,避免反复写同一位置导致 Flash 寿命缩短。理论上每个扇区可擦写约 10 万次,NVS 将写操作分散到整个 NVS 分区。
NVS 分区大小建议
至少 0x6000(24KB)。系统需要约 8KB 存储 Wi-Fi 配置和 PHY 校准数据,剩余才是用户可用空间。

NVS 读写示例

#include "nvs_flash.h"
#include "nvs.h"
#include "esp_log.h"

#define NVS_NS  "app_config"   // 命名空间
static const char *TAG = "NVS";

void nvs_write_config(const char *ssid, const char *password, int32_t interval)
{
    nvs_handle_t handle;
    ESP_ERROR_CHECK(nvs_open(NVS_NS, NVS_READWRITE, &handle));

    nvs_set_str(handle, "wifi_ssid", ssid);
    nvs_set_str(handle, "wifi_pass", password);
    nvs_set_i32(handle, "report_interval", interval);

    ESP_ERROR_CHECK(nvs_commit(handle));  // 必须 commit 才真正写入 Flash
    nvs_close(handle);
    ESP_LOGI(TAG, "配置已保存");
}

void nvs_read_config(void)
{
    nvs_handle_t handle;
    esp_err_t err = nvs_open(NVS_NS, NVS_READONLY, &handle);
    if (err == ESP_ERR_NVS_NOT_FOUND) {
        ESP_LOGW(TAG, "命名空间不存在,使用默认配置");
        return;
    }

    char ssid[33] = {0};
    size_t ssid_len = sizeof(ssid);
    nvs_get_str(handle, "wifi_ssid", ssid, &ssid_len);

    int32_t interval = 60;    // 默认值
    nvs_get_i32(handle, "report_interval", &interval);

    ESP_LOGI(TAG, "SSID: %s, 上报间隔: %" PRId32 "秒", ssid, interval);
    nvs_close(handle);
}

void app_main(void)
{
    /* 初始化 NVS(必须在 Wi-Fi 初始化前调用)*/
    esp_err_t ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        ESP_ERROR_CHECK(nvs_flash_erase());   // 清空后重试
        ret = nvs_flash_init();
    }
    ESP_ERROR_CHECK(ret);

    nvs_write_config("HomeWiFi", "password123", 30);
    nvs_read_config();
}

LittleFS — 推荐文件系统

LittleFS 是 ARM Mbed 团队开发的嵌入式文件系统,专为 Flash 存储设计,具有断电安全、磨损均衡、压缩等特性。ESP-IDF v5 起推荐使用 LittleFS 替代老旧的 SPIFFS。

LittleFS 优点

  • 支持真正的目录结构
  • 原子操作(断电安全)
  • 更好的磨损均衡
  • 更快的文件系统修复

SPIFFS 局限性

  • 不支持目录(所有文件在根)
  • 断电可能损坏文件
  • 大量小文件时性能下降
  • 官方建议迁移到 LittleFS
#include "esp_littlefs.h"
#include "esp_log.h"
#include <stdio.h>
#include <sys/stat.h>

static const char *TAG = "LFS";

void littlefs_init(void)
{
    esp_vfs_littlefs_conf_t conf = {
        .base_path              = "/littlefs",    // 挂载点
        .partition_label        = "littlefs",     // 分区表中的名称
        .format_if_mount_failed = true,           // 挂载失败时自动格式化
    };
    ESP_ERROR_CHECK(esp_vfs_littlefs_register(&conf));

    size_t total = 0, used = 0;
    esp_littlefs_info("littlefs", &total, &used);
    ESP_LOGI(TAG, "文件系统: 总计 %d KB, 已用 %d KB", total/1024, used/1024);
}

void write_sensor_log(float temp, float hum, uint32_t timestamp)
{
    FILE *f = fopen("/littlefs/sensor.csv", "a");
    if (!f) {
        ESP_LOGE(TAG, "无法打开文件");
        return;
    }
    fprintf(f, "%" PRIu32 ",%0.1f,%0.1f\n", timestamp, temp, hum);
    fclose(f);
}

void read_sensor_log(void)
{
    FILE *f = fopen("/littlefs/sensor.csv", "r");
    if (!f) return;
    char line[64];
    while (fgets(line, sizeof(line), f)) {
        ESP_LOGI(TAG, "%s", line);
    }
    fclose(f);
}
上传文件到 Flash 的方法

将本地文件(HTML/配置文件/证书)打包到 LittleFS 镜像中:
1. 将文件放入 data/ 目录
2. CMakeLists.txt 中添加 littlefs_create_partition_image(littlefs ../data FLASH_IN_PROJECT)
3. idf.py flash 时会自动打包上传

SD 卡(SPI 模式)

#include "driver/spi_common.h"
#include "driver/sdspi_host.h"
#include "sdmmc_cmd.h"
#include "esp_vfs_fat.h"

#define SD_CS   GPIO_NUM_5
#define SD_CLK  GPIO_NUM_18
#define SD_MOSI GPIO_NUM_23
#define SD_MISO GPIO_NUM_19

void sd_card_init(void)
{
    sdmmc_host_t host = SDSPI_HOST_DEFAULT();
    spi_bus_config_t bus_cfg = {
        .mosi_io_num = SD_MOSI,
        .miso_io_num = SD_MISO,
        .sclk_io_num = SD_CLK,
        .quadwp_io_num = -1,
        .quadhd_io_num = -1,
    };
    spi_bus_initialize(host.slot, &bus_cfg, SDSPI_DEFAULT_DMA);

    sdspi_device_config_t slot_config = SDSPI_DEVICE_CONFIG_DEFAULT();
    slot_config.gpio_cs = SD_CS;

    esp_vfs_fat_sdmmc_mount_config_t mount_config = {
        .format_if_mount_failed = false,
        .max_files              = 5,
        .allocation_unit_size   = 16 * 1024,
    };

    sdmmc_card_t *card;
    esp_vfs_fat_sdspi_mount("/sdcard", &host, &slot_config, &mount_config, &card);
    sdmmc_card_print_info(stdout, card);

    /* 之后可以用标准 POSIX 文件 API */
    FILE *f = fopen("/sdcard/log.txt", "a");
    fprintf(f, "ESP32 启动\n");
    fclose(f);
}
SD 卡 SPI 模式速度限制

SPI 模式下 SD 卡最高约 20~25 MHz,实际读写速度约 2~4 MB/s。若需更高速度(如连续录制音频/视频),应选用 SDMMC 4-bit 接口,但需注意引脚冲突(GPIO 12/13/14/15 与 JTAG 和 Flash 共用)。