ESP32 的 NVS,并不是你以为的“Flash 存数据”

ESP32 的 NVS,我是踩过坑之后才真正搞懂的

一开始接触 NVS(Non-Volatile Storage)的时候,我对它的理解非常简单:

"不就是往 Flash 里存点数据吗?"

结果在真实项目里用起来之后才发现,
NVS 并不是一个"随便存、随便读"的东西

而是一个有明确使用边界和工程语义的持久化存储机制

这篇文章,我就完全基于我在 ESP32 / ESP32-S3 项目里用 NVS 的真实经验,把它讲清楚。


一、NVS 到底是个什么东西?

先说一句非常重要、但官方文档很少强调的话:

NVS 不是文件系统,也不是 Flash 的裸读写接口

NVS 的本质是:

  • 位于 ESP32 Flash 中的一个独立分区
  • key-value(键值对) 的形式组织数据
  • 面向的是:
    配置参数、状态变量、少量业务数据

你可以把它理解成:

一个存在于 Flash 里的「持久化哈希表」

  • key:字符串
  • value:整数 / 字符串 / 二进制块
  • 掉电不丢
  • 重启还能接着用

📌 它最典型的使用场景包括:

  • 设备重启计数
  • WiFi / 蓝牙参数
  • 设备 ID、序列号
  • 校准参数
  • 用户配置项

二、为什么 NVS 一定要初始化,而且还可能失败?

在真正开始用 NVS 之前,你必须初始化整个 NVS 分区:

c 复制代码
esp_err_t nvs_flash_init(void);

这一步一般都会放在 app_main() 的最前面。

初始化为什么会失败?

在工程里,我实际遇到过的主要就是这两种:

  • ESP_ERR_NVS_NO_FREE_PAGES
  • ESP_ERR_NVS_NEW_VERSION_FOUND

这两种错误,本质上都在说一件事:

当前 NVS 分区的数据结构,已经不能正常使用了

所以官方给出的标准处理方式其实非常工程化:

c 复制代码
esp_err_t err = nvs_flash_init();
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
    ESP_ERROR_CHECK(nvs_flash_erase());
    err = nvs_flash_init();
}
ESP_ERROR_CHECK(err);

📌 这段代码我强烈建议你直接当模板用,原因很简单:

  • 你迟早会遇到 NVS 版本变更
  • 你迟早会在调试阶段写满 NVS
  • 不处理,程序就会卡在初始化阶段

三、NVS 的"命名空间",比你想的重要

第一次用 NVS 的时候,我其实忽略了 nvs_open() 的第一个参数:

c 复制代码
nvs_open("storage", NVS_READWRITE, &my_handle);

后来才意识到,这个 namespace(命名空间) 非常关键。

你可以把它理解为:

Flash 里的"逻辑文件夹"

  • 同一个 namespace 下面

    • key 不能重名
  • 不同 namespace 之间

    • key 可以相同,互不影响

📌 在稍微复杂一点的项目里,我一般会这样拆:

  • "sys":系统级参数
  • "user":用户配置
  • "cal":校准数据
  • "stat":运行状态统计

这样做的好处是:

  • 逻辑清晰
  • 不容易误删
  • 后期维护成本低

四、为什么 nvs_commit() 这么重要?

这是我踩过最真实的一次坑

一开始我以为:

nvs_set_xxx() 写了数据,就已经存进 Flash 了

但事实完全不是这样。

NVS 的写入流程是:

  1. nvs_set_xxx()
    👉 只是把数据写进 RAM 缓冲区
  2. nvs_commit()
    👉 才是真正写入 Flash

也就是说:

nvs_commit() 之前,所有数据都是"临时的"

如果你在 commit 之前:

  • 重启
  • 掉电
  • 崩溃

👉 数据全部丢失

所以现在我的使用习惯是:

只要 set 过,就一定 commit

哪怕你觉得"这次改动不重要"。


五、NVS 能存什么?以及我一般怎么选

NVS 支持的类型其实非常全:

  • 各种整型(i8 / u8 / i16 / u32 / i64 ...)
  • 字符串
  • 二进制 blob

我个人的使用经验是:

  • 配置项、计数器
    👉 直接用整型
  • 设备名、版本号
    👉 用 nvs_set_str
  • 结构体 / 校准表 / 自定义数据
    👉 用 nvs_set_blob

比如存一个二进制数据块:

c 复制代码
uint8_t blob_value[] = {0x01, 0x02, 0x03, 0x04};
nvs_set_blob(handle, "blob_key", blob_value, sizeof(blob_value));

📌 注意一点

NVS 不是数据库,不适合高频、大数据量写入。


六、读取 NVS 时,我一般会注意什么?

