基于ESP32的桌面小屏幕实战[11]:非易失性存储(NVS)
项目位置:~/esp/esp-idf/examples/storage/nvs_rw_blob
我把它复制到了 ~/esp/demo4
1. 基本概念
非易失性存储(Non-Volatile Storage,NVS)是 ESP-IDF 中提供的一种轻量级存储解决方案,专门用于在设备重启或断电后保留数据。NVS 是通过使用 Flash 存储实现的,主要用于存储小型的键值对(key-value pairs),例如 Wi-Fi 配置、用户设置、计数器等不需要频繁更新但需要持久保存的数据。
KeyMap 存储是指将键值对映射存储在某种数据结构中,以便快速查找和管理键值对数据。通常,键(Key)表示一个唯一标识符,值(Value)表示与该键关联的数据。
2. 源码
包含头文件
#include <stdio.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" //定义一个宏 STORAGE_NAMESPACE,值为字符串 "storage"
/* 根据目标平台条件配置 BOOT_MODE_PIN 的值
* 这种条件编译方法允许在不同的硬件平台上使用相同代码 */
#if CONFIG_IDF_TARGET_ESP32C3 //如果编译选项 CONFIG_IDF_TARGET_ESP32C3 为 true(表明当前平台为 ESP32-C3)
#define BOOT_MODE_PIN GPIO_NUM_9
#else
#define BOOT_MODE_PIN GPIO_NUM_0
#endif //CONFIG_IDF_TARGET_ESP32C3
定义一个名为 save_restart_counter 的函数,用于保存模块重启次数到非易失性存储(NVS)。具体步骤包括读取已存储的重启计数、将计数加1、写回并提交。
esp_err_t save_restart_counter(void) //函数返回值类型是 esp_err_t,表示操作成功或错误代码
{
nvs_handle_t my_handle; // my_handle 是 NVS 句柄,用于访问存储数据的命名空间
esp_err_t err; // err 用于接收并检查每一步的操作状态
// Open
err = nvs_open(STORAGE_NAMESPACE, NVS_READWRITE, &my_handle);//用 nvs_open 打开名为 STORAGE_NAMESPACE 的 NVS 命名空间,权限为读写
if (err != ESP_OK) return err;//无法打开(例如命名空间不存在),则返回错误
// Read
int32_t restart_counter = 0; // value will default to 0, if not set yet in NVS
/* 使用 nvs_get_i32 函数从 NVS 中读取 restart_counter 值,初始值为 0。
* 如果密钥 "restart_conter" 未找到,则 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;
// Write
restart_counter++;
err = nvs_set_i32(my_handle, "restart_conter", restart_counter);//使用 nvs_set_i32 将新的计数器值写入 NVS
if (err != ESP_OK) return err;//如果写入失败,函数返回错误
// Commit written value.
// After setting any values, nvs_commit() must be called to ensure changes are written
// to flash storage. Implementations may write to storage at other times,
// but this is not guaranteed.
err = nvs_commit(my_handle);//调用 nvs_commit 将更改提交到闪存中,确保数据已写入
if (err != ESP_OK) return err;//如果提交失败,返回错误
// Close
nvs_close(my_handle);//调用 nvs_close 关闭 NVS 句柄,释放资源
return ESP_OK;
}
- 在编程中,句柄(Handle)是一种特殊的变量或指针,用来间接引用系统资源。句柄本质上是一个唯一标识符,指向特定资源,使程序可以通过它来访问或管理该资源,而不需要直接操作资源的底层细节。句柄在许多操作系统和库中广泛使用,用于管理文件、内存、设备、进程、网络连接等各种资源。
- 句柄的工作流程通常如下:
- 初始化或打开资源:调用相应的函数,生成句柄。例如
nvs_open函数将会打开 NVS 分区,并返回一个句柄。 - 通过句柄操作资源:程序使用句柄来执行资源的各种操作,如读取、写入等。句柄充当资源的“钥匙”。
- 释放资源:操作结束后,通过句柄释放或关闭资源。例如,在代码中
nvs_close(my_handle)用来释放 NVS 句柄。
- 初始化或打开资源:调用相应的函数,生成句柄。例如
- 句柄的优点是让程序以一种更安全的方式管理系统资源,同时隐藏底层实现细节,提高代码的可维护性和平台兼容性。
定义一个名为 save_run_time 的函数,用于将新运行时间的值保存到 NVS(非易失性存储)中。实现的方式是读取之前保存的时间数据,将新数据添加到表的末尾,并将整个更新后的数据表重新写入 NVS。
esp_err_t save_run_time(void) //返回值类型是 esp_err_t,用于表示操作是否成功或返回错误代码
{
/* 定义 NVS 句柄和错误变量 */
nvs_handle_t my_handle;
esp_err_t err;
// Open
err = nvs_open(STORAGE_NAMESPACE, NVS_READWRITE, &my_handle);//调用nvs_open打开NVS命名空间STORAGE_NAMESPACE,权限为读写
if (err != ESP_OK) return err;//如果无法打开,则返回err
// Read the size of memory space required for blob
size_t required_size = 0; // value will default to 0, if not set yet in NVS
/* 使用 nvs_get_blob 函数获取键 "run_time" 关联的 blob 数据的大小。
* 参数 NULL 指示仅读取大小而不实际读取内容,结果存储在 required_size 中。
* 如果该键不存在,则 required_size 将保持为 0。*/
err = nvs_get_blob(my_handle, "run_time", NULL, &required_size);
if (err != ESP_OK && err != ESP_ERR_NVS_NOT_FOUND) return err;
// Read previously saved blob if available
/* 声明一个 uint32_t 类型的指针变量 run_time 指向 malloc 分配的内存块 */
uint32_t* run_time = malloc(required_size + sizeof(uint32_t));
if (required_size > 0) {//如果键 "run_time" 关联的 blob 数据存在
err = nvs_get_blob(my_handle, "run_time", run_time, &required_size);//从 NVS 中读取 "run_time" 关联的 blob 数据
if (err != ESP_OK) {
free(run_time);//如果读取失败,释放内存并返回错误
return err;
}
}
// Write value including previously saved blob if available
required_size += sizeof(uint32_t);//通过增加required_size使得分配的数组空间容纳新数据
/* 使用 xTaskGetTickCount() * portTICK_PERIOD_MS 获取当前运行时间,将其作为数组的最后一个元素。*/
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_set_blob 将更新后的数组写入 NVS
free(run_time);//完成后,释放分配的内存
if (err != ESP_OK) return err;
// Commit
err = nvs_commit(my_handle);//使用 nvs_commit 提交更改,确保新数据写入闪存
if (err != ESP_OK) return err;
// Close
nvs_close(my_handle);//使用 nvs_close 关闭 NVS 句柄以释放资源
return ESP_OK;
}
- Blob 是 Binary Large Object 的缩写,表示二进制大对象。在编程和数据库领域中,Blob 通常用于存储大量的二进制数据,比如图像、音频、视频、文件、序列化数据等,数据内容可以是任何格式,而不仅仅是文本。由于 Blob 数据的内容是二进制的,它不受特定数据格式的限制,因此可以灵活地存储各种类型的数据。
- Blob 的特点
- 格式灵活:Blob 可以存储多种类型的数据,不要求数据是结构化的。
- 大容量:Blob 通常用于存储大体积的数据,尤其是超过传统文本字段限制的数据。
- 操作接口:存储和读取 Blob 数据时,通常通过特定的接口来操作二进制数据,以保证数据的一致性和完整性。
从 NVS 中读取之前存储的 "restart_conter" 和 "run_time" 数据
esp_err_t print_what_saved(void)
{
nvs_handle_t my_handle;
esp_err_t err;
// Open
err = nvs_open(STORAGE_NAMESPACE, NVS_READWRITE, &my_handle);//用nvs_open打开NVS中名为STORAGE_NAMESPACE的存储空间,权限为读写
if (err != ESP_OK) return err;
// Read restart counter
int32_t restart_counter = 0; // value will default to 0, if not set yet in NVS
err = nvs_get_i32(my_handle, "restart_conter", &restart_counter);//从NVS中读取"restart_conter"的值,并将其存储在restart_counter中
if (err != ESP_OK && err != ESP_ERR_NVS_NOT_FOUND) return err;
printf("Restart counter = %d\n", restart_counter);
// Read run time blob
size_t required_size = 0; // value will default to 0, if not set yet in NVS
// obtain required memory space to store blob being read from NVS
// 从 NVS 中读取一个 blob 类型的数据
// "run_time" 是存储的数据的键名,NULL 表示我们不需要读取数据内容,只是想知道所需的内存大小。
err = nvs_get_blob(my_handle, "run_time", NULL, &required_size);
if (err != ESP_OK && err != ESP_ERR_NVS_NOT_FOUND) return err;//如果 nvs_get_blob 出现错误,函数会立即返回错误
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);//从"run_time"中读取数据并存储到run_time数组中
if (err != ESP_OK) {//如果读取失败,释放分配的内存并返回错误
free(run_time);
return err;
}
/* 如果读取成功,使用一个 for 循环遍历 run_time 数组并打印每个值。
数组的大小是通过 required_size / sizeof(uint32_t) 计算得到的,
因为每个元素是一个 uint32_t 类型(通常为 4 字节)*/
for (int i = 0; i < required_size / sizeof(uint32_t); i++) {
printf("%d: %d\n", i + 1, run_time[i]);
}
free(run_time);
}
// Close
nvs_close(my_handle);//关闭 NVS 句柄,释放资源
return ESP_OK;
}
主函数用来管理设备的启动计数、运行时间记录和重启逻辑。
void app_main(void)
{
/* 初始化 NVS 存储 */
esp_err_t err = nvs_flash_init();//初始化 NVS
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
// 如果初始化时返回 ESP_ERR_NVS_NO_FREE_PAGES 或 ESP_ERR_NVS_NEW_VERSION_FOUND,
// 这意味着 NVS 分区被截断或版本不兼容,可能需要重新格式化。
// NVS partition was truncated and needs to be erased
// Retry nvs_flash_init
ESP_ERROR_CHECK(nvs_flash_erase());//清空 NVS 存储
err = nvs_flash_init();//重新初始化
}
ESP_ERROR_CHECK( err );
/* 读取并打印已保存的重启计数器和运行时间表 */
err = print_what_saved();// 从 NVS 中读取并打印保存的重启计数器和运行时间表
if (err != ESP_OK) printf("Error (%s) reading data from NVS!\n", esp_err_to_name(err));
/* 增加重启计数器并保存 */
err = save_restart_counter();// 在 NVS 中将重启计数器值增加并保存
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);// 将引脚 BOOT_MODE_PIN 重置到默认状态
gpio_set_direction(BOOT_MODE_PIN, GPIO_MODE_INPUT);// 设置引脚方向为输入,以便读取其电平状态。
/* Read the status of GPIO0. If GPIO0 is LOW for longer than 1000 ms,
then save module's run time and restart it
监测 GPIO 引脚状态并判断是否重启
*/
while (1) {
if (gpio_get_level(BOOT_MODE_PIN) == 0) { // 检测该引脚是否为低电平(按下状态)
vTaskDelay(1000 / portTICK_PERIOD_MS);
if(gpio_get_level(BOOT_MODE_PIN) == 0) {
err = save_run_time(); // 保存当前的运行时间到 NVS。如果保存出错,打印错误信息。
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);// 如果 BOOT_MODE_PIN 引脚未被按下,延迟 200 毫秒后重新检测
}
}
- GPIO 是 General-Purpose Input/Output 的缩写,表示通用输入/输出,是一种常用于微控制器(如 ESP32)和单板计算机(如 Raspberry Pi)上的可编程引脚,用来和外部硬件(如传感器、LED、电机等)进行通信。
3. 编译工程
在终端中输入
idf.py fullclean
idf.py clean
idf.py build
如果出现idf.py: command not found需要重新安装编译链,然后设置环境
cd ~/esp/esp-idf
export IDF_GITHUB_ASSETS="dl.espressif.com/github_assets"
./install.sh
. /home/xzh/esp/esp-idf/export.sh
回到项目路径
cd ~/esp/demo4
再次在终端中输入
idf.py fullclean
idf.py clean
idf.py build
出现下图表示编译成功

4. 下载运行
将板子接入电脑,输入 ls /dev/ttyUSB* 查看设备号

下载程序时按住IO0,轻按RST,2个按键同时放开,在终端中输入命令:
idf.py -p /dev/ttyUSB0 flash monitor
当出现下图时,再按一下RST键,程序就能开始运行

程序运行成功会显示下图这样

按一下RST,会显示

再按一下RST,会显示

说明程序已经成功将重启计数器 restart_counter 的值从 0 增加到 1,并在重启后打印了该值。不过,Run time 部分仍然显示 "Nothing saved yet!",这表明 run_time 的数据尚未保存。
长按IO0,首先会显示重启

然后可以看到

再次按下RST,可以看到

再次按下IO0,可以看到


1: 和 2: 后面的 159630 和 3830 代表了保存的运行时间(以毫秒为单位)。这些时间值是在你长按 BOOT_MODE_PIN 触发 save_run_time 函数时记录的,表示从系统启动到按下按钮的经过时间。

浙公网安备 33010602011771号