ESP-IDF+vscode开发ESP32第十三讲——NVS

目录

前言

一、NVS梳理

[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控制台命令

四、结果展示


前言

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进行初始化。

相关推荐
一个数据大开发3 小时前
大模型驱动下的数据中台架构演进:从服务化到智能化
大数据·数据仓库·vscode·pycharm
LOOKWORD20214 小时前
vscode配置插件opencode
vscode·opencode
图像僧4 小时前
没有sudo权限也能安装和使用 VSCode
ide·vscode·编辑器
沧海一笑-dj5 小时前
【Tools】Visual Studio Code UNC host ‘192.168.236.128‘ access is not allowed错误
vscode·visual studio code·host·unc·access is not
zhaqonianzhu1 天前
Qoder CN 插件停更与迁移指南:从 VS Code 到通义灵码 IDE
ide·vscode
BU摆烂会噶1 天前
【LangGraph】House_Agent 实战(一):架构与环境配置
人工智能·vscode·python·架构·langchain·人机交互
IceSugarJJ1 天前
Windows下VSCode+ WSL项目启动流程
linux·windows·vscode·ubuntu·wsl
@noNo1 天前
Visual Studio Code添加自定义大模型API
ide·vscode·编辑器
NPE~1 天前
[嵌入式]嵌入式在线仿真平台 —— Wokwi 入门指南
stm32·嵌入式·esp32·教程·平台