ESP32 NVS存储优化实践:C/C++双接口代码详解及应用指南
摘要
本文介绍了如何在ESP32平台上使用ESP-IDF的非易失性存储(NVS)功能,通过C API和C++ API两种方式实现数据的持久化存储。文章穿插了关键代码片段,详细解析了分区遍历、命名空间管理、数据读写和内存管理等技术点,旨在为嵌入式开发者提供一份完整且实用的代码示例及最佳实践指南。
引言
在物联网和嵌入式系统开发中,数据的持久化存储至关重要。ESP32作为一款应用广泛的物联网芯片,内置的NVS模块能高效地保存应用配置和状态数据。ESP-IDF为NVS提供了两种API接口:传统的C API和面向对象的C++ API。本文将通过具体代码示例,讲解如何正确使用这两种接口,并解释代码中的优化技巧和注意事项。
分区信息与NVS命名空间
列出数据分区信息
在ESP32系统中,数据通常存储在特定的分区中。通过以下代码,我们可以遍历并打印所有数据分区的信息,便于开发者了解设备的存储布局:
// 列出所有数据分区信息
void list_partitions() {
// 查找所有数据分区(类型为数据,子类型任意)
esp_partition_iterator_t it = esp_partition_find(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_ANY, NULL);
if (it == NULL) {
ESP_LOGE(TAG, "未找到任何分区");
return;
}
ESP_LOGI(TAG, "列出所有数据分区:");
while (it != NULL) {
const esp_partition_t *part = esp_partition_get(it);
ESP_LOGI(TAG, "名称: %s, 偏移: 0x%06x, 大小: %d KB, 类型: %d, 子类型: %d",
part->label,
(unsigned int)part->address,
(unsigned int)(part->size / 1024),
(unsigned int)part->type,
(unsigned int)part->subtype);
it = esp_partition_next(it);
}
// 释放获取的迭代器资源
esp_partition_iterator_release(it_orig);
}
提示:注意,此处列出的数据分区信息与NVS存储的“命名空间”不同。NVS默认存储于名为 “nvs” 的分区中。
NVS C API 示例
打开命名空间并读写数据
NVS C API通过 nvs_open
打开一个命名空间,此处的命名空间名称(例如 “c”)并不是存储分区名,而是数据隔离的标识。当该命名空间不存在时,系统会自动创建。以下代码展示了如何写入和读取字符串数据:
// 使用 NVS C API 示例:
// 打开名称为 "c" 的命名空间,若不存在则自动创建
void nvs_c_api_example() {
nvs_handle_t handle;
ESP_ERROR_CHECK(nvs_open("c", NVS_READWRITE, &handle));
// 写入字符串数据到键 "server_name"
ESP_ERROR_CHECK(nvs_set_str(handle, "server_name", "cheungxiongwei.com"));
ESP_ERROR_CHECK(nvs_commit(handle));
// 读取字符串数据:先获取所需缓冲区大小
size_t required_size = 0;
ESP_ERROR_CHECK(nvs_get_str(handle, "server_name", NULL, &required_size));
char *server_name = (char *)malloc(required_size);
if (server_name == NULL) {
ESP_LOGE(TAG, "内存分配失败");
nvs_close(handle);
return;
}
ESP_ERROR_CHECK(nvs_get_str(handle, "server_name", server_name, &required_size));
printf("读取到的服务器名称:%s, 长度:%d\n", server_name, (int)required_size);
free(server_name);
nvs_close(handle);
}
说明:在读取数据之前先获取所需缓冲区大小,再进行动态内存分配,并对分配失败进行判断,确保程序的稳定性。
NVS C++ API 示例
面向对象的NVS操作
ESP-IDF同时提供了C++ API,方便开发者使用面向对象的方式进行NVS操作。下例通过 nvs::open_nvs_handle
打开命名空间 “cpp”,并对一个整数型数据进行读写操作:
// 使用 NVS C++ API 示例:
// 打开名称为 "cpp" 的命名空间,默认存储于 "nvs" 分区中
void nvs_cpp_api_example() {
printf("打开非易失性存储(NVS)句柄... ");
esp_err_t err = ESP_OK;
std::unique_ptr<nvs::NVSHandle> handle = nvs::open_nvs_handle("cpp", NVS_READWRITE, &err);
if (err != ESP_OK) {
printf("打开 NVS 句柄失败(%s)!\n", esp_err_to_name(err));
} else {
printf("完成\n");
printf("从 NVS 读取重启计数器... ");
int32_t restart_counter = 0; // 若未初始化则默认值为 0
err = handle->get_item("restart_counter", restart_counter);
switch (err) {
case ESP_OK:
printf("完成\n");
printf("重启计数器 = %" PRIu32 "\n", restart_counter);
break;
case ESP_ERR_NVS_NOT_FOUND:
printf("该值尚未初始化!\n");
break;
default:
printf("读取失败(%s)!\n", esp_err_to_name(err));
break;
}
printf("更新 NVS 中的重启计数器... ");
restart_counter++;
err = handle->set_item("restart_counter", restart_counter);
printf((err != ESP_OK) ? "失败!\n" : "完成\n");
printf("提交 NVS 更新... ");
err = handle->commit();
printf((err != ESP_OK) ? "失败!\n" : "完成\n");
}
printf("\n");
// 倒计时后重启设备
for (int i = 3; i >= 0; i--) {
printf("将在 %d 秒后重启...\n", i);
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
printf("现在重启。\n");
fflush(stdout);
esp_restart();
}
说明:C++ API使用智能指针管理资源,简化内存管理,并通过面向对象的方式实现数据操作,使代码逻辑更加清晰。
完整代码示例
下面贴出完整代码,整合了上述所有功能与优化:
#include <stdio.h>
#include <inttypes.h>
#include <stdlib.h> // 包含 malloc/free 函数
#include "nvs_flash.h"
#include "nvs.h"
#include "nvs_handle.hpp"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "esp_log.h"
// 日志标签
constexpr char * TAG = "nvs";
// 列出所有数据分区信息
void list_partitions() {
// 查找所有数据分区(类型为数据,子类型任意)
esp_partition_iterator_t it = esp_partition_find(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_ANY, NULL);
if (it == NULL) {
ESP_LOGE(TAG, "未找到任何分区");
return;
}
ESP_LOGI(TAG, "列出所有数据分区:");
while (it != NULL) {
const esp_partition_t *part = esp_partition_get(it);
ESP_LOGI(TAG, "名称: %s, 偏移: 0x%06x, 大小: %d KB, 类型: %d, 子类型: %d",
part->label,
(unsigned int)part->address,
(unsigned int)(part->size / 1024),
(unsigned int)part->type,
(unsigned int)part->subtype);
it = esp_partition_next(it);
}
// 释放获取的迭代器资源
esp_partition_iterator_release(it_orig);
}
// 使用 NVS C API 示例:
// 打开名称为 "c" 的命名空间(注意:此处的 "c" 为命名空间名称,不是分区名称,若不存在则自动创建,最大 254 个分区)
// nvs_open 默认打开 nvs 分区,如果你从其他分区(确保 partitions.csv 中存在)打开,请使用 nvs_open_from_partition 函数。
// 参考 https://docs.espressif.com/projects/esp-idf/zh_CN/latest/esp32/api-reference/storage/nvs_flash.html#id6
void nvs_c_api_example() {
nvs_handle_t handle;
ESP_ERROR_CHECK(nvs_open("c", NVS_READWRITE, &handle));
// 写入字符串数据到键 "server_name"
ESP_ERROR_CHECK(nvs_set_str(handle, "server_name", "cheungxiongwei.com"));
ESP_ERROR_CHECK(nvs_commit(handle));
// 读取字符串数据:先获取所需缓冲区大小
size_t required_size = 0;
ESP_ERROR_CHECK(nvs_get_str(handle, "server_name", NULL, &required_size));
char *server_name = (char *)malloc(required_size);
if (server_name == NULL) {
ESP_LOGE(TAG, "内存分配失败");
nvs_close(handle);
return;
}
ESP_ERROR_CHECK(nvs_get_str(handle, "server_name", server_name, &required_size));
printf("读取到的服务器名称:%s, 长度:%d\n", server_name, (int)required_size);
free(server_name);
nvs_close(handle);
}
// 使用 NVS C++ API 示例:
// 打开名称为 "cpp" 的命名空间(同样,"cpp" 为命名空间名称,不代表分区,默认存储于 "nvs" 分区中)
// 如果需要从其他分区打开,请使用 open_nvs_handle_from_partition 函数。
// 参考 https://docs.espressif.com/projects/esp-idf/zh_CN/latest/esp32/api-reference/storage/nvs_flash.html#_CPPv48nvs_openPKc15nvs_open_mode_tP12nvs_handle_t
// 参考 https://docs.espressif.com/projects/esp-idf/zh_CN/latest/esp32/api-reference/storage/nvs_flash.html#_CPPv423nvs_open_from_partitionPKcPKc15nvs_open_mode_tP12nvs_handle_t
void nvs_cpp_api_example() {
printf("打开非易失性存储(NVS)句柄... ");
esp_err_t err = ESP_OK;
std::unique_ptr<nvs::NVSHandle> handle = nvs::open_nvs_handle("cpp", NVS_READWRITE, &err);
if (err != ESP_OK) {
printf("打开 NVS 句柄失败(%s)!\n", esp_err_to_name(err));
} else {
printf("完成\n");
printf("从 NVS 读取重启计数器... ");
int32_t restart_counter = 0; // 若未初始化则默认值为 0
err = handle->get_item("restart_counter", restart_counter);
switch (err) {
case ESP_OK:
printf("完成\n");
printf("重启计数器 = %" PRIu32 "\n", restart_counter);
break;
case ESP_ERR_NVS_NOT_FOUND:
printf("该值尚未初始化!\n");
break;
default:
printf("读取失败(%s)!\n", esp_err_to_name(err));
break;
}
printf("更新 NVS 中的重启计数器... ");
restart_counter++;
err = handle->set_item("restart_counter", restart_counter);
printf((err != ESP_OK) ? "失败!\n" : "完成\n");
printf("提交 NVS 更新... ");
err = handle->commit();
printf((err != ESP_OK) ? "失败!\n" : "完成\n");
}
printf("\n");
// 倒计时后重启设备
for (int i = 3; i >= 0; i--) {
printf("将在 %d 秒后重启...\n", i);
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
printf("现在重启。\n");
fflush(stdout);
esp_restart();
}
extern "C" void app_main(void)
{
// 列出所有数据分区
list_partitions();
// 初始化 NVS
esp_err_t err = nvs_flash_init();
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
// 若 NVS 分区无可用页或版本不匹配,则需擦除 NVS 分区并重新初始化
ESP_ERROR_CHECK(nvs_flash_erase());
err = nvs_flash_init();
}
ESP_ERROR_CHECK(err);
// 调用 NVS C API 示例
nvs_c_api_example();
// 调用 NVS C++ API 示例
nvs_cpp_api_example();
}
总结
本文通过穿插关键代码片段,详细解析了如何在ESP32平台上使用ESP-IDF实现NVS数据存储。无论是基于C语言的传统API,还是采用面向对象的C++ API,都展示了如何正确管理命名空间、分区以及内存资源。掌握这些最佳实践将帮助开发者构建高效、稳定的嵌入式存储解决方案,从而在物联网应用中实现数据的持久化保存。希望本文能为您的开发工作提供有价值的参考。