目录
[1.1 分区 (Partition):NVS 的专属"仓库"](#1.1 分区 (Partition):NVS 的专属“仓库”)
[1.2 页面 (Page):仓库里的"货架"](#1.2 页面 (Page):仓库里的“货架”)
[1.3 条目 (Entry):货架上的"最小存储格"](#1.3 条目 (Entry):货架上的“最小存储格”)
[1.4 键值对 (Key-Value Pair):实际存放的"货物"](#1.4 键值对 (Key-Value Pair):实际存放的“货物”)
[1.5 命名空间 (Namespace):货物的"分类文件夹"](#1.5 命名空间 (Namespace):货物的“分类文件夹”)
[1 nvs_basics.c](#1 nvs_basics.c)
[2 nvs_basics.h](#2 nvs_basics.h)
[3 mian.c](#3 mian.c)
前言
NVS(Non-Volatile Storage,非易失性存储)是一个数据储存库。它的核心作用就是在 Flash 中持久化地存储键值对格式的数据。它非常适合存储那些不经常更改、但需要长期保留的配置数据。例如wifi和蓝牙的各种配网凭据。
本文使用的开发板是微雪的**ESP32-P4-Module-DEV-KIT。**ESP-IDF版本是6.0.1。基于第一章的工程模板。来实现NVS库的各种操作。
一、NVS梳理
NVS(Non-Volatile Storage,非易失性存储)是 ESP32 中用于在 Flash 中持久化保存数据的官方库。它本质上是一个轻量级的键值对存储系统。但是各种名词、概念较多,容易产生误解。这里我从宏观到微观的层级来梳理这些概念。
1.1 分区 (Partition):NVS 的专属"仓库"
NVS 的数据最终是存储在 ESP32 的 Flash 上的。 ESP32 的 Flash使用一个分区表对各个类型的数据空间进行管理,其中就有(也必须有)一个专门的分区给 NVS 使用。
- 在
partitions.csv分区表中,类型(Type)为data,子类型(SubType)为nvs的分区。 - 默认通常是 24KB (0x6000)。如果你的设备需要存储大量配置,可以在分区表中手动调大这个数值。
- 它是 NVS 库操作的物理边界,所有的 NVS 数据都只能在这个划定的 Flash 区域内读写。
ESP32 的 Flash实际分区表如下:关于各种分区的解释可以去看《分区表》,前面工程《ESP-IDF+vscode开发ESP32第九讲------I2S工程1》也涉及到分区表的使用。
bash
# ESP-IDF Partition Table
# Name , Type, SubType, Offset , Size , Flags
nvs , data, nvs , 0x9000 , 0x6000,
phy_init , data, phy , 0xf000 , 0x1000,
factory , app , factory, 0x10000, 10M , ,
1.2 页面 (Page):仓库里的"货架"
NVS 并不是把数据杂乱无章地堆在分区里,而是将分区划分成了若干个固定大小的"页面"。
- 大小:通常一个页面的大小等于 Flash 的一个物理扇区,即 4096 字节 (4KB)。
- 状态管理:每个页面都有自己的生命周期状态(如:空、活跃、写满、擦除中)。
- 磨损均衡:NVS 采用日志式的写入方式。当前活跃的页面写满后,系统会自动切换到下一个空白页面继续写入。这种机制避免了频繁擦写同一个物理地址,极大地延长了 Flash 的使用寿命。
1.3 条目 (Entry):货架上的"最小存储格"
页面进一步被划分为更小的单元,称为"条目"。
- 大小:每个条目固定为 32 字节。
- 作用:它是 NVS 存储数据的最小物理单位。无论是存一个小小的整数,还是存一段很长的字符串,都会占用至少一个条目。
- 空间计算:如果你发现 NVS 经常报"空间不足",往往是因为条目被耗尽了。例如,在一个 24KB 的 NVS 分区中,大约只有几千个条目可供使用。
1.4 键值对 (Key-Value Pair):实际存放的"货物"
这是开发者在写代码时最直接接触的概念。NVS 的所有数据操作都是基于键值对的。
- 键 (Key):数据的名字(ASCII 字符串),最大长度不能超过 15 个字符。
- 值 (Value):实际要存储的数据。支持多种类型:
- 整数(如
int8_t,int32_t,uint64_t等) - 字符串(以
\0结尾,最大约 4000 字节) - 二进制大对象(BLOB,用于存结构体、数组等,最大约 50 万字节)
- 整数(如
- 存储逻辑:当你写入一个键值对时,NVS 会根据数据的大小,占用 1 个或多个连续的"条目"来存放它。
1.5 命名空间 (Namespace):货物的"分类文件夹"
当我们需要存入大量键值对,很有可能有重名的键,为了防止不同功能模块的键名(Key)发生冲突,NVS 引入了命名空间的概念。
- 作用:相当于电脑里的"文件夹"。你可以把 Wi-Fi 的配置放在名为
"wifi"的命名空间里,把传感器的校准数据放在"sensor"的命名空间里。 - 隔离性:即使两个命名空间里都有叫
"config"的键,它们也是完全独立、互不干扰的。 - 限制:命名空间的名称同样不能超过 15 个字符。
到这所有NVS的储存结构我们清楚了,下面就可以尝试去使用NVS了。
二、完善工程
创建组件nvs_basics,在cmakelists中添加依赖声明
bash
REQUIRES nvs_flash
1 nvs_basics.c
cpp
#include <stdio.h>
#include "nvs_basics.h"
static const char *TAG = "NVS_BASICS";
typedef struct {
uint8_t id;
char name[32];
float values[2];
uint32_t flags;
int16_t counts[2];
bool active;
} test_data_t;
void nvs_init(void)
{
esp_err_t err;
err = nvs_flash_init();
if (err == ESP_ERR_NVS_NO_FREE_PAGES ) {
ESP_LOGW(TAG, "分区中没有了空页面");
return;
}
else if (err == ESP_ERR_NOT_FOUND ) {
ESP_LOGW(TAG, "没有找到NVS分区");
return;
}
nvs_handle_t storage_handle;
ESP_ERROR_CHECK(nvs_open("storage", NVS_READWRITE_PURGE , &storage_handle));
ESP_ERROR_CHECK(nvs_set_i32(storage_handle, "num", 123456789));
ESP_ERROR_CHECK(nvs_set_str(storage_handle, "str", "Hello, ESP32!"));
test_data_t test_data = {
.id = 123,
.name = "Test Sample",
.values = {3.14f, 2.718f},
.flags = 0xABCD1234,
.counts = {-100, 100},
.active = true
};
err = nvs_set_blob(storage_handle, "test_data", &test_data, sizeof(test_data_t));
int32_t num;
ESP_ERROR_CHECK(nvs_get_i32(storage_handle, "num", &num));
ESP_LOGI(TAG, "从NVS读取到的整数: %d", num);
size_t len = 0;
err = nvs_get_str(storage_handle, "str", NULL, &len);
if(err == ESP_OK){
char* message = malloc(len); // 分配足够的内存来存储字符串
ESP_ERROR_CHECK(nvs_get_str(storage_handle, "str", message, &len));
ESP_LOGI(TAG, "从NVS读取到的字符串: %s", message);
free(message);
}
size_t required_size = 0;
err = nvs_get_blob(storage_handle, "test_data", NULL, &required_size);
if(err == ESP_OK){
test_data_t* test_data = malloc(required_size);
ESP_ERROR_CHECK(nvs_get_blob(storage_handle, "test_data", test_data, &required_size));
ESP_LOGI(TAG, "从NVS读取到的测试数据: ID=%d, Name=%s", test_data->id, test_data->name);
free(test_data);
}
nvs_iterator_t it;
err = nvs_entry_find("nvs", "storage", NVS_TYPE_ANY, &it);
if(err == ESP_OK){
do {
nvs_entry_info_t info;
ESP_ERROR_CHECK(nvs_entry_info(it, &info));
ESP_LOGI(TAG, "命名空间:%s, 找到的键: %s, 类型: %x", info.namespace_name, info.key, info.type);
} while (nvs_entry_next(&it) == ESP_OK);
nvs_release_iterator(it);
}
nvs_stats_t nvs_stats;
ESP_ERROR_CHECK(nvs_get_stats("nvs", &nvs_stats));
ESP_LOGI(TAG, "NVS统计信息 - 已使用条目: %d, 空闲条目:%d, 可用条目: %d, 总条目: %d, 命名空间数量: %d",
nvs_stats.used_entries, nvs_stats.free_entries, nvs_stats.available_entries, nvs_stats.total_entries, nvs_stats.namespace_count);
ESP_ERROR_CHECK(nvs_commit(storage_handle));
nvs_close(storage_handle);
//ESP_ERROR_CHECK(nvs_flash_deinit());
}
2 nvs_basics.h
cpp
#ifndef NVS_BASICS_H
#define NVS_BASICS_H
#include <stdio.h> // 输入输出函数
#include <string.h> // 字符串处理函数
#include "esp_log.h" // ESP32日志函数
#include "FreeRTOS/FreeRTOS.h" // FreeRTOS函数
#include "FreeRTOS/task.h" // FreeRTOS任务管理函数
#include "FreeRTOS/semphr.h" // FreeRTOS信号量管理函数
#include "nvs_flash.h" // NVS Flash函数
void nvs_init(void);
#endif // NVS_BASICS_H
3 mian.c
cpp
#include <stdio.h>
#include "user.h"
#include "nvs_basics.h"
void app_main(void)
{
CONSOLE_REPL_INIT(); // 初始化控制台REPL环境
nvs_init();
while(1)
{
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
以上实现了分区初始化,接着向默认分区存入了一个int32、string和blob数据,接着读取以上数据查看是否正确,最后对分区所有信息进行了一个检索。
代码使用比较简单,就不讲解了。各种函数的定义去查看官方文档或《ESP32实用API指南3》。
三、添加官方nvs控制台命令
打开esp安装目录,例如我的是《E:\esp\v6.0.1\esp-idf\examples\system\console\advanced\components》

可以看到共有三个控制台命令文件夹,其中system在第二章新建工程模板中就已经添加了,现在继续添加nvs。
找到user组件的idf_component.yml,新增nvs的路径依赖,如下。
bash
version: "1.0.0"
dependencies:
cmd_system:
path: ${IDF_PATH}/examples/system/console/advanced/components/cmd_system
cmd_nvs:
path: ${IDF_PATH}/examples/system/console/advanced/components/cmd_nvs
接着在user.c中的控制台初始化程序中注册nvs相关命令。
cpp
/*--------------------------------------------------------------------------*/
/**
* @brief console REPL init
* @param[in] void
* @note
* @return void
*/
/*--------------------------------------------------------------------------*/
void CONSOLE_REPL_INIT(void)
{
esp_console_repl_config_t repl_config = ESP_CONSOLE_REPL_CONFIG_DEFAULT(); // 使用默认REPL配置
esp_console_repl_t *repl = NULL;
#if CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG
esp_console_dev_usb_serial_jtag_config_t usb_serial_jtag_config = ESP_CONSOLE_DEV_USB_SERIAL_JTAG_CONFIG_DEFAULT(); // 使用默认USB串行JTAG配置
ESP_ERROR_CHECK(esp_console_new_repl_usb_serial_jtag(&usb_serial_jtag_config, &repl_config, &repl)); // 创建USB串行JTAG REPL环境
#else
esp_console_dev_uart_config_t uart_config = ESP_CONSOLE_DEV_UART_CONFIG_DEFAULT(); // 使用默认UART配置
ESP_ERROR_CHECK(esp_console_new_repl_uart(&uart_config, &repl_config, &repl)); // 创建UART REPL环境
#endif
ESP_ERROR_CHECK(esp_console_start_repl(repl)); // 启动REPL环境
esp_console_register_help_command(); // 注册帮助命令
register_system(); // 注册系统常用命令
register_nvs(); // 注册NVS相关命令
// linenoiseSetDumbMode(1); // 设置linenoise为简单模式,适用于串行终端
}
这样就完成了
四、结果展示

以上是我们对nvs的测试结果。

使用help命令可以发现新增了很多nvs的命令,注意这些命令操控的分区首先要在代码中使用nvs_flash_init进行初始化。