ESP32-C3 入门笔记06:存储 API - 非易失性存储库 (NVS) (读/写/删除数据)

在这里插入图片描述

通俗的来说,NVS 就是在 flash 上分配的一块内存空间 ,提供给用户保存掉电不丢失的数据 。

在 ESP32-C3 上使用 ESP-IDF 5.2.3 版本框架开发时,如果要实现 EEPROM 掉电存储功能,可以使用 NVS (Non-Volatile Storage) 库。ESP32 并没有直接支持 EEPROM,但是 NVS 库提供了类似 EEPROM 的功能,它可以用来保存少量的非易失性数据,存储内容在设备掉电后不会丢失。

在这里插入图片描述

在这里插入图片描述

应用示例

nvs_rw_value

演示如何读取及写入 NVS 单个整数值。

此示例中的值表示 ESP32 模组重启次数。NVS 中数据不会因为模组重启而丢失,因此只有将这一值存储于 NVS 中,才能起到重启次数计数器的作用。

该示例也演示了如何检测读取/写入操作是否成功,以及某个特定值是否在 NVS 中尚未初始化。诊断程序以纯文本形式提供,有助于追踪程序流程,及时发现问题。

#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"

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 分区被截断(没有足够的空间)或版本不匹配,则需要擦除分区并重新初始化
        ESP_ERROR_CHECK(nvs_flash_erase());  // 擦除分区
        err = nvs_flash_init();  // 重新初始化 NVS
    }
    ESP_ERROR_CHECK( err );  // 检查是否初始化成功

    // 打开名为 "storage" 的 NVS 存储区,NVS_READWRITE 表示读写权限
    printf("\n");
    printf("Opening Non-Volatile Storage (NVS) handle... ");
    nvs_handle_t my_handle;
    err = nvs_open("storage", NVS_READWRITE, &my_handle); // 打开NVS
    if (err != ESP_OK) {
        // 如果打开 NVS 存储区失败,输出错误信息
        printf("Error (%s) opening NVS handle!\n", esp_err_to_name(err));
    } else {
        // 如果打开成功,输出提示
        printf("Done\n");

        // 从 NVS 读取 "restart_counter" 的值
        printf("Reading restart counter from NVS ... ");
        int32_t restart_counter = 0;  // 如果没有初始化,值默认为 0
        err = nvs_get_i32(my_handle, "restart_counter", &restart_counter);// 读取NVS
        switch (err) {
            case ESP_OK:  // 读取成功
                printf("Done\n");
                printf("Restart counter = %" PRIu32 "\n", restart_counter);  // 打印计数器值
                break;
            case ESP_ERR_NVS_NOT_FOUND:  // 如果没有找到 "restart_counter" 键(第一次运行)
                printf("The value is not initialized yet!\n");
                break;
            default :  // 其他错误
                printf("Error (%s) reading!\n", esp_err_to_name(err));
        }

        // 更新 "restart_counter" 的值
        printf("Updating restart counter in NVS ... ");
        restart_counter++;  // 增加计数器
        err = nvs_set_i32(my_handle, "restart_counter", restart_counter);  // 写入NVS
        printf((err != ESP_OK) ? "Failed!\n" : "Done\n");

        // 提交更改到 NVS 存储
        printf("Committing updates in NVS ... ");
        err = nvs_commit(my_handle);  // 提交NVS
        printf((err != ESP_OK) ? "Failed!\n" : "Done\n");

        // 关闭 NVS 存储区,释放资源
        nvs_close(my_handle); // 关闭 NVS
    }

    printf("\n");

    // 模拟重启过程,倒计时 10 秒后重启模块
    for (int i = 10; i >= 0; i--) {
        printf("Restarting in %d seconds...\n", i);
        vTaskDelay(1000 / portTICK_PERIOD_MS);  // 延迟 1 秒
    }
    printf("Restarting now.\n");
    fflush(stdout);  // 刷新输出缓冲区
    esp_restart();  // 重启 ESP32
}

输出效果

在这里插入图片描述

