Skip to content

第八章:存储与 NVS

8.1 ESP32-S3 存储体系

存储层次(从快到慢,从小到大):

内部 SRAM(512KB)
  └─ 运行时变量、栈、堆
  └─ 断电丢失

外部 Flash(16MB,N16)
  ├─ 分区表(Partition Table)
  │   ├─ bootloader(64KB)
  │   ├─ nvs(16KB)         ← 键值存储
  │   ├─ phy_init(4KB)
  │   ├─ app(主程序,~3MB)
  │   └─ spiffs(剩余空间)  ← 文件系统
  └─ 断电保留

外部 PSRAM(8MB,R8)
  └─ 大缓冲区(图像、音频)
  └─ 断电丢失

SD 卡(可选,通过 SPI/SDMMC)
  └─ 大容量文件存储

8.2 NVS(非易失性存储)

NVS 特性

NVS = Non-Volatile Storage(非易失性存储)
  类似于 Windows 注册表 / Linux /etc 配置文件
  存储键值对:key → value
  支持类型:int8/16/32/64, uint8/16/32/64, float, string, blob
  命名空间:类似文件夹,隔离不同模块的数据
  断电保留,支持磨损均衡

基础读写

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

static const char *TAG = "NVS";

// 初始化 NVS(app_main 开头必须调用)
void nvs_init(void)
{
    esp_err_t ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES ||
        ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        // NVS 分区损坏或版本不匹配,擦除重建
        ESP_LOGW(TAG, "NVS 分区异常,正在擦除...");
        ESP_ERROR_CHECK(nvs_flash_erase());
        ret = nvs_flash_init();
    }
    ESP_ERROR_CHECK(ret);
    ESP_LOGI(TAG, "NVS 初始化完成");
}

// 写入整数
esp_err_t nvs_write_int(const char *ns, const char *key, int32_t val)
{
    nvs_handle_t handle;
    esp_err_t ret;

    ret = nvs_open(ns, NVS_READWRITE, &handle);
    if (ret != ESP_OK) return ret;

    ret = nvs_set_i32(handle, key, val);
    if (ret == ESP_OK) {
        ret = nvs_commit(handle);  // 必须 commit 才真正写入!
    }
    nvs_close(handle);
    return ret;
}

// 读取整数
esp_err_t nvs_read_int(const char *ns, const char *key,
                        int32_t *val, int32_t default_val)
{
    nvs_handle_t handle;
    esp_err_t ret;

    ret = nvs_open(ns, NVS_READONLY, &handle);
    if (ret != ESP_OK) {
        *val = default_val;
        return ret;
    }

    ret = nvs_get_i32(handle, key, val);
    if (ret == ESP_ERR_NVS_NOT_FOUND) {
        *val = default_val;  // 键不存在,使用默认值
        ret = ESP_OK;
    }
    nvs_close(handle);
    return ret;
}

// 写入字符串
esp_err_t nvs_write_str(const char *ns, const char *key, const char *val)
{
    nvs_handle_t handle;
    esp_err_t ret = nvs_open(ns, NVS_READWRITE, &handle);
    if (ret != ESP_OK) return ret;

    ret = nvs_set_str(handle, key, val);
    if (ret == ESP_OK) ret = nvs_commit(handle);
    nvs_close(handle);
    return ret;
}

// 读取字符串
esp_err_t nvs_read_str(const char *ns, const char *key,
                        char *buf, size_t buf_size)
{
    nvs_handle_t handle;
    esp_err_t ret = nvs_open(ns, NVS_READONLY, &handle);
    if (ret != ESP_OK) return ret;

    size_t required_size = buf_size;
    ret = nvs_get_str(handle, key, buf, &required_size);
    nvs_close(handle);
    return ret;
}

实用示例:保存配置

c
// 配置结构体
typedef struct {
    char     wifi_ssid[32];
    char     wifi_pass[64];
    uint8_t  led_brightness;
    uint32_t boot_count;
} app_config_t;

#define NVS_NS  "app_config"

// 保存配置
void config_save(const app_config_t *cfg)
{
    nvs_handle_t h;
    nvs_open(NVS_NS, NVS_READWRITE, &h);

    nvs_set_str(h, "wifi_ssid", cfg->wifi_ssid);
    nvs_set_str(h, "wifi_pass", cfg->wifi_pass);
    nvs_set_u8(h,  "led_bri",   cfg->led_brightness);
    nvs_set_u32(h, "boot_cnt",  cfg->boot_count);

    nvs_commit(h);
    nvs_close(h);
    ESP_LOGI(TAG, "配置已保存");
}

// 加载配置(带默认值)
void config_load(app_config_t *cfg)
{
    nvs_handle_t h;
    esp_err_t ret = nvs_open(NVS_NS, NVS_READONLY, &h);

    if (ret == ESP_OK) {
        size_t len;
        len = sizeof(cfg->wifi_ssid);
        if (nvs_get_str(h, "wifi_ssid", cfg->wifi_ssid, &len) != ESP_OK)
            strcpy(cfg->wifi_ssid, "");

        len = sizeof(cfg->wifi_pass);
        if (nvs_get_str(h, "wifi_pass", cfg->wifi_pass, &len) != ESP_OK)
            strcpy(cfg->wifi_pass, "");

        if (nvs_get_u8(h,  "led_bri",  &cfg->led_brightness) != ESP_OK)
            cfg->led_brightness = 50;

        if (nvs_get_u32(h, "boot_cnt", &cfg->boot_count) != ESP_OK)
            cfg->boot_count = 0;

        nvs_close(h);
    } else {
        // 首次运行,使用默认值
        memset(cfg, 0, sizeof(*cfg));
        cfg->led_brightness = 50;
        cfg->boot_count = 0;
    }
}

