0、前言
在嵌入式物联网设备中,或多或少都会涉猎到数据掉电存储的需求。例如生活中常见的智能锁,如果不能实现数据掉电存储,智能锁的数据仅仅是存储于RAM内存中的话,一但掉电,此前用户所作的修改密码操作也会失效,仅仅有一个最初烧录到程序代码中的初始密码,如果这样的产品上市,也不会有用户愉快的买单。因此数据掉电存储在嵌入式物联网开发中是非常常见的,我们日常生活的许多电子产品也都有数据掉电存储的功能。
提醒:本文使用的是乐鑫官方的ESP-IDF开发工具对ESP32开发,并非Arduino、Micro-python等第三方工具开发。
1、NVS介绍
NVS即Non-volatile storage,是一种非易失性存储技术,用于在嵌入式系统中保存持久化数据。它主要用于在flash存储器中存储键值格式的数据,提供了一种简单且有效的方法来保存和读取配置信息、状态数据、用户设置等应用程序数据。NVS在设备重新启动或断电后能够恢复状态,因此非常适合保存需要长期存储的数据。
NVS库通常通过调用底层的flash API进行读、写、擦除操作,可以对主flash的部分空间进行管理。应用程序可以通过调用NVS的API来选择使用带有NVS标签的分区,或者使用指定名称的任意分区。NVS还支持BLOB(Binary Large OBjects)类型的数据存储,但有一定的大小限制,通常取决于分区的总大小和预留空间。
通俗的来说,NVS存储 就是在内置的 flash 上分配的一块内存空间 ,提供给用户保存掉电不丢失的数据 。
本文使用的乐鑫ESP32系列芯片中,提供了非易失性存储(NVS)库,这是一个专为ESP32系列设计的用于保存简单数据的库。如图所示为ESP-IDF开发工具安装路径下的nvs_flash组件库。
NVS 最适合存储一些较小的数据,而非字符串或二进制大对象 (BLOB) 等较大的数据。如需存储较大的 BLOB 或者字符串,则需要考虑使用基于磨损均衡库的 FAT 文件系统。
键值对:NVS 的操作对象为键值对,其中键是 ASCII 字符串,当前支持的最大键长为 15 个字符,键必须唯一。
值可以为以下几种类型:
①、整数型:uint8_t、int8_t、uint16_t、int16_t、uint32_t、int32_t、uint64_t 和 int64_t;
②、以 0 结尾的字符串;
③、可变长度的二进制数据 (BLOB)
字符串值当前上限为 4000 字节,其中包括空终止符。BLOB 值上限为 508,000 字节或分区大小的 97.6% 减去 4000 字节,以较低值为准。
提醒:当前ESP32的NVS并不支持float和double类型的数据存储!!!
命名空间:为了减少不同组件之间键名的潜在冲突,NVS 将每个键值对分配给一个命名空间。命名空间的命名规则遵循键名的命名规则。此外,单个 NVS 分区最多只能容纳 254 个不同的命名空间。
2、常用的NVS API说明
在ESP32官方参考指南中,选择好对应的芯片型号和开发工具版本后,找到API参考目录下的存储API,可以看到官方对非易失性存储库的介绍。同一个开发工具版本的不同芯片API参考库说明变动不大。如下所示为乐鑫官网的存储API部分内容。
如下所述API所在的头文件路径:components/nvs_flash/include/nvs_flash.h
①、nvs_flash_init
esp_err_t nvs_flash_init(void);
//初始化默认的NVS分区,每次操作NVS分区都需要先初始化NVS
//此 API 初始化默认的 NVS 分区。默认的 NVS 分区是在分区表中标记为“nvs”的分区。
//在 menuconfig 中启用“NVS_ENCRYPTION”时,此 API 会为默认 NVS 分区启用 NVS 加密。
//初始化成功返回 ESP_OK。
②、nvs_flash_init_partition
esp_err_t nvs_flash_init_partition(const char *partition_label);
//初始化指定分区的 NVS 闪存。
//参数: partition_label – [in] 分区的标签。长度不得超过 16 个字符。
//初始化成功返回 ESP_OK。
③、nvs_flash_deinit
esp_err_t nvs_flash_deinit(void);
//取消初始化默认 NVS 分区的 NVS 存储。
//默认 NVS 分区是分区表中带有“nvs”标签的分区。
//成功时返回ESP_OK(存储已取消初始化),否则返回ESP_ERR_NVS_NOT_INITIALIZED在此调用之前是否未初始化存储
④、nvs_flash_deinit_partition
esp_err_t nvs_flash_deinit_partition(const char *partition_label);
//取消初始化给定 NVS 分区的 NVS 存储。
//参数: partition_label – [in] 分区的标签
//成功时返回ESP_OK,否则返回ESP_ERR_NVS_NOT_INITIALIZED 在此调用之前是否未初始化给定分区的存储
⑤、nvs_flash_erase
esp_err_t nvs_flash_erase(void);
//擦除默认的 NVS 分区。
//擦除默认 NVS 分区(标签为“nvs”的分区)的所有内容。
//如果分区已初始化,则此函数首先取消初始化它。之后,必须再次初始化分区才能使用。
//成功返回ESP_OK
⑥、nvs_flash_erase_partition
esp_err_t nvs_flash_erase_partition(const char *part_name);
//擦除指定的 NVS 分区。
//擦除指定 NVS 分区的所有内容
//如果分区已初始化,则此函数首先取消初始化它。之后,必须再次初始化分区才能使用。
//参数:part_name – [in] 应擦除的分区的名称(标签)
//成功返回ESP_OK
如下所述API所在的头文件路径:components/nvs_flash/include/nvs.h
①、nvs_open
esp_err_t nvs_open(const char *namespace_name, nvs_open_mode_t open_mode, nvs_handle_t *out_handle);
//使用默认NVS分区中的给定名称空间,打开非易失性存储。
//为了减少键名上可能发生的冲突,每个模块都可以使用自己的命名空间。默认的NVS分区是分区表中标记为“NVS”的分区。
//参数:
namespace_name : 命名空间名称。最大长度为(NVS_KEY_NAME_MAX_SIZE-1)个字符。不应该是空的。
open_mode :[in] NVS_READWRITE或NVS_READONLY。如果NVS_READONLY,将打开一个只读句柄。对于这个句柄,所有写请求都将被拒绝。
out_handle :[out]如果成功(返回代码为0),则在此参数中返回句柄。
//成功返回ESP_OK
②、nvs_commit
esp_err_t nvs_commit(nvs_handle_t handle);
//将设置的值,写入更新到非易失性存储器中。
//在设置任何值之后,必须调用nvs_commit()以确保将更改写入非易失性存储。
//参数: handle--通过nvs_open获取的存储句柄。以只读方式打开的句柄不能使用。
//成功返回ESP_OK
③、nvs_close
void nvs_close(nvs_handle_t handle);
//关闭存储句柄并释放所有已分配的资源。
//关闭句柄可能不会自动将更改写入非易失性存储。必须使用nvs_commit函数显式地完成。
④、nvs_erase_all
esp_err_t nvs_erase_all(nvs_handle_t handle);
//擦除命名空间中的所有键值对。
//注意,在调用nvs_commit函数之前,实际存储可能不会更新。
//参数:handle--通过nvs_open获取的存储句柄。以只读方式打开的句柄不能使用。
//成功返回ESP_OK
⑤、nvs_set_i8
esp_err_t nvs_set_i8(nvs_handle_t handle、const char *key、int8_t value);
//为给定键设置int8_t值
//根据其名称设置键的值。请注意,在调用nvs_commit函数之前,不会更新实际存储。
//参数:handle – [in] 从nvs_open函数获取的句柄。不能使用以只读方式打开的句柄。
// key – [in] 键名。最大长度为 (NVS_KEY_NAME_MAX_SIZE-1) 个字符。不应为空。
// value – [in] 要设置的值。
//成功返回ESP_OK
⑥、nvs_set_str
esp_err_t nvs_set_str(nvs_handle_t handle, const char *key, const char *value);
//为给定键设置字符串类型的值
//根据其名称设置键的值。请注意,在调用nvs_commit函数之前,不会更新实际存储。
//参数:handle – [in] 从nvs_open函数获取的句柄。不能使用以只读方式打开的句柄。
// key – [in] 键名。最大长度为 (NVS_KEY_NAME_MAX_SIZE-1) 个字符。不应为空。
// value – [in] 要设置的值。对于字符串最大的长度为4000字节(包括NULL)
//成功返回ESP_OK
⑦、除了写入的数据类型不同,下面列出的这些函数的使用与上面的 nvs_set_i8 函数使用方法基本相同
esp_err_t nvs_set_u8(nvs_handle_t handle、const char *key、uint8_t value);
esp_err_t nvs_set_i16(nvs_handle_t handle, const char *key, int16_t value);
esp_err_t nvs_set_u16(nvs_handle_t handle, const char *key, uint16_t value);
esp_err_t nvs_set_i32(nvs_handle_t handle, const char *key, int32_t value);
esp_err_t nvs_set_u32(nvs_handle_t handle, const char *key, uint32_t value);
esp_err_t nvs_set_i64(nvs_handle_t handle, const char *key, int64_t value);
esp_err_t nvs_set_u64(nvs_handle_t handle, const char *key, uint64_t value);
⑧、nvs_get_i8
esp_err_t nvs_get_i8(nvs_handle_t handle, const char *key, int8_t *out_value);
//获取给定键的int8_t值
//这些函数根据键的名称检索键的值。如果key不存在,或者请求的变量类型与设置值时使用的类型不匹配,则返回错误。如果出现任何错误,则不修改out_value。
//参数:
handle -从nvs_open函数中获取的句柄。
key - [in]密钥名称。最大长度为(NVS_KEY_NAME_MAX_SIZE-1)个字符。不应该是空的。
out_value -输出值指针。对于nvs_get_str和nvs_get_blob可能为NULL,在这种情况下,所需的长度将在长度参数中返回。
//成功返回ESP_OK
⑨、nvs_get_str
esp_err_t nvs_get_str(nvs_handle_t handle, const char *key, char *out_value, size_t *length);
//获取给定键的字符串值
//这些函数检索给定键的条目的数据。如果key不存在,或者请求的变量类型与设置值时使用的类型不匹配,则返回错误。
//参数:
handle -从nvs_open函数中获取的句柄。
key - [in]密钥名称。最大长度为(NVS_KEY_NAME_MAX_SIZE-1)个字符。不应该是空的。
out_value - [out]输出值指针。对于nvs_get_str和nvs_get_blob可能为NULL,在这种情况下,所需的长度将在长度参数中返回。
length - [inout]指向保存out_value长度的变量的非零指针。如果out_value为零,则设置为保存该值所需的长度。如果out_value不为零,将被设置为写入值的实际长度。对于nvs_get_str,这包括零终止符。
//成功返回ESP_OK
⑩、除了获取的数据类型不同,下面列出的这些函数的使用与上面的 nvs_get_i8 函数使用方法基本相同
esp_err_t nvs_get_u8(nvs_handle_t handle, const char *key, uint8_t *out_value);
esp_err_t nvs_get_i16(nvs_handle_t handle, const char *key, int16_t *out_value);
esp_err_t nvs_get_u16(nvs_handle_t handle, const char *key, uint16_t *out_value);
esp_err_t nvs_get_i32(nvs_handle_t handle, const char *key, int32_t *out_value);
esp_err_t nvs_get_u32(nvs_handle_t handle, const char *key, uint32_t *out_value);
esp_err_t nvs_get_i64(nvs_handle_t handle, const char *key, int64_t *out_value);
esp_err_t nvs_get_u64(nvs_handle_t handle, const char *key, uint64_t *out_value);
3、NVS存储参考程序
#include <stdio.h>
#include <inttypes.h>
#include "sdkconfig.h"
#include "esp_flash.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "spi_flash_mmap.h"
/**
* @brief 设置设备初始化状态标志位
* @param void
* @retval 成功返回0,失败返回-1
*/
int set_device_init_flag(void)
{
esp_err_t ret;
nvs_handle_t nvs_handle;
uint8_t flag = 0xac; //设备初始化标志
//1、初始化NVS
ret = nvs_flash_init();
if(ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND)
{
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
//2、打开NVS
ret = nvs_open("list", NVS_READWRITE, &nvs_handle);
if(ret != ESP_OK)
{
ESP_LOGI("nvs", "nvs open failed");
nvs_close(nvs_handle);
return -1;
}
//3、写入数据
ret = nvs_set_u8(nvs_handle, "FLAG", flag); //写一个uint8_t类型的数据
if(ret != ESP_OK)
{
ESP_LOGI("nvs", "nvs write init flag failed");
nvs_close(nvs_handle);
return -1;
}
//4、写完提交数据
ret = nvs_commit(nvs_handle);
if(ret != ESP_OK)
{
ESP_LOGI("nvs", "nvs commit failed");
nvs_close(nvs_handle);
return -1;
}
//4、关闭NVS
nvs_close(nvs_handle);
return 0;
}
/**
* @brief 获取设备初始化状态标志位
* @param void
* @retval 成功返回:flag,失败返回:1
*/
uint8_t get_device_init_flag(void)
{
esp_err_t ret;
nvs_handle_t nvs_handle;
uint8_t flag = 0;
//1、初始化NVS
ret = nvs_flash_init();
if(ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND)
{
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
//2、打开NVS
ret = nvs_open("list", NVS_READONLY, &nvs_handle);
if(ret != ESP_OK)
{
printf("ret = %d\n", ret);
ESP_LOGI("nvs", "nvs open failed");
nvs_close(nvs_handle);
return 1;
}
//3、读取数据
ret = nvs_get_u8(nvs_handle, "FLAG", &flag);
if(ret != ESP_OK)
{
ESP_LOGI("nvs", "nvs read init flag failed");
nvs_close(nvs_handle);
return 1;
}
//ESP_LOGI("nvs", "FLAG = %#X", flag);
printf("FLAG = %#X\n", flag);
//4、关闭NVS
nvs_close(nvs_handle);
return flag;
}
int app_main(void)
{
uint8_t flag = 0x00;
int8_t ret = 0;
ret = set_device_init_flag();
if(ret == -1)
{
printf("set device init flag failed\n");
return -1;
}
printf("set flag success!\n");
flag = get_device_init_flag();
if(flag == 1)
{
printf("get init flag failed!");
return -1;
}
printf("get init flag success\nFlag = %0X\n", flag);
while (1)
{
vTaskDelay(1000);
}
return 0;
}
ESP32 NVS读写数据程序运行效果如下图所示: