通俗的来说,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_PAGES
或ESP_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);
这一行的作用是 增加空间 来存储一个新的时间戳。我们逐步分析这行代码:
-
required_size
变量最初保存了从 NVS 中读取的 Blob 数据 的大小。在之前的代码中,nvs_get_blob()
会返回当前存储的数据的大小,并将其保存在required_size
变量中。 -
sizeof(uint32_t)
是每个时间戳占用的内存空间大小(4 字节,uint32_t
是 32 位无符号整数类型)。因此,required_size += sizeof(uint32_t);
的意思是:为即将添加的新时间戳增加空间,即让required_size
表示增加一个新的时间戳后所需要的总内存大小。 -
例如,如果
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
数组中。
-
run_time
是一个动态分配的内存数组,大小为required_size
字节。由于每个时间戳占用sizeof(uint32_t)
字节(4 字节),因此可以使用required_size / sizeof(uint32_t)
来计算出当前run_time
数组中已经存储了多少个时间戳。 -
required_size / sizeof(uint32_t)
会返回当前数组中已经存储的时间戳的数量。假设required_size
的当前值是 16 字节,那么此时required_size / sizeof(uint32_t)
就等于 4,表示当前数组中已经存储了 4 个时间戳。 -
run_time[required_size / sizeof(uint32_t) - 1]
这个索引就是新时间戳将要存储的位置。假设required_size
为 16 字节(4 个uint32_t
时间戳),那么required_size / sizeof(uint32_t) - 1
会等于 3,即新时间戳将被存储在run_time[3]
(第四个位置)。 -
xTaskGetTickCount() * portTICK_PERIOD_MS
会返回从系统启动以来的运行时间(单位为毫秒)。xTaskGetTickCount()
返回的是一个以 ticks 为单位的值,而portTICK_PERIOD_MS
是一个常量,它表示每个系统 tick 的时间(单位为毫秒)。乘积的结果就是当前的运行时间,单位为毫秒。 -
run_time[required_size / sizeof(uint32_t) - 1] = xTaskGetTickCount() * portTICK_PERIOD_MS;
就是将新的时间戳(系统启动以来的时