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

相关推荐
Mars-xq15 小时前
vscode 开发Android
android·ide·vscode
嵌入式小站16 小时前
STM32 可移植教程 01:VSCode 环境搭建 + 点亮 LED(实战篇)
vscode·stm32·嵌入式硬件
Mars-xq16 小时前
VSCode 开发 Android 时,类、方法无法跳转
android·ide·vscode
Mars-xq17 小时前
VSCode 开发Android 新手必装插件清单
android·ide·vscode
xskukuku1 天前
使用VSCode配置C语言运行环境
c语言·ide·vscode
小王C语言1 天前
vscode智能提示问题、跳转问题
ide·vscode·编辑器
郝亚军1 天前
如何在vscode上运行python程序
ide·vscode·编辑器
Arvin.Angela1 天前
VsCode 安装文档
ide·vscode·编辑器
CAir22 天前
copilot配置deepseek和skills
vscode·copilot·skill·deepseek
独隅2 天前
Visual Studio Code 和 Visual Studio 2026 两大开发工具的核心差异
java·vscode·visual studio