void app_main(void)
{
    nvs_init();

    app_config_t cfg;
    config_load(&cfg);

    cfg.boot_count++;
    ESP_LOGI(TAG, "第 %"PRIu32" 次启动", cfg.boot_count);

    config_save(&cfg);
}

8.3 SPIFFS 文件系统

c
#include "esp_spiffs.h"
#include <stdio.h>
#include <dirent.h>

// 挂载 SPIFFS
void spiffs_init(void)
{
    esp_vfs_spiffs_conf_t conf = {
        .base_path              = "/spiffs",  // 挂载点
        .partition_label        = NULL,        // 使用默认分区
        .max_files              = 5,
        .format_if_mount_failed = true,        // 挂载失败则格式化
    };
    ESP_ERROR_CHECK(esp_vfs_spiffs_register(&conf));

    size_t total = 0, used = 0;
    esp_spiffs_info(NULL, &total, &used);
    ESP_LOGI(TAG, "SPIFFS: 总 %d KB,已用 %d KB",
             total / 1024, used / 1024);
}

// 写文件
void spiffs_write_file(const char *path, const char *content)
{
    FILE *f = fopen(path, "w");
    if (f == NULL) {
        ESP_LOGE(TAG, "无法创建文件: %s", path);
        return;
    }
    fprintf(f, "%s", content);
    fclose(f);
    ESP_LOGI(TAG, "文件写入: %s", path);
}

// 读文件
void spiffs_read_file(const char *path)
{
    FILE *f = fopen(path, "r");
    if (f == NULL) {
        ESP_LOGE(TAG, "文件不存在: %s", path);
        return;
    }
    char line[128];
    while (fgets(line, sizeof(line), f)) {
        printf("%s", line);
    }
    fclose(f);
}

// 列出目录
void spiffs_list_dir(const char *path)
{
    DIR *dir = opendir(path);
    if (!dir) return;

    struct dirent *entry;
    while ((entry = readdir(dir)) != NULL) {
        ESP_LOGI(TAG, "  %s", entry->d_name);
    }
    closedir(dir);
}

void app_main(void)
{
    nvs_init();
    spiffs_init();

    // 写入配置文件
    spiffs_write_file("/spiffs/config.txt",
                      "brightness=80\nwifi_ssid=MyWiFi\n");

    // 读取
    spiffs_read_file("/spiffs/config.txt");

    // 列出文件
    spiffs_list_dir("/spiffs");
}

8.4 分区表配置

自定义分区表(partitions.csv)

csv
# Name,   Type, SubType, Offset,  Size,   Flags
nvs,      data, nvs,     0x9000,  0x6000,
phy_init, data, phy,     0xf000,  0x1000,
factory,  app,  factory, 0x10000, 3M,
spiffs,   data, spiffs,  ,        12M,

在 CMakeLists.txt 中指定

cmake
set(PARTITION_TABLE_CSV_PATH "${CMAKE_CURRENT_LIST_DIR}/partitions.csv")

或在 menuconfig 中: Partition Table → Custom partition table CSV


📝 第八章练习题

练习 8-1:开机计数器(基础)

目标:掌握 NVS 基础读写

任务

  • 每次开机读取 NVS 中的计数值
  • 计数 +1 后写回 NVS
  • 打印:这是第 N 次开机
  • 断电重启后计数不丢失

练习 8-2:配置管理器(基础)

目标:NVS 结构化数据存储

任务:实现配置管理模块,支持:

  • config set <key> <value> → 保存配置
  • config get <key> → 读取配置
  • config list → 列出所有配置
  • config reset → 恢复默认值

通过 UART 命令行交互,配置断电保留。


练习 8-3:日志文件(进阶)

目标:SPIFFS 文件操作

任务

  • 每次开机在 /spiffs/log.txt 追加一条记录
  • 格式:[开机次数] [时间戳] 系统启动
  • 文件超过 10KB 时,自动删除旧文件重建
  • 通过 UART 命令 log show 打印日志内容

练习 8-4:参数存储(进阶)

目标:NVS Blob 存储结构体

任务

  • 定义一个传感器校准参数结构体(包含偏移量、增益等)
  • 使用 NVS Blob 存储整个结构体
  • 实现校准流程:采集 10 次数据,计算平均值作为偏移量
  • 保存校准参数,下次开机自动加载

NVS Blob API

c
nvs_set_blob(handle, "calib", &calib_data, sizeof(calib_data));
nvs_get_blob(handle, "calib", &calib_data, &size);

练习 8-5:思考题

  1. NVS 写入时为什么必须调用 nvs_commit()?如果不调用会发生什么?

  2. Flash 的擦写次数有限(通常 10 万次),NVS 如何通过磨损均衡延长寿命?

  3. SPIFFS 和 FAT 文件系统各有什么优缺点?什么时候选择 SPIFFS,什么时候选择 FAT(SD 卡)?

  4. NVS 分区默认只有 16KB,能存多少数据?如果需要存储更多配置怎么办?


本章小结

知识点掌握程度自评
NVS 初始化⬜⬜⬜⬜⬜
NVS 键值读写⬜⬜⬜⬜⬜
NVS Blob 存储⬜⬜⬜⬜⬜
SPIFFS 挂载⬜⬜⬜⬜⬜
文件读写操作⬜⬜⬜⬜⬜
分区表配置⬜⬜⬜⬜⬜

上一章← FreeRTOS 基础下一章Wi-Fi 编程 →

个人知识库