在 ESP32 中实现掉电不丢失的数据存储,最常用的方案是 NVS(Non-Volatile Storage,非易失性存储)。NVS 基于键值对(Key-Value)模型,底层使用闪存(Flash)存储,支持字符串、整数、浮点数等常见数据类型,适合存储配置信息、用户参数等小数据量场景。
一、步骤说明
-
步骤 1:配置 NVS 分区
ESP32 的闪存需要预先划分 NVS 专用分区。默认情况下,ESP-IDF 会自动分配一个名为nvs
的分区(大小约 0x6000,即 24KB
-
初始化 NVS 分区
在app_main
中初始化 NVS 分区,为存储做准备。 -
写入数据到 NVS
使用nvs_set_xxx
系列函数写入键值对数据。 -
提交更改
调用nvs_commit
确保数据写入 Flash。 -
读取数据
使用nvs_get_xxx
系列函数读取已存储的数据。 -
错误处理
检查每个操作的返回值,处理可能的错误。
步骤 1:配置 NVS 分区
ESP32 的闪存需要预先划分 NVS 专用分区。默认情况下,ESP-IDF 会自动分配一个名为 nvs
的分区(大小约 0x6000,即 24KB)。如果需要更大空间或自定义分区,需手动配置分区表:
-
在项目根目录创建
partitions.csv
文件,内容示例:# Name Type SubType Offset Size Flags nvs data nvs 0x9000 0x6000 # 默认 NVS 分区(24KB) # 如果需要更大空间,可调整 Size(如 0x10000 表示 64KB)
-
在
sdkconfig
中指定分区表路径(通过idf.py menuconfig
进入配置):- 搜索
Partition Table
→Custom partition table CSV
→ 选择partitions.csv
。
- 搜索
步骤 2:初始化 NVS
在使用 NVS 前,需要初始化闪存驱动并挂载 NVS 分区:
#include "nvs.h"
#include "nvs_flash.h"
#include "esp_log.h"
static const char* TAG = "NVS_Demo";
// 初始化 NVS
esp_err_t nvs_init()
{
// 初始化 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();
}
return err;
}
步骤 3、4:写入数据到 NVS
使用 nvs_set_*
系列函数写入数据(支持 int8_t
/int32_t
/uint32_t
/float
/string
等类型),完成后需调用 nvs_commit()
提交更改。
步骤 5:从 NVS 读取数据
使用 nvs_get_*
系列函数读取数据,需提前定义变量存储结果,并检查返回的错误码。
二、实例代码:NVS 数据读写
代码1:
示例代码
#include <stdio.h>
#include "nvs_flash.h"
#include "nvs.h"
void app_main(void)
{
// 初始化 NVS
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
// 如果 NVS 分区被截断或版本不匹配,擦除并重新初始化
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
// 打开命名空间(类似"文件夹")
nvs_handle_t nvs_handle;
ret = nvs_open("storage", NVS_READWRITE, &nvs_handle);
if (ret != ESP_OK) {
printf("Error opening NVS: %s\n", esp_err_to_name(ret));
return;
}
// 写入数据
int32_t value = 42;
ret = nvs_set_i32(nvs_handle, "my_value", value);
if (ret != ESP_OK) {
printf("Failed to write value: %s\n", esp_err_to_name(ret));
}
// 写入字符串
const char* str_data = "Hello, NVS!";
ret = nvs_set_str(nvs_handle, "my_str", str_data);
if (ret != ESP_OK) {
printf("Failed to write string: %s\n", esp_err_to_name(ret));
}
// 提交更改到 Flash
ret = nvs_commit(nvs_handle);
if (ret != ESP_OK) {
printf("Commit failed: %s\n", esp_err_to_name(ret));
}
// 读取整数
int32_t read_value = 0;
ret = nvs_get_i32(nvs_handle, "my_value", &read_value);
switch (ret) {
case ESP_OK:
printf("Read value: %d\n", read_value);
break;
case ESP_ERR_NVS_NOT_FOUND:
printf("Value not set yet\n");
break;
default:
printf("Error reading value: %s\n", esp_err_to_name(ret));
}
// 读取字符串
size_t required_size;
ret = nvs_get_str(nvs_handle, "my_str", NULL, &required_size);
if (ret == ESP_OK) {
char* read_str = malloc(required_size);
ret = nvs_get_str(nvs_handle, "my_str", read_str, &required_size);
if (ret == ESP_OK) {
printf("Read string: %s\n", read_str);
} else {
printf("Error reading string: %s\n", esp_err_to_name(ret));
}
free(read_str);
} else {
printf("String not found: %s\n", esp_err_to_name(ret));
}
// 关闭 NVS 句柄
nvs_close(nvs_handle);
}
关键点解释
-
NVS 初始化
nvs_flash_init()
初始化 NVS 分区。- 如果检测到分区损坏或版本问题,需先擦除 (
nvs_flash_erase()
)。
-
命名空间
- 使用
nvs_open
打开一个命名空间(类似文件夹),所有操作在此空间内进行。
- 使用
-
数据类型
- 支持整数 (
i8/u8/i16/u16/i32/u32/i64/u64
)、字符串、二进制 Blob。 - 示例中演示了
i32
和str
的读写。
- 支持整数 (
-
字符串读取
- 需要两次调用
nvs_get_str
:第一次获取长度,第二次读取实际数据。
- 需要两次调用
-
错误处理
- 检查每个函数的返回值 (
ESP_OK
表示成功)。 - 使用
esp_err_to_name()
将错误码转换为可读信息。
- 检查每个函数的返回值 (
代码2:
演示了如何初始化 NVS、写入/读取整数、字符串和浮点数,并在 ESP32 重启后验证数据持久性。
#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 "esp_log.h"
#include "string.h"
static const char* TAG = "NVS_Demo";
static const char* NVS_NAMESPACE = "my_storage"; // NVS 命名空间(可自定义)
// 初始化 NVS(包含错误处理)
esp_err_t nvs_init()
{
esp_err_t err = nvs_flash_init();
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND)
{
ESP_LOGW(TAG, "NVS 分区需要擦除,重新初始化...");
ESP_ERROR_CHECK(nvs_flash_erase());
err = nvs_flash_init();
}
return err;
}
// 写入数据到 NVS
void nvs_write_demo()
{
nvs_handle_t my_handle;
esp_err_t err;
// 打开命名空间(读/写模式)
err = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &my_handle);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "打开命名空间失败: %s", esp_err_to_name(err));
return;
}
// 写入整数
err = nvs_set_i32(my_handle, "int_key", 132);
if (err != ESP_OK) ESP_LOGE(TAG, "写入整数失败: %s", esp_err_to_name(err));
// 写入字符串
const char* str_value = "Hello, NVS.hi!";
err = nvs_set_str(my_handle, "str_key", str_value);
if (err != ESP_OK) ESP_LOGE(TAG, "写入字符串失败: %s", esp_err_to_name(err));
// 写入浮点数(NVS 不直接支持 float,需转换为 uint32_t 存储)
float float_value = 1.23456f;
uint32_t float_as_uint = *(uint32_t*)&float_value; // 内存级转换
err = nvs_set_u32(my_handle, "float_key", float_as_uint);
if (err != ESP_OK) ESP_LOGE(TAG, "写入浮点数失败: %s", esp_err_to_name(err));
// 提交更改(必须调用,否则数据不会保存到闪存)
err = nvs_commit(my_handle);
if (err != ESP_OK) ESP_LOGE(TAG, "提交失败: %s", esp_err_to_name(err));
// 关闭命名空间
nvs_close(my_handle);
ESP_LOGI(TAG, "数据写入完成");
}
// 从 NVS 读取数据
int nvs_read_demo()
{
nvs_handle_t my_handle;
esp_err_t err;
// 打开命名空间(只读模式)
err = nvs_open(NVS_NAMESPACE, NVS_READONLY, &my_handle);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "打开命名空间失败: %s", esp_err_to_name(err));
return err;
}
// 读取整数
int32_t int_value;
err = nvs_get_i32(my_handle, "int_key", &int_value);
if (err == ESP_OK)
{
ESP_LOGI(TAG, "读取整数: %ld", int_value);
}
else
{
ESP_LOGE(TAG, "读取整数失败: %s", esp_err_to_name(err));
return err;
}
// 读取字符串
char str_value[32]; // 预分配缓冲区
size_t str_len = sizeof(str_value);
err = nvs_get_str(my_handle, "str_key", str_value, &str_len);
if (err == ESP_OK)
{
ESP_LOGI(TAG, "读取字符串: %s", str_value);
}
else if (err == ESP_ERR_NVS_NOT_FOUND)
{
ESP_LOGW(TAG, "字符串键未找到");
return err;
}
else
{
ESP_LOGE(TAG, "读取字符串失败: %s", esp_err_to_name(err));
return err;
}
// 读取浮点数(从 uint32_t 转换回 float)
uint32_t float_as_uint;
err = nvs_get_u32(my_handle, "float_key", &float_as_uint);
if (err == ESP_OK)
{
float float_value = *(float*)&float_as_uint; // 内存级转换
ESP_LOGI(TAG, "读取浮点数: %.5f", float_value);
}
else
{
ESP_LOGE(TAG, "读取浮点数失败: %s", esp_err_to_name(err));
return err;
}
// 关闭命名空间
nvs_close(my_handle);
return err;
}
void app_main(void)
{
// 初始化 NVS
esp_err_t err = nvs_init();
if (err != ESP_OK)
{
ESP_LOGE(TAG, "NVS 初始化失败: %s", esp_err_to_name(err));
return;
}
// 尝试读取数据(如果是首次运行,数据不存在)
ESP_LOGI(TAG, "--- 首次读取 ---");
err=nvs_read_demo();
if(err != ESP_OK)
{
// 写入数据
ESP_LOGI(TAG, "\n--- 开始写入数据 ---");
nvs_write_demo();
// 重新读取验证
ESP_LOGI(TAG, "\n--- 写入后读取 ---");
nvs_read_demo();
}
// 提示用户重启开发板验证掉电存储
ESP_LOGI(TAG, "\n请重启开发板,观察数据是否保留");
}
重启之后
代码说明
- 命名空间(Namespace):NVS 通过命名空间隔离不同功能的数据(如
my_storage
),避免键冲突。 - 数据类型支持:
- 整数:直接使用
nvs_set_i32
/nvs_get_i32
。 - 字符串:使用
nvs_set_str
/nvs_get_str
,需预分配足够大的缓冲区。 - 浮点数:NVS 不直接支持,需通过内存转换为
uint32_t
存储(或自行实现字符串格式化)。
- 整数:直接使用
- 提交更改:
nvs_commit()
是关键,否则数据仅保存在内存中,未写入闪存。 - 错误处理:通过
esp_err_t
检查每一步操作,避免因闪存损坏或空间不足导致数据丢失。
验证掉电存储
- 编译并烧录代码到 ESP32,观察日志:
- 首次运行时,读取会提示“键未找到”。
- 写入后重新读取,会显示已写入的数据。
- 手动重启开发板(或断开电源后重新上电),再次运行代码,日志会显示读取到之前写入的数据,验证掉电不丢失。
三、注意事项
- 键名长度:键名最长为 15 个字符。
- 频繁写入:避免频繁写入以减少 Flash 磨损,必要时合并数据。
- 分区大小:默认 NVS 分区通常足够,如需调整需修改
partitions.csv
。 - 数据大小限制:单个键值对的字符串最大长度为
NVS_MAX_VALUE_LEN
(默认 496 字节),大文件需拆分或使用 SPIFFS(文件系统)。 - 性能:NVS 写入操作会消耗闪存擦写次数(约 10 万次),频繁写入需优化(如合并多次修改后再提交)。
- 分区管理:自定义分区表时,需确保分区大小合理,避免与其他功能(如 OTA 升级)冲突。
参考:
https://github.com/espressif/esp-idf/blob/v5.2.5/examples/storage/nvsgen/main/nvsgen_example_main.c
分区表:
https://docs.espressif.com/projects/esp-idf/zh_CN/v5.2.5/esp32/api-guides/partition-tables.html