注释概述:

  • 初始化 NVS: 初始化非易失性存储(NVS),并确保存储区没有问题(例如需要擦除)。
  • 打开 NVS 存储区: 打开 "storage" 存储区以便进行读写操作。
  • 读取数据: 读取存储区中的 restart_counter 键的值。如果数据没有找到,则返回默认值 0,并提示用户。
  • 写入数据: 更新 restart_counter 的值,并写入 NVS 存储区。
  • 提交更改: 调用 nvs_commit() 确保所有更改都被写入闪存。
  • 关闭 NVS: 关闭存储区,释放资源。
  • 重启过程: 模拟设备重启,在重启前倒计时并打印信息,最后调用 esp_restart() 实际重启设备。

这段代码展示了如何在 ESP32 上使用 NVS(非易失性存储)来读取和写入数据,特别是一个“重启计数器”的例子。它演示了如何初始化 NVS、读取存储的数据、更新计数器、提交修改并关闭 NVS 存储区。以下是代码逐步解释:

代码解析

1. 初始化 NVS
esp_err_t err = nvs_flash_init();
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
    // NVS 分区被截断,可能需要擦除以便重新初始化
    ESP_ERROR_CHECK(nvs_flash_erase());
    err = nvs_flash_init();
}
ESP_ERROR_CHECK( err );
  • nvs_flash_init() 初始化 NVS 存储。第一次调用时,它会加载存储区域。如果存储区域有问题或不兼容,会抛出 ESP_ERR_NVS_NO_FREE_PAGESESP_ERR_NVS_NEW_VERSION_FOUND 错误。此时,我们会擦除存储并重新初始化。
2. 打开 NVS 存储区
nvs_handle_t my_handle;
err = nvs_open("storage", NVS_READWRITE, &my_handle);
if (err != ESP_OK) {
    printf("Error (%s) opening NVS handle!\n", esp_err_to_name(err));
} else {
    printf("Done\n");
}
  • 使用 nvs_open() 打开名为 "storage" 的 NVS 存储区,并提供读写权限 (NVS_READWRITE)。
  • 如果打开失败,输出错误信息。
3. 读取数据
int32_t restart_counter = 0;
err = nvs_get_i32(my_handle, "restart_counter", &restart_counter);
switch (err) {
    case ESP_OK:
        printf("Done\n");
        printf("Restart counter = %" PRIu32 "\n", restart_counter);
        break;
    case ESP_ERR_NVS_NOT_FOUND:
        printf("The value is not initialized yet!\n");
        break;
    default:
        printf("Error (%s) reading!\n", esp_err_to_name(err));
}
  • 使用 nvs_get_i32() 读取存储在 "restart_counter" 键下的整数。如果读取成功,会输出当前计数器的值。如果没有找到数据(第一次读取),会输出“值尚未初始化”提示。
4. 写入数据
restart_counter++;
err = nvs_set_i32(my_handle, "restart_counter", restart_counter);
printf((err != ESP_OK) ? "Failed!\n" : "Done\n");
  • 更新 restart_counter 的值,并使用 nvs_set_i32() 写入新的计数值。
5. 提交修改
err = nvs_commit(my_handle);
printf((err != ESP_OK) ? "Failed!\n" : "Done\n");
  • 使用 nvs_commit() 确保对 NVS 的修改被写入闪存。调用此函数后,数据会持久化到存储中。
6. 关闭 NVS 存储区
nvs_close(my_handle);
  • 完成操作后,使用 nvs_close() 关闭 NVS 存储区句柄,释放资源。
7. 重启模块
for (int i = 10; i >= 0; i--) {
    printf("Restarting in %d seconds...\n", i);
    vTaskDelay(1000 / portTICK_PERIOD_MS);
}
printf("Restarting now.\n");
fflush(stdout);
esp_restart();
  • 输出一个倒计时,等待 10 秒后重启模块。esp_restart() 会触发模块重启,重新加载应用。