nvs_get_i32() 为例:

c 复制代码
int32_t value = 0;
err = nvs_get_i32(handle, "restart_counter", &value);

这里有一个很容易被忽略的点:

  • 如果 key 不存在

    • 返回的不是你想象中的 0
    • 而是一个错误码(通常是 ESP_ERR_NVS_NOT_FOUND

所以在工程里,我一般会:

  • 先给变量一个默认值
  • 再根据返回值判断是否读取成功

七、完整示例:重启计数器(非常实用)

这个例子我非常喜欢,因为它完全符合 NVS 的设计初衷

  • 数据量小
  • 写入频率低
  • 掉电必须保留

每次启动:

  1. 从 NVS 读重启次数
  2. 自增
  3. 写回 Flash
  4. 延时后重启
c 复制代码
#include <stdio.h>
#include <inttypes.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "nvs_flash.h"
#include "nvs.h"
#include "esp_log.h"

static const char *TAG = "NVS"; // 定义日志标签

void app_main(void)
{
    // 初始化 NVS
    esp_err_t err = nvs_flash_init();
    if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        // NVS 分区被截断,需要擦除
        // 重新初始化 nvs_flash
        ESP_ERROR_CHECK(nvs_flash_erase());
        err = nvs_flash_init();
    }
    ESP_ERROR_CHECK(err);

    // 打开命名空间
    ESP_LOGI(TAG, "打开非易失性存储 (NVS) 句柄...");
    nvs_handle_t my_handle;
    err = nvs_open("storage", NVS_READWRITE, &my_handle);
    if (err != ESP_OK) {
        ESP_LOGI(TAG, "打开 NVS 句柄时出错 (%s)!\n", esp_err_to_name(err));
    } else {
        // 读取重启计数器
        ESP_LOGI(TAG, "从 NVS 读取重启计数器 ... ");
        int32_t restart_counter = 0; // 如果 NVS 中未设置值,则默认为 0
        err = nvs_get_i32(my_handle, "restart_counter", &restart_counter);
        ESP_LOGI(TAG, "重启计数器 = %" PRIu32 "\n", restart_counter);

        // 更新重启计数器
        ESP_LOGI(TAG, "更新 NVS 中的重启计数器 ... ");
        restart_counter++;
        err = nvs_set_i32(my_handle, "restart_counter", restart_counter);

        // 提交写入的值。设置任何值后,必须调用 nvs_commit() 以确保更改写入闪存存储。
        ESP_LOGI(TAG, "提交 NVS 中的更改 ... ");
        err = nvs_commit(my_handle);
        // 关闭命名空间
        nvs_close(my_handle);
    }

    ESP_LOGI(TAG, "\n");

    // 重启模块
    for (int i = 10; i >= 0; i--) {
        ESP_LOGI(TAG, "将在 %d 秒后重启...\n", i);
        vTaskDelay(1000 / portTICK_PERIOD_MS);
    }
    ESP_LOGI(TAG, "现在重启。\n");
    fflush(stdout);
    esp_restart();
}

📌 这种代码结构,在真实项目里是可以长期保留的


八、我现在对 NVS 的"最终理解"

用到现在,我对 NVS 的定位已经非常清晰:

  • 它不是存日志的
  • 也不是存大数据的
  • 更不是替代文件系统的

👉 它是用来"记住状态"的

如果一句话总结:

NVS = 少量、关键、必须跨重启存在的数据

只要你不越界使用,

NVS 会是 ESP32 系列里非常稳定、非常好用的一块基础设施

相关推荐
YJlio5 小时前
1.7 通过 Sysinternals Live 在线运行工具:不下载也能用的“云端工具箱”
c语言·网络·python·数码相机·ios·django·iphone
Lbs_gemini060311 小时前
01-01-01 C++编程知识 C++入门 工具安装
c语言·开发语言·c++·学习·算法
shihui200312 小时前
两个8*8点阵流水屏
c语言·51单片机·proteus
ghx_echo13 小时前
c/c++结构体对齐,extern “C”与关键字const
c语言·c++
IvanCodes14 小时前
五、C语言数组
c语言·开发语言
雪域迷影16 小时前
MacOS下源码安装SDL3并运行hello.c示例程序
c语言·开发语言·macos·sdl3
BackCatK Chen17 小时前
14.2【保姆级C语言入门】 标准I/O实战宝典:6大模块手把手教你搞定文件操作,新手直接上手!
c语言·开发语言·fopen·fclose·标准i/o·getc·putc
IvanCodes19 小时前
六、C语言字符串与字符
c语言·开发语言
搏博19 小时前
蓝鸟四轴飞行器初版V3.0(三)——主控硬件设计
c语言·python