Skip to content

3.4 NVS 配置存储 ​

这节要搞懂:怎么让设置断电之后还在。 需要基础:3.3

一、为什么需要它 ​

普通变量存在 RAM 里,一断电就没了。

但闹钟时间、WiFi 密码这些,你肯定希望拔了电源重新插上还在。

二、什么是 NVS ​

NVS(Non-Volatile Storage,非易失性存储)是 ESP-IDF 提供的键值对存储组件。数据保存在 Flash 中,掉电不丢失。它内部处理了磨损均衡和掉电保护,比直接读写 Flash 安全得多。

打个比方:NVS 就是芯片里的一个小记事本,你按"名字 → 内容"的方式往上记,断电了本子还在。

三、它的数据模型 ​

就是最简单的**"名字 → 值",再用命名空间**(相当于文件夹)分组:

命名空间 "wifi"   ├─ "ssid" → "MyHome"
                  └─ "pass" → "12345678"

命名空间 "clock"  ├─ "hour" → 7
                  └─ "min"  → 30

为什么要命名空间:避免不同模块起了相同的键名互相覆盖。

四、存一个数字 ​

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

esp_err_t storage_save_volume(uint8_t volume)
{
    nvs_handle_t handle;

    // 1. 打开命名空间(读写模式)
    esp_err_t err = nvs_open("clock", NVS_READWRITE, &handle);
    if (err != ESP_OK) {
        return err;
    }

    // 2. 写入
    err = nvs_set_u8(handle, "volume", volume);

    // 3. ⭐ 提交!!不提交等于白写
    if (err == ESP_OK) {
        err = nvs_commit(handle);
    }

    // 4. 关闭
    nvs_close(handle);
    return err;
}

最经典的坑:忘了 commit

nvs_set_u8 只是写在缓存里,没有真正存进 Flash。

必须调用 nvs_commit(),否则一断电就全没了。

症状:设置的时候看着好好的,一重启就恢复默认值 —— 十有八九是忘了 commit。

五、读一个数字 ​

c
uint8_t storage_load_volume(void)
{
    nvs_handle_t handle;
    uint8_t volume = 70;                  // ⭐ 先给个合理的默认值

    if (nvs_open("clock", NVS_READONLY, &handle) == ESP_OK) {
        // 读不到的话,volume 保持默认值 70
        nvs_get_u8(handle, "volume", &volume);
        nvs_close(handle);
    }

    return volume;
}

一定要给默认值

第一次开机时 NVS 里什么都没有,读取会失败。

先赋一个合理的默认值,读失败就用默认的,程序才不会拿到一个随机数。

六、存字符串(比如 WiFi 密码) ​

c
// 存
esp_err_t storage_save_wifi(const char *ssid, const char *password)
{
    nvs_handle_t handle;
    esp_err_t err = nvs_open("wifi", NVS_READWRITE, &handle);
    if (err != ESP_OK) return err;

    nvs_set_str(handle, "ssid", ssid);
    nvs_set_str(handle, "pass", password);
    err = nvs_commit(handle);

    nvs_close(handle);
    return err;
}

// 读
esp_err_t storage_load_ssid(char *buffer, size_t buffer_size)
{
    nvs_handle_t handle;
    if (nvs_open("wifi", NVS_READONLY, &handle) != ESP_OK) {
        return ESP_ERR_NVS_NOT_FOUND;
    }

    size_t length = buffer_size;          // ⚠️ 要先填缓冲区大小
    esp_err_t err = nvs_get_str(handle, "ssid", buffer, &length);

    nvs_close(handle);
    return err;
}

七、一次存一整个结构体(推荐) ​

配置一多,一个个存太啰嗦。打包成结构体一次存:

c
typedef struct {
    char    city[32];            // 天气城市,如 "shenzhen"
    char    alarm[16];           // "07:30" 或 "close"
    bool    auto_light;          // 自动亮度开关
    uint8_t light;               // 当前亮度 0~100
    uint8_t auto_light_min;      // 自动亮度下限
    uint8_t auto_light_max;      // 自动亮度上限
    uint8_t volume;              // 音量 0~100
    uint8_t refresh_minutes;     // 天气刷新间隔(分钟)
    float   maxtemp_set;         // 温度报警上限
    float   minitemp_set;        // 温度报警下限
    uint8_t maxhum_set;          // 湿度报警上限
    uint8_t minihum_set;         // 湿度报警下限
} app_config_t;