总结:

  • 这段代码展示了如何使用 ESP32 的 NVS 存储来保存一个重启计数器的值,并在设备重启后读取和更新该值。
  • 使用 nvs_flash_init() 来初始化存储,nvs_open() 打开存储区,nvs_get_*nvs_set_* 用于读写数据,nvs_commit() 用于提交更改,nvs_close() 关闭存储。
  • 该代码可以在 ESP32 中用于保存小型配置数据,并在掉电或重启后恢复数据。

如果你想在实际项目中使用这段代码,可以根据需要修改数据键名和数据类型,扩展功能以满足你的需求。


nvs_rw_blob

演示如何读取及写入 NVS 单个整数值和 BLOB(二进制大对象),并在 NVS 中存储这一数值,即便 ESP32 模组重启也不会消失。

  • value - 记录 ESP32 模组软重启次数和硬重启次数。

  • blob - 内含记录模组运行次数的表格。此表格将被从 NVS 读取至动态分配的 RAM 上。每次手动软重启后,表格内运行次数即增加一次,新加的运行次数被写入 NVS。下拉 GPIO0 即可手动软重启。

该示例也演示了如何执行诊断程序以检测读取/写入操作是否成功。

以下是对该代码的详细注释:

#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 "driver/gpio.h"

#define STORAGE_NAMESPACE "storage"  // 定义NVS存储空间的命名空间

#if CONFIG_IDF_TARGET_ESP32C3
#define BOOT_MODE_PIN GPIO_NUM_9  // 根据不同目标板选择GPIO引脚(ESP32-C3使用GPIO_NUM_9)
#else
#define BOOT_MODE_PIN GPIO_NUM_0  // 默认使用GPIO_NUM_0(通常用于启动模式选择)
#endif //CONFIG_IDF_TARGET_ESP32C3

/* 保存模块重启计数到NVS中
   首先读取已保存的计数值,然后递增计数器
   如果过程中出现错误,则返回错误
 */
esp_err_t save_restart_counter(void)
{
    nvs_handle_t my_handle;
    esp_err_t err;

    // 打开NVS存储,进行读写操作
    err = nvs_open(STORAGE_NAMESPACE, NVS_READWRITE, &my_handle);
    if (err != ESP_OK) return err;

    // 读取重启计数器,如果未初始化,默认为0
    int32_t restart_counter = 0;
    err = nvs_get_i32(my_handle, "restart_conter", &restart_counter);
    if (err != ESP_OK && err != ESP_ERR_NVS_NOT_FOUND) return err;

    // 增加重启计数器
    restart_counter++;
    err = nvs_set_i32(my_handle, "restart_conter", restart_counter);
    if (err != ESP_OK) return err;

    // 提交更改到NVS
    err = nvs_commit(my_handle);
    if (err != ESP_OK) return err;

    // 关闭NVS存储
    nvs_close(my_handle);
    return ESP_OK;
}
/** 保存新的运行时间值到NVS中
*   首先读取之前保存的时间表,然后将新的值添加到表末尾
*   如果过程中出现错误,则返回错误
**/
esp_err_t save_run_time(void)
{
    nvs_handle_t my_handle;
    esp_err_t err;

    // 打开NVS存储,进行读写操作
    err = nvs_open(STORAGE_NAMESPACE, NVS_READWRITE, &my_handle);
    if (err != ESP_OK) return err;

    // 读取之前保存的 "run_time" Blob数据大小
	//读nvs,读取键值对为 "run_time" 处的内容放入变量 required_size
    size_t required_size = 0;
	
    /*先读取1次但第3个参数输出地址使用的是 NULL,
    表示读出的数据不保存,因为这里使用只是为了看一下 "run_time" 处是否
    有数据,只是先读一下数据,看一下读完以后 required_size 还是不是”0“
    */
    err = nvs_get_blob(my_handle, "run_time", NULL, &required_size);// 读取NVS
    if (err != ESP_OK && err != ESP_ERR_NVS_NOT_FOUND) return err;

   //这里申请一块地址,定义为 run_time ,地址上存放的示 uint32_t 数据,大小为:required_size 大小 + sizeof(uint32_t) 
    uint32_t* run_time = malloc(required_size + sizeof(uint32_t));  // 为新的Blob数据分配空间
	
	// 读取之前保存的 Blob 数据(如果有的话)就先读出来,保存在刚才申请的 地址 run_time 处(第3个参数)。
    if (required_size > 0) {
        err = nvs_get_blob(my_handle, "run_time", run_time, &required_size);// 读取NVS 保存在 run_time
        if (err != ESP_OK) {
            free(run_time);
            return err;
        }
    }

    required_size += sizeof(uint32_t);// 增加一个新的时间戳后所需要的总内存大小(4字节)
	
	// 写入新的运行时间,并追加到已保存的Blob数据末尾 
	// 数组形式保存数据:类似于 uint32_t run_time[] 数组给数组赋值
    run_time[required_size / sizeof(uint32_t) - 1] = xTaskGetTickCount() * portTICK_PERIOD_MS;
    err = nvs_set_blob(my_handle, "run_time", run_time, required_size);// 写入NVS
    free(run_time);  // 释放内存

    if (err != ESP_OK) return err;

    // 提交更改到NVS
    err = nvs_commit(my_handle);// 提交NVS
    if (err != ESP_OK) return err;

    // 关闭NVS存储
    nvs_close(my_handle); // 关闭NVS
    return ESP_OK;
}

/* 从NVS中读取并打印重启计数器和运行时间表
   如果过程中出现错误,则返回错误
 */
esp_err_t print_what_saved(void)
{
    nvs_handle_t my_handle;
    esp_err_t err;

    // 打开NVS存储,进行读写操作
    err = nvs_open(STORAGE_NAMESPACE, NVS_READWRITE, &my_handle); // 打开 NVS
    if (err != ESP_OK) return err;

    // 读取并打印重启计数器
    int32_t restart_counter = 0;
    err = nvs_get_i32(my_handle, "restart_conter", &restart_counter);// 读取 NVS
    if (err != ESP_OK && err != ESP_ERR_NVS_NOT_FOUND) return err;
    printf("Restart counter = %" PRIu32 "\n", restart_counter);

    // 读取并打印运行时间的Blob数据
    size_t required_size = 0;
    err = nvs_get_blob(my_handle, "run_time", NULL, &required_size); // 读取 NVS
    if (err != ESP_OK && err != ESP_ERR_NVS_NOT_FOUND) return err;
    printf("Run time:\n");
    if (required_size == 0) {
        printf("Nothing saved yet!\n");
    } else {
        uint32_t* run_time = malloc(required_size);
        err = nvs_get_blob(my_handle, "run_time", run_time, &required_size);// 读取 NVS
        if (err != ESP_OK) {
            free(run_time);
            return err;
        }
        // 打印每个保存的运行时间
        for (int i = 0; i < required_size / sizeof(uint32_t); i++) {
            printf("%d: %" PRIu32 "\n", i + 1, run_time[i]);
        }
        free(run_time);  // 释放内存
    }

    // 关闭NVS存储
    nvs_close(my_handle);// 关闭 NVS
    return ESP_OK;
}


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分区被截断,或者版本不匹配,则擦除分区并重新初始化
        ESP_ERROR_CHECK(nvs_flash_erase());
        err = nvs_flash_init();
    }
    ESP_ERROR_CHECK( err );

    // 3.打印NVS中的保存数据
    err = print_what_saved();
    if (err != ESP_OK) printf("Error (%s) reading data from NVS!\n", esp_err_to_name(err));

    // 1.保存重启计数到NVS
    err = save_restart_counter();
    if (err != ESP_OK) printf("Error (%s) saving restart counter to NVS!\n", esp_err_to_name(err));

    // 配置GPIO引脚
    gpio_reset_pin(BOOT_MODE_PIN);
    gpio_set_direction(BOOT_MODE_PIN, GPIO_MODE_INPUT);

    /* 监测BOOT_MODE_PIN(GPIO0)引脚状态
       如果GPIO0为低电平持续超过1000ms,则保存运行时间并重启
     */
    while (1) {
        if (gpio_get_level(BOOT_MODE_PIN) == 0) {
            vTaskDelay(1000 / portTICK_PERIOD_MS);  // 等待1秒钟
            if(gpio_get_level(BOOT_MODE_PIN) == 0) {  // 如果引脚仍为低电平
                //2. 保存运行时间并重启
                err = save_run_time();
                if (err != ESP_OK) printf("Error (%s) saving run time blob to NVS!\n", esp_err_to_name(err));
                printf("Restarting...\n");
                fflush(stdout);
                esp_restart();  // 重启设备
            }
        }
        vTaskDelay(200 / portTICK_PERIOD_MS);  // 每200ms检查一次
    }
}


