ESP32开发——基于idf框架使用NVS操作存储设备读写
文章目录
- 一、非易失性存储库简介
- 1.1 底层存储
- 1.2 键值对
- 1.3 命名空间
- 二、常用API函数
- 三、代码示例
- 3.1 nvs操作流程
- 3.2 使用nvs读写value操作
- 3.2 读写多个键值对
- 3.3 使用nvs读写 BLOB 二进制大对象数据
- 3.4 擦除数据
- 四、自定义nvs存储分区
- 4.1 自定义partions分区信息
- 4.2 程序调用
一、非易失性存储库简介
非易失性存储 (NVS) 库主要用于在 flash 中存储键值格式的数据。
1.1 底层存储
NVS 库通过调用 esp_partition API 使用主 flash 的部分空间,即类型为 data
且子类型为 nvs
的所有分区。应用程序可调用 nvs_open()
API 选择使用带有 nvs
标签的分区,也可以通过调用 nvs_open_from_partition()
API 选择使用指定名称的任意分区。
如果 NVS 分区被截断(例如,更改分区表布局时),则应擦除分区内容。可以使用 ESP-IDF 构建系统中的 idf.py erase_flash
命令擦除 flash 上的所有内容。
NVS 最适合存储一些较小的数据,而非字符串或二进制大对象 (BLOB) 等较大的数据。如需存储较大的 BLOB
或者字符串
,请考虑使用基于磨损均衡库的 FAT 文件系统。
1.2 键值对
NVS 的操作对象为键值对,其中键是 ASCII
字符串,当前支持的最大键长为 15 个字符。值可以为以下几种类型:
- 整数型:uint8_t、int8_t、uint16_t、int16_t、uint32_t、int32_t、uint64_t 和 int64_t;
- 以 0 结尾的字符串;
- 可变长度的二进制数据 (BLOB)
键必须唯一。为现有的键写入新的值可能产生如下结果:
- 如果新旧值数据类型相同,则更新值;
- 如果新旧值数据类型不同,则返回错误。
读取值时也会执行数据类型检查。如果读取操作的数据类型与该值的数据类型不匹配,则返回错误。
1.3 命名空间
为了减少不同组件之间键名的潜在冲突,NVS 将每个键值对分配给一个命名空间。命名空间的命名规则遵循键名的命名规则,例如,最多可占 15 个字符。命名空间的名称在调用 nvs_open()
或 nvs_open_from_partition
中指定,调用后将返回一个不透明句柄,用于后续调用 nvs_get_*
、nvs_set_*
和 nvs_commit
函数。这样,一个句柄关联一个命名空间,键名便不会与其他命名空间中相同键名冲突。
请注意,不同 NVS 分区中具有相同名称的命名空间将被视为不同的命名空间。
二、常用API函数
参考文件: esp-idf\components\nvs_flash\include\nvs_flash.h
- 初始化默认NVS分区
esp_err_t nvs_flash_init(void);
- 初始化自定义NVS分区
esp_err_t nvs_flash_init_partition(const char *partition_label);
- 从默认 NVS 分区打开指定命名空间的非易失性存储
esp_err_t nvs_open(const char* name, nvs_open_mode_t open_mode, nvs_handle_t *out_handle);
- 从指定分区而非默认分区打开 NVS 的命名空间
esp_err_t nvs_open_from_partition(const char *part_name, const char *name, nvs_open_mode_topen_mode, nvs_handle_t *out_handle);
- 擦除默认NVS分区
esp_err_t nvs_flash_erase(void);
- 擦除自定义NVS分区
esp_err_t nvs_flash_erase_partition(const char *part_name);
- 设置 int 32 类型值到键值内
esp_err_t nvs_set_i32(nvs_handle_thandle, const char *key, int32_t value);
- 设置 uint32_t 数据类型的值到键值内
esp_err_t nvs_set_u32(nvs_handle_thandle, const char *key, uint32_t value);
- 设置字符串值到键值内
esp_err_t nvs_set_str(nvs_handle_thandle, const char *key, const char *value);
- 从键值内读取 int_32 类型值
esp_err_t nvs_get_i32(nvs_handle_thandle, const char *key, int32_t *out_value);
- 从键值内读取 uint_32 类型值
esp_err_t nvs_get_u32(nvs_handle_thandle, const char *key, uint32_t *out_value);
- 从键值内读取字符串值
esp_err_t nvs_get_str(nvs_handle_thandle, const char *key, char *out_value, size_t *length);
- 从键值内读取二进制大对象的值
esp_err_t nvs_get_blob(nvs_handle_thandle, const char *key, void *out_value, size_t *length);
- 将要修改的数据写入到nvs内
esp_err_t nvs_commit(nvs_handle_t handle);
- 擦除特定键名的键值
esp_err_t nvs_erase_key(nvs_handle_t handle, const char* key);
- 擦除命名空间内所有的键对值
esp_err_t nvs_erase_all(nvs_handle_thandle);
- 关闭nvs存储句柄并释放所有分配的资源
void nvs_close(nvs_handle_thandle);
三、代码示例
3.1 nvs操作流程
- 初始化nvs flash
- 打开一个命名空间
- 读取或者写入数据
- 使用nvs_commit确保修改完成
- 关闭nvs flash
拷贝示例模板
book@100ask:~/esp$ cp esp-idf/examples/storage/nvs_rw_value/ -rfd .
3.2 使用nvs读写value操作
写入 nvs 命名空间为 store 的 int32 类型键对值 键名为val 键值为0 每次重启加一。
编辑nvs_value_example_main.c:
book@100ask:~/esp/nvs_rw_value$ gedit main/nvs_value_example_main.c#include <stdio.h>
#include <string.h>
#include "esp_log.h"
#include "nvs.h"
#include "nvs_flash.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"#define TAG "NVS"void app_main()
{// 延时1秒,确保系统稳定启动vTaskDelay(1000 / portTICK_PERIOD_MS);// 初始化NVS闪存分区(默认分区名为"nvs")// 若分区已满或版本不兼容,需先擦除再初始化ESP_ERROR_CHECK(nvs_flash_init());// 打开名为"store"的命名空间,使用读写模式// 若命名空间不存在,会自动创建(读写模式下)nvs_handle handle;ESP_ERROR_CHECK(nvs_open("store", NVS_READWRITE, &handle));// 读取键"val"对应的int32_t值int32_t val = 0;esp_err_t result = nvs_get_i32(handle, "val", &val);// 处理读取结果switch (result){// 键不存在的情况(两种错误码处理相同逻辑)case ESP_ERR_NVS_NOT_FOUND:case ESP_ERR_NOT_FOUND:ESP_LOGE(TAG, "Value not set yet");break;// 读取成功的情况case ESP_OK:ESP_LOGI(TAG, "Value is %d", val);break;// 其他错误(如句柄无效、内存不足等)default:ESP_LOGE(TAG, "Error (%s) opening NVS handle!\n", esp_err_to_name(result));break;}// 将值递增1(无论之前是否存在,val已有初始值0)val++;// 将递增后的值写入NVSESP_ERROR_CHECK(nvs_set_i32(handle, "val", val));// 提交更改到闪存(确保数据持久化)ESP_ERROR_CHECK(nvs_commit(handle));// 关闭NVS句柄,释放资源nvs_close(handle);
}
设置环境变量:
get_idf
设置芯片为 esp32c3:
idf.py set-target esp32c3
编译命令:
idf.py build
烧写:
idf.py -p /dev/ttyUSB0 flash
监视运行:
idf.py -p /dev/ttyUSB0 monitor
键值从0开始累加,每次重新启动监视器或者对板子进行复位都会累加 val。
3.2 读写多个键值对
读写几种数据类型的键值对:
#include <stdio.h>
#include <string.h>
#include "esp_log.h"
#include "nvs.h"
#include "nvs_flash.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "esp_wifi.h"#define TAG "NVS"void nvs_key_write(void)
{nvs_handle handle;ESP_ERROR_CHECK(nvs_open("store", NVS_READWRITE, &handle));// 定义键名static const char *DATA = "key value1"; // 用于存储int32_tstatic const char *DATA1 = "key value2"; // 用于存储字符串static const char *DATA2 = "key value3"; // 用于存储结构体(Blob)// WiFi配置结构体初始化wifi_config_t wifi_config_sta = {.sta = {.ssid = "LjunG",.password = "666888",},};// 写入整数类型int32_t val = 123;ESP_ERROR_CHECK(nvs_set_i32(handle, DATA, val));// 写入字符串类型ESP_ERROR_CHECK(nvs_set_str(handle, DATA1, "i am LjunG"));// 写入二进制大对象(Blob) - WiFi配置结构体ESP_ERROR_CHECK(nvs_set_blob(handle, DATA2, &wifi_config_sta, sizeof(wifi_config_sta)));// 提交更改到闪存ESP_ERROR_CHECK(nvs_commit(handle));// 关闭句柄nvs_close(handle);
}void nvs_key_read(void)
{nvs_handle handle;ESP_ERROR_CHECK(nvs_open("store", NVS_READWRITE, &handle));// 定义键名(需与写入时一致)static const char *DATA = "key value1";static const char *DATA1 = "key value2";static const char *DATA2 = "key value3";// 读取整数类型int32_t val = 0;ESP_ERROR_CHECK(nvs_get_i32(handle, DATA, &val));// 读取字符串类型// 注意: str_len既是输入参数(缓冲区大小),也是输出参数(实际字符串长度)uint32_t str_len = 32; char str_data[32] = {0};ESP_ERROR_CHECK(nvs_get_str(handle, DATA1, str_data, &str_len));// 读取二进制大对象(Blob)wifi_config_t wifi_config_sta;memset(&wifi_config_sta, 0x0, sizeof(wifi_config_sta));uint32_t wifi_len = sizeof(wifi_config_sta);ESP_ERROR_CHECK(nvs_get_blob(handle, DATA2, &wifi_config_sta, &wifi_len));// 输出读取结果printf("DATA value is: %d \r\n", val);printf("DATA1 string is: %s len%u \r\n", str_data, str_len);printf("DATA2 wifi is:%s passwd:%s\r\n", wifi_config_sta.sta.ssid, wifi_config_sta.sta.password);// 关闭句柄nvs_close(handle);
}void app_main()
{// 延时1秒,确保系统稳定启动vTaskDelay(1000 / portTICK_PERIOD_MS);// 初始化NVS闪存ESP_ERROR_CHECK(nvs_flash_init());// 执行写入和读取操作nvs_key_write();nvs_key_read();
}
设置环境变量:
get_idf
设置芯片为 esp32c3:
idf.py set-target esp32c3
编译命令:
idf.py build
烧写:
idf.py -p /dev/ttyUSB0 flash
监视运行:
idf.py -p /dev/ttyUSB0 monitor
3.3 使用nvs读写 BLOB 二进制大对象数据
#include <stdio.h>
#include <string.h>
#include "esp_log.h"
#include "nvs.h"
#include "nvs_flash.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"#define TAG "NVS_BLOB_EXAMPLE"
#define STORAGE_NAMESPACE "blob_storage" // 命名空间
#define BLOB_KEY "device_config" // Blob数据的键名// 定义一个示例结构体(作为Blob存储的数据)
typedef struct {uint8_t device_id; // 设备IDchar device_name[32]; // 设备名称float temperature; // 温度数据bool is_active; // 设备激活状态uint16_t version; // 配置版本号
} DeviceConfig;/*** 向NVS写入Blob类型数据(结构体)*/
void nvs_blob_write() {nvs_handle_t nvs_handle;esp_err_t err;// 1. 打开命名空间(读写模式)err = nvs_open(STORAGE_NAMESPACE, NVS_READWRITE, &nvs_handle);if (err != ESP_OK) {ESP_LOGE(TAG, "打开命名空间失败: %s", esp_err_to_name(err));return;}// 2. 准备要存储的结构体数据DeviceConfig config = {.device_id = 0x01,.temperature = 25.6f,.is_active = true,.version = 0x0102,};strncpy(config.device_name, "SensorNode-001", sizeof(config.device_name)-1);// 3. 写入Blob数据err = nvs_set_blob(nvs_handle, BLOB_KEY, &config, sizeof(config));if (err != ESP_OK) {ESP_LOGE(TAG, "写入Blob失败: %s", esp_err_to_name(err));nvs_close(nvs_handle);return;}// 4. 提交更改(确保数据写入闪存)err = nvs_commit(nvs_handle);if (err != ESP_OK) {ESP_LOGE(TAG, "提交Blob失败: %s", esp_err_to_name(err));nvs_close(nvs_handle);return;}ESP_LOGI(TAG, "Blob数据写入成功,大小: %d 字节", sizeof(config));nvs_close(nvs_handle); // 关闭句柄
}/*** 从NVS读取Blob类型数据(结构体)*/
void nvs_blob_read() {nvs_handle_t nvs_handle;esp_err_t err;// 1. 打开命名空间(只读模式即可,这里用读写模式仅作示例)err = nvs_open(STORAGE_NAMESPACE, NVS_READWRITE, &nvs_handle);if (err != ESP_OK) {ESP_LOGE(TAG, "打开命名空间失败: %s", esp_err_to_name(err));return;}// 2. 先获取Blob数据的大小(输入NULL作为缓冲区)size_t blob_size = 0;err = nvs_get_blob(nvs_handle, BLOB_KEY, NULL, &blob_size);if (err != ESP_OK) {ESP_LOGE(TAG, "获取Blob大小失败: %s", esp_err_to_name(err));nvs_close(nvs_handle);return;}ESP_LOGI(TAG, "读取到的Blob大小: %d 字节", blob_size);// 3. 分配缓冲区并读取Blob数据DeviceConfig read_config;// 检查数据大小是否匹配(防止数据损坏或版本不兼容)if (blob_size != sizeof(read_config)) {ESP_LOGE(TAG, "Blob大小不匹配!预期: %d, 实际: %d", sizeof(read_config), blob_size);nvs_close(nvs_handle);return;}err = nvs_get_blob(nvs_handle, BLOB_KEY, &read_config, &blob_size);if (err != ESP_OK) {ESP_LOGE(TAG, "读取Blob失败: %s", esp_err_to_name(err));nvs_close(nvs_handle);return;}// 4. 打印读取到的结构体数据ESP_LOGI(TAG, "Blob数据读取成功:");printf(" 设备ID: 0x%02X\n", read_config.device_id);printf(" 设备名称: %s\n", read_config.device_name);printf(" 温度: %.1f°C\n", read_config.temperature);printf(" 激活状态: %s\n", read_config.is_active ? "已激活" : "未激活");printf(" 版本号: 0x%04X\n", read_config.version);nvs_close(nvs_handle); // 关闭句柄
}void app_main() {// 初始化NVS(必须在所有NVS操作前执行)esp_err_t err = nvs_flash_init();if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {// 若分区满或版本不兼容,擦除后重新初始化ESP_ERROR_CHECK(nvs_flash_erase());err = nvs_flash_init();}ESP_ERROR_CHECK(err);// 执行Blob写入和读取操作nvs_blob_write();nvs_blob_read();
}
设置环境变量:
get_idf
设置芯片为 esp32c3:
idf.py set-target esp32c3
编译命令:
idf.py build
烧写:
idf.py -p /dev/ttyUSB0 flash
监视运行:
idf.py -p /dev/ttyUSB0 monitor
3.4 擦除数据
写入数据值之后进行擦除操作:
#include <stdio.h>
#include <string.h>
#include "esp_log.h"
#include "nvs.h"
#include "nvs_flash.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"#define TAG "NVS"
// 存储在NVS中的数据键名(用于标识要操作的数据)
static const char *DATA = "val";void erase(void)
{nvs_handle handle; // NVS操作句柄,用于后续NVS操作// 延迟1秒(等待前序操作完成,单位为系统时钟周期,portTICK_PERIOD_MS为1ms)vTaskDelay(1000 / portTICK_PERIOD_MS); // 打开NVS命名空间"store",以读写模式(NVS_READWRITE)操作// 参数:命名空间名称、操作模式、输出句柄指针ESP_ERROR_CHECK(nvs_open("store", NVS_READWRITE, &handle));// 擦除指定键(DATA即"val")的数据ESP_ERROR_CHECK(nvs_erase_key(handle, DATA));// 注释:如需擦除命名空间中所有数据,可使用以下函数// ESP_ERROR_CHECK(nvs_erase_all(handle));// 提交更改(将内存中的操作同步到闪存,NVS操作需显式提交才会生效)ESP_ERROR_CHECK(nvs_commit(handle));// 关闭NVS句柄(释放资源,避免内存泄漏)nvs_close(handle);// 输出日志提示,即将重启ESP_LOGI(TAG, "Set Restart now.\n");// 重启ESP32系统(测试数据持久性的常用方式)esp_restart();
}void write_data(void)
{nvs_handle handle; // NVS操作句柄int32_t out_val = 0; // 存储读取到的整数数据(32位,适配nvs_get_i32)// 打开命名空间"store",读写模式ESP_ERROR_CHECK(nvs_open("store", NVS_READWRITE, &handle));// 读取"val"键对应的整数数据到out_valesp_err_t result = nvs_get_i32(handle, DATA, &out_val);// 根据读取结果进行日志输出switch (result){case ESP_ERR_NVS_NOT_FOUND: // 键不存在(首次运行时触发)case ESP_ERR_NOT_FOUND: // 未找到数据(与上一条等价,兼容不同版本)ESP_LOGE(TAG, "Value not set yet"); // 提示值尚未设置break;case ESP_OK: // 读取成功ESP_LOGI(TAG, "Value is %d", out_val); // 输出当前值break;default: // 其他错误ESP_LOGE(TAG, "Error (%s) opening NVS handle!\n", esp_err_to_name(result));break;}// 将读取到的值自增1(首次运行时从0变为1)out_val++;// 将自增后的值写入"val"键ESP_ERROR_CHECK(nvs_set_i32(handle, DATA, out_val));// 提交更改(同步到闪存)ESP_ERROR_CHECK(nvs_commit(handle));// 关闭句柄nvs_close(handle);
}void app_main()
{// 延迟1秒(等待系统初始化完成,避免启动阶段资源冲突)vTaskDelay(1000 / portTICK_PERIOD_MS);// 初始化NVS闪存(必须在所有NVS操作前执行,否则会报错)ESP_ERROR_CHECK(nvs_flash_init());// 执行数据写入(自增)操作write_data();// 执行数据擦除并重启erase();
}
设置环境变量:
get_idf
设置芯片为 esp32c3:
idf.py set-target esp32c3
编译命令:
idf.py build
烧写:
idf.py -p /dev/ttyUSB0 flash
监视运行:
idf.py -p /dev/ttyUSB0 monitor
板子复位后在val中写入1,自动重启后将其擦除掉。
四、自定义nvs存储分区
NVS 分区生成程序根据 CSV 文件中的键值对生成二进制文件。该二进制文件与 非易失性存储器 (NVS) 中定义的 NVS 结构兼容。NVS 分区生成程序适合用于生成二进制数据(Blob),其中包括设备生产时可从外部烧录的ODM/OEM 数据。这也使得生产制造商在使用同一个固件的基础上,通过自定义参数,如序列号等,为每个设备生成不同配置。
4.1 自定义partions分区信息
工程目录下增加 partitions.csv
文件加入自定义分区信息:
# Name, Type, SubType, Offset, Size, Flags
# Note: if you change the phy_init or app partition offset, make sure to change the offset in Kconfig.projbuild
nvs, data, nvs, , 0x6000,
phy_init, data, phy, , 0x1000,
factory, app, factory, , 1M,
MyNvs, data, nvs, , 1M,
使用 idf.py menuconfig
来配置分区表:
之后我们设置一下板载SPI FLASH的大小:
4.2 程序调用
#include <stdio.h>
#include <string.h>
#include "esp_log.h"
#include "nvs.h"
#include "nvs_flash.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"// 日志标签,用于标识当前模块的日志输出
#define TAG "NVS"void app_main()
{// 延迟1秒(等待系统初始化完成,避免启动阶段资源冲突)vTaskDelay(1000 / portTICK_PERIOD_MS);// 初始化指定名称的NVS分区("MyNvs")// 注:需在partition.csv中预先定义该分区,否则会初始化失败ESP_ERROR_CHECK(nvs_flash_init_partition("MyNvs"));nvs_handle handle; // NVS操作句柄// 从指定NVS分区("MyNvs")打开命名空间("store"),读写模式// 参数:分区名、命名空间、操作模式、句柄指针ESP_ERROR_CHECK(nvs_open_from_partition("MyNvs", // 自定义NVS分区名称"store", // 命名空间NVS_READWRITE, // 读写模式&handle // 输出句柄));nvs_stats_t nvsStats; // 用于存储NVS分区状态信息的结构体// 获取指定NVS分区("MyNvs")的状态信息(已用/空闲条目数等)nvs_get_stats("MyNvs", &nvsStats);// 打印NVS分区状态信息ESP_LOGI(TAG, "used: %d, free: %d, total: %d, namespace count: %d", nvsStats.used_entries, // 已使用的条目数nvsStats.free_entries, // 空闲的条目数nvsStats.total_entries, // 总条目数(已用+空闲)nvsStats.namespace_count // 已创建的命名空间数量);int32_t val = 0; // 用于存储读取到的整数数据// 读取命名空间中"val"键对应的32位整数esp_err_t result = nvs_get_i32(handle, "val", &val);// 根据读取结果进行日志输出switch (result){case ESP_ERR_NVS_NOT_FOUND: // 键不存在(首次运行时触发)case ESP_ERR_NOT_FOUND: // 未找到数据(兼容不同版本的错误码)ESP_LOGE(TAG, "Value not set yet"); // 提示值尚未设置break;case ESP_OK: // 读取成功ESP_LOGI(TAG, "Value is %d", val); // 输出当前值break;default: // 其他错误ESP_LOGE(TAG, "Error (%s) reading value!\n", esp_err_to_name(result));break;}val++; // 将值自增1(首次运行时从0→1)// 将自增后的值写入"val"键ESP_ERROR_CHECK(nvs_set_i32(handle, "val", val));// 提交更改(将操作同步到闪存,必须调用才会生效)ESP_ERROR_CHECK(nvs_commit(handle));// 关闭NVS句柄(释放资源)nvs_close(handle);
}
设置环境变量:
get_idf
设置芯片为 esp32c3:
idf.py set-target esp32c3
编译命令:
idf.py build
烧写:
idf.py -p /dev/ttyUSB0 flash
监视运行:
idf.py -p /dev/ttyUSB0 monitor
每次重启板子或者重新启动监视器val的值会累加。