// 保存
esp_err_t config_save(const app_config_t *config)
{
    nvs_handle_t handle;
    esp_err_t err = nvs_open("clock", NVS_READWRITE, &handle);
    if (err != ESP_OK) return err;

    err = nvs_set_blob(handle, "cfg", config, sizeof(app_config_t));
    if (err == ESP_OK) {
        err = nvs_commit(handle);
    }

    nvs_close(handle);
    return err;
}

// 读取
void config_load(app_config_t *config)
{
    // 先填默认值
    strcpy(config->city,  "shenzhen");
    strcpy(config->alarm, "close");        // 默认不开闹钟
    config->auto_light      = true;
    config->light           = 80;
    config->auto_light_min  = 15;
    config->auto_light_max  = 90;
    config->volume          = 70;
    config->refresh_minutes = 10;

    nvs_handle_t handle;
    size_t length = sizeof(app_config_t);

    if (nvs_open("clock", NVS_READONLY, &handle) == ESP_OK) {
        // 读成功就覆盖默认值,失败就保持默认
        nvs_get_blob(handle, "cfg", config, &length);
        nvs_close(handle);
    }
}

blob 是什么

blob(Binary Large Object)就是"一坨二进制数据"。

NVS 不关心里面是什么,原样存、原样取。用它存结构体最方便。

八、别频繁写 ⭐ ​

Flash 有擦写寿命

NVS 存在 Flash 里,大约能擦写 10 万次。

数据能存吗
闹钟、城市、亮度、音量、报警阈值✅ 用户改的时候才写,一天几次
当前温度(2 秒一次)❌ 绝对不行!几天就把 Flash 写坏了

原则:只在用户改动设置的时候写,绝不定时写。

九、恢复出厂设置 ​

c
nvs_flash_erase();       // 擦掉整个 NVS 分区

可以做成"同时长按两个键 5 秒恢复出厂"。

本节要点 ​

  • NVS = 存在 Flash 里的键值对存储,掉电不丢
  • 用命名空间分组,避免键名冲突
  • ⭐ nvs_set 之后必须 nvs_commit,否则白存
  • 读之前先给默认值,第一次开机读不到很正常
  • 配置多就用 blob 存整个结构体
  • ⭐ 别定时写!Flash 会写坏

操作过程 ​

目标:改一个配置,断电重启,验证它确实保住了。

第 1 步:编译烧录 ​

第 2 步:看开机时读到了什么 ​

在 app_config_init() 之后加一行打印:

c
app_config_t cfg;
app_config_get(&cfg);
ESP_LOGI(TAG, "config loaded: city=%s alarm=%s light=%d volume=%d",
         cfg.city, cfg.alarm, cfg.light, cfg.volume);

第一次开机(NVS 还是空的)应该看到默认值:

I (1234) main: config loaded: city=shenzhen alarm=close light=80 volume=70

第 3 步:改一个配置 ​

用微信小程序连上蓝牙(3.10),往配置特征值写:

json
{"volume":30}

串口应该打印出配置被更新了。

第 4 步:断电重启 ​

直接拔掉 USB 再插上 —— 不要按复位键,要真正断电才能验证 NVS。

第 5 步:应该看到什么 ​

I (1234) main: config loaded: city=shenzhen alarm=close light=80 volume=30
                                                                 ↑ 保住了

volume 从 70 变成了 30 并且断电后还在 —— 说明 NVS 工作正常。

第 6 步:故意做错一次(重要) ​

把 app_config.c 里保存配置的那个 nvs_commit() 注释掉,重新烧录,再改一次音量,然后断电重启。

你会看到配置又变回去了。

这就是最经典的 NVS 坑

nvs_set_* 只是写进缓存,不 commit 断电就全丢。

亲手制造一次这个现象,比看十遍文字管用。测完记得把那行加回去。

动手试试 ​

  1. 存一个数字进 NVS,重启后读出来,确认还在。
  2. 故意把 nvs_commit() 注释掉,重启看数据是不是丢了 —— 加深印象。
  3. 用蓝牙改一次音量(3.10),重启验证它确实存住了。