save_run_time()函数

怎么理解下面代码?

    required_size += sizeof(uint32_t); 
    run_time[required_size / sizeof(uint32_t) - 1] = xTaskGetTickCount() * portTICK_PERIOD_MS;怎么理解 

这两行代码在处理 NVS 存储的 Blob 数据时,用于更新存储的运行时间列表并添加新的时间戳。具体来看,它们的作用是:

required_size += sizeof(uint32_t);

这一行的作用是 增加空间 来存储一个新的时间戳。我们逐步分析这行代码:

  1. required_size 变量最初保存了从 NVS 中读取的 Blob 数据 的大小。在之前的代码中,nvs_get_blob() 会返回当前存储的数据的大小,并将其保存在 required_size 变量中。

  2. sizeof(uint32_t) 是每个时间戳占用的内存空间大小(4 字节,uint32_t 是 32 位无符号整数类型)。因此,required_size += sizeof(uint32_t); 的意思是:为即将添加的新时间戳增加空间,即让 required_size 表示增加一个新的时间戳后所需要的总内存大小。

  3. 例如,如果 required_size 原来表示存储了 3 个时间戳的数据(3 × 4 字节 = 12 字节),那么通过 required_size += sizeof(uint32_t) 后,required_size 会增加 4 字节,表示需要为新添加的时间戳留出空间。此时 required_size 变为 16 字节。

run_time[required_size / sizeof(uint32_t) - 1] = xTaskGetTickCount() * portTICK_PERIOD_MS;

这一行是将新的时间戳添加到 run_time 数组中。

  1. run_time 是一个动态分配的内存数组,大小为 required_size 字节。由于每个时间戳占用 sizeof(uint32_t) 字节(4 字节),因此可以使用 required_size / sizeof(uint32_t) 来计算出当前 run_time 数组中已经存储了多少个时间戳。

  2. required_size / sizeof(uint32_t) 会返回当前数组中已经存储的时间戳的数量。假设 required_size 的当前值是 16 字节,那么此时 required_size / sizeof(uint32_t) 就等于 4,表示当前数组中已经存储了 4 个时间戳。

  3. run_time[required_size / sizeof(uint32_t) - 1] 这个索引就是新时间戳将要存储的位置。假设 required_size 为 16 字节(4 个 uint32_t 时间戳),那么 required_size / sizeof(uint32_t) - 1 会等于 3,即新时间戳将被存储在 run_time[3](第四个位置)。

  4. xTaskGetTickCount() * portTICK_PERIOD_MS 会返回从系统启动以来的运行时间(单位为毫秒)。xTaskGetTickCount() 返回的是一个以 ticks 为单位的值,而 portTICK_PERIOD_MS 是一个常量,它表示每个系统 tick 的时间(单位为毫秒)。乘积的结果就是当前的运行时间,单位为毫秒。

  5. run_time[required_size / sizeof(uint32_t) - 1] = xTaskGetTickCount() * portTICK_PERIOD_MS; 就是将新的时间戳(系统启动以来的时

上一篇:CSRF 跨站请求伪造的实现原理和预防措施


下一篇:NLP论文速读|Describe-then-Reason: 通过视觉理解训练来提升多模态数学的推理