ESP-IDF+vscode开发ESP32第十三讲——NVS
目录
一、NVS梳理
1.1 分区 (Partition):NVS 的专属“仓库”
1.2 页面 (Page):仓库里的“货架”
1.3 条目 (Entry):货架上的“最小存储格”
1.4 键值对 (Key-Value Pair):实际存放的“货物”
1.5 命名空间 (Namespace):货物的“分类文件夹”
二、完善工程
1 nvs_basics.c
2 nvs_basics.h
3 mian.c
三、添加官方nvs控制台命令
四、结果展示
五、nvs分区生成程序
5.1 使用函数烧录
5.2 使用python工具烧录
5.3 结果展示
前言
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》也涉及到分区表的使用。
# 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中添加依赖声明
REQUIRES nvs_flash1 nvs_basics.c
#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
#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_H3 mian.c
#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的路径依赖,如下。
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相关命令。
/*--------------------------------------------------------------------------*/ /** * @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进行初始化。
五、nvs分区生成程序
从上文可以看到,我们很容易向nvs分区中写入和读取数据。但是如果我们现在准备了大量的键值对需要存入,那么这种代码程序的写入方式就很低效。于是,我们需要使用NVS 分区生成程序。
NVS 分区生成程序根据 CSV 文件中的键值对生成二进制文件。
首先需要自定义一个CSV文件,储存需要烧录分区的键值对。例如nvs_data.cvs,内容如下:
key,type,encoding,value my_app,namespace,, wifi_ssid,data,string,"MyHomeWiFi" wifi_pass,data,string,"password123" device_id,data,u32,10086关于文件格式见《CSV文件格式》
说明
- 分区生成程序不会对重复键进行检查,而将数据同时写入这两个重复键中。请注意不要使用同名的键;
- 新页面创建后,前一页的空白处不会再写入数据。CSV 文件中的字段须按次序排列以优化内存;
- 暂不支持 64 位数据类型。
注意,前面我们说的分区表也是CSV格式文件,但是作用不相同。
| 对比维度 | 分区表 (Partition Table) | NVS 数据 CSV |
|---|---|---|
| 核心作用 | 定义 Flash 存储空间的物理划分(地址、大小、类型) | 定义要写入 NVS 分区的具体键值对数据 |
| 处理时机 | 编译阶段由构建系统解析,生成partition-table.bin | 烧录前由nvs_partition_gen.py解析,生成数据.bin |
| 必需列名 | Name,Type,SubType,Offset,Size,Flags | key,type,encoding,value |
| 内容示例 | nvs, data, nvs, 0x9000, 0x6000, | wifi_ssid, data, string, "MyWiFi" |
定义好nvs_data.cvs之后,需要将其中的键值对烧录进nvs分区,提供了两者方法,分别是直接使用函数nvs_create_partition_image通过 CMake 创建分区二进制文件和手动调用nvs_partition_gen.py工具
5.1使用函数烧录
打开组件nvs_basics的中的CMakeLists.txt文件,修改如下:
idf_component_register(SRCS "nvs_basics.c" INCLUDE_DIRS "include" REQUIRES nvs_flash) # 分区名:nvs,分区数据文件:nvs_data.csv,分区类型:FLASH_IN_PROJECT nvs_create_partition_image(nvs nvs_data.csv FLASH_IN_PROJECT)nvs_create_partition_image宏命令原型如下:
nvs_create_partition_image(<partition> <csv> [FLASH_IN_PROJECT] [DEPENDS dep1 dep2 ...])
| 参数 | 必填 | 说明 |
|---|---|---|
<partition> | ✅ | 分区名称(必须与partitions.csv中的 Name 字段完全一致,如nvs、my_nvs) |
<csv> | ✅ | NVS 数据源 CSV 文件的路径(支持绝对路径或相对于当前CMakeLists.txt的路径) |
FLASH_IN_PROJECT | ❌ | 添加此标志后,执行idf.py flash时会自动烧录该 NVS 分区到芯片。不加则只生成.bin文件但不烧录 |
DEPENDS | ❌ | 指定额外依赖项。当这些文件发生变化时,触发 NVS 镜像重新生成。常用于 CSV 引用了外部二进制文件的场景 |
注:如果csv文件和CMakeLists.txt文件在同一个路径文件下,<csv>可直接填入csv文件名
5.2 使用python工具烧录
首先找到nvs_partition_gen.py的地址,例如:
E:\esp\v6.0.1\esp-idf\components\nvs_flash\nvs_partition_generator将nvs_partition_gen.py文件粘贴到工程根目录下,接着就可以使用python命令了,命令使用见《NVS 分区生成程序 - ESP32-P4 - — ESP-IDF 编程指南 v6.0.1 文档》
5.3 结果展示
可以看出nvs_data.cvs所有键值对都写入了nvs分区中
