ESP32 IDF开发 驱动篇⑩ 存储NVS高级应用和自定义分区表
别迷路-导航栏
快速导航找到你想要的(文章目录)
此篇文章如果对你有用,请点赞收藏,您的支持就是博主坚持的动力。
1、博主写这篇技术文章的目的:
(1)、了解 分区比的作用;
(2)、掌握 自定义分区表;
(3)、学会 自定义NVS分区的大小;
(3)、学会 对多个NVS分区的读写操作;
2、ESP32 分区表的概述
在“ESP32 IDF开发 驱动篇⑨ 存储NVS的基本应用”章节我们已经介绍了nvs的介绍和基本操作,并且留下了许多疑问。下面就来对这些疑问一一揭开神秘的面纱。
(1)分区表的介绍
分区表文件位于: esp-idf\components\partition_table文件下, 该路径下所有的.csv文件都是用来对Flash分区进行配置如下图:
无OTA分区:partitions_singleapp.csv、partitions_singleapp_coredump.csv
双OTA分区:partitions_two_ota.csv、partitions_two_ota_coredump.csv
打开四个分区表:
分析分区表中的内容
Name:字段可以是任何有意义的名称,但不能超过 16 个字符(之后的内容将被截断)。该字段对 ESP32 并不是特别重要;
Type:字段可以指定为 app (0) 或者 data (1),也可以直接使用数字 0-254(或者十六进制 0x00-0xFE)。注意,0x00-0x3F 不得使用(预留给 esp-idf 的核心功能)。
如果您的应用程序需要保存数据,请在 0x40-0xFE 内添加一个自定义分区类型。
注意,启动加载器将忽略 app (0) 和 data (1) 以外的其他分区类型。
SubType:字段长度为 8 bit,内容与具体 Type 有关。目前,esp-idf 仅仅规定了 “app” 和 “data” 两种子类型。
1、当 Type 定义为 app 时,SubType 字段可以指定为 factory (0),ota_0 (0x10) … ota_15 (0x1F) 或者 test (0x20)。
· factory (0) 是默认的 app 分区。启动加载器将默认加载该应用程序。但如果存在类型为 data/ota 分区,则启动加载器将加载 data/ota 分区中的数据,进而判断启动哪个 OTA 镜像文件。
· OTA 升级永远都不会更新 factory 分区中的内容。
·如果您希望在 OTA 项目中预留更多 flash,可以删除 factory 分区,转而使用 ota_0 分区。
· ota_0 (0x10) … ota_15 (0x1F) 为 OTA 应用程序分区,启动加载器将根据 OTA 数据分区中的数据来决定加载哪个 OTA 应用程序分区中的程序。在使用 OTA 功能时,应用程序应至少拥有 2 个 OTA 应用程序分区(ota_0 和 ota_1) 。
· test (0x2) 为预留 app 子类型,用于工厂测试流程。如果没有其他有效 app 分区,test 将作为备选启动分区使用。也可以在每次启动时配置启动加载器读取 GPIO,如果 GPIO 被拉低则启动该分区。
2、当 Type 定义为 data 时,SubType 字段可以指定为 ota (0),phy (1),nvs (2) 或者 nvs_keys (4)。
· ota (0) 即 OTA 数据分区 ,用于存储当前所选的 OTA 应用程序的信息。这个分区的大小需要设定为 0x2000。
· phy (1) 分区用于存放 PHY 初始化数据,从而保证可以为每个设备单独配置 PHY,而非必须采用固件中的统一 PHY 初始化数据。
·默认配置下,phy 分区并不启用,而是直接将 phy 初始化数据编译至应用程序中,从而节省分区表空间(直接将此分区删掉)。
· 如果需要从此分区加载 phy 初始化数据,请打开项目配置菜单(menuconfig),并且能 CONFIG_ESP32_PHY_INIT_DATA_IN_PARTITION 选项。此时,您还需要手动将 phy 初始化数据烧至设备 flash(esp-idf 编译系统并不会自动完成该操作)。
· nvs (2) 是专门给 非易失性存储 (NVS) API 使用的分区。
·用于存储每台设备的 PHY 校准数据(注意,并不是 PHY 初始化数据)。
·用于存储 Wi-Fi 数据(如果使用了 esp_wifi_set_storage(WIFI_STORAGE_FLASH) 初始化函数)。
· NVS API 还可以用于其他应用程序数据。
·强烈建议您应为 NVS 分区分配至少 0x3000 字节空间。
·如果使用 NVS API 存储大量数据,请增加 NVS 分区的大小(默认是 0x6000 字节)。
·nvs_keys (4) 是 NVS 秘钥分区。
·用于存储加密密钥(如果启用了 NVS 加密 功能)。
·此分区应至少设定为 4096 字节。
其它数据子类型已预留给 esp-idf 未来使用。
Offset和size:分区若偏移地址为空,则会紧跟着前一个分区之后开始;若为首个分区,则将紧跟着分区表开始。
app 分区的偏移地址必须要与 0x10000 (64K) 对齐,如果将偏移字段留空,gen_esp32part.py 工具会自动计算得到一个满足对齐要求的偏移地址。如果 app 分区的偏移地址没有与 0x10000 (64K) 对齐,则该工具会报错。
app 分区的大小和偏移地址可以采用十进制数、以 0x 为前缀的十六进制数,且支持 K 或 M 的倍数单位(分别代表 1024 和 1024*1024 字节)。
如果您希望允许分区表中的分区采用任意起始偏移量 (CONFIG_PARTITION_TABLE_OFFSET),请将分区表(CSV 文件)中所有分区的偏移字段都留空。注意,此时,如果您更改了分区表中任意分区的偏移地址,则其他分区的偏移地址也会跟着改变。这种情况下,如果您之前还曾设定某个分区采用固定偏移地址,则可能造成分区表冲突,从而导致报错。
Flags:当前仅支持 encrypted 标记。如果 Flags 字段设置为 encrypted,且已启用 Flash Encryption 功能,则该分区将会被加密。
app 分区始终会被加密,不管 Flags 字段是否设置。
(2)分区表的使用
双OTA分区将在 【ESP32 IDF开发 应用篇⑱ 空中升级OTA】讲解
Nvs:保存的是用户操作nvs api设置的一些数据
phy_init:区是RF 的数据
factory:应用程序
coredump:存放的是系统运行过程中产生的一些错误快照,当系统崩溃时产出的错处都保存在coredump去,从而可以在随后在 PC 上分析失败的原因。(后续将以实例讲解怎么使用)
otadata:存放启动应用程序的参数
ota_0:升级程序的APP1
ota_1:升级程序的APP2
这篇主要介绍“nvs”默认使用的是partitions_singleapp.csv分区表,从表中可以看到nvs的大小为0x6000 =
24K
3、NVS库的介绍
有关NVS API基本函数在“ESP32 IDF开发 驱动篇⑨ 存储NVS的基本应用”篇已经介绍过了
在这里我们只介绍几个重要的函数
esp_err_t nvs_open(const char* name, nvs_open_mode_t open_mode,nvs_handle_t *out_handle);
从这个函数就可以发现他并没有指定使用哪一个“nvs”,打开源文件
extern "C" esp_err_t nvs_open(const char* name, nvs_open_mode_t open_mode, nvs_handle_t *out_handle)
{
return nvs_open_from_partition(NVS_DEFAULT_PART_NAME, name, open_mode, out_handle);
}
#define NVS_DEFAULT_PART_NAME "nvs" /*!< Default partition name of the NVS partition in the partition table *
nvs_open函数默认使用的“nvs”分区
esp_err_t nvs_get_str (nvs_handle_t handle, const char* key, char* out_value, size_t* length);
esp_err_t nvs_get_blob(nvs_handle_t handle, const char* key, void* out_value, size_t* length);
这几个函数都是对不定长的数据进行存储和读取,可以看到我们需要传入一个长度和一个key。那么在这个过程中我们并不知道要读取的数据长度究竟是多少,所以我们需要传入一个长度的指针进去。
参数介绍:
* @param out_value 指向输出值的指针。对于nvs_get_str和nvs_get_blob,可能为NULL,所需长度的将在length参数中返回。
* @param [inout] length 指向变量out_value长度的非零指针。
*如果out_value为零,则将其设置为长度必须保持该值。 如果out_value不是零,将设置为实际长度值
每次写入之后必须使用 nvs_commit提交
4、软件设计
(1)、初始化nvs
nvs_flash_init()
(2)、打开nvs并创建一个表,以读写的方式打开
nvs_open(“test_list”, NVS_READWRITE, &my_handle);
(3)、根据键值对读写字符串、数组、结构体等数据
nvs_get_str、nvs_get_blob、nvs_set_str、nvs_set_blob
(4)、提交nvs
err = nvs_commit(my_handle);
(4)、关闭nvs
nvs_close(my_handle);
5、实例分析
复制上一个工程改名字为 idf_nvs2即可 文件名字改为 idf_nvs2.C makefile文件也改成 PROJECT_NAME := idf_nvs2即可,然后复制一下代码测试。
添加自定的分区“nvs_1”,在原分区表上添加如下分区,大小自定义这里是4000,
然后在程序里面将默认分区定义为修改后的分区
#define NVS_DEFAULT_PART_NAME “nvs_1”
如果要同时使用两个分区,就需要使用
nvs_open_from_partition(NVS_DEFAULT_PART_NAME, name, open_mode, out_handle);
这个函数来打开多个分区即可。
/**********************************************************************
* 文件名: idf_nvs2.c
* 创建人:
* 创建日期:
* 修改人:
* 修改日期:
* 版本号: V1.1
* 备注:
* 公司:
********************************************************************/
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "nvs.h"
#include "esp_log.h"
#include "nvs_flash.h"
static const char *TAG = "NVS2_";
nvs_handle my_handle;
uint8_t nvs_Array[100]={0};
typedef struct _msgData
{
char data1[10];
uint8_t data2;
}MSG_DATA;
MSG_DATA msg_data; //从机信号机制
/***********************************************************************
* 函数:
* 描述: 主函数
* 参数:
* 返回: 无
* 备注:
************************************************************************/
void app_main()
{
//初始化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();
}
//打开,类似数据库的表
err = nvs_open("test_list", NVS_READWRITE, &my_handle);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "opening NVS Error (%s)!\n", esp_err_to_name(err));
}
else
{
size_t len=0;
char strval[50];
ESP_LOGI(TAG, "NVS open OK");
//字符串
//如果len长度为0,则读取的是数据的长度,保存在len中,参数说明参考API介绍
//如果len长度不为为0,则读取的是实际数据
err = nvs_get_str (my_handle, "str_key", NULL, &len);
if(err==ESP_OK) ESP_LOGI(TAG, "str_key read size = %d", len);
err = nvs_get_str (my_handle, "str_key", strval, &len);
if(err==ESP_OK) ESP_LOGI(TAG, "str_key read data = %s", strval);
//读出大数据,数组
err = nvs_get_blob(my_handle, "blob_arrray", NULL, &len);
if (err == ESP_OK ) ESP_LOGI(TAG, "blob_arrray read size = %d", len);
err = nvs_get_blob(my_handle, "blob_arrray", nvs_Array, &len);
if (err == ESP_OK ) ESP_LOGI(TAG, "blob_arrray read data = %s", nvs_Array);
//读出结构体
err = nvs_get_blob(my_handle, "blob_struct", NULL, &len);
if (err == ESP_OK ) ESP_LOGI(TAG, "blob_struct read size = %d", len);
err = nvs_get_blob(my_handle, "blob_struct", &msg_data, &len);
if (err == ESP_OK )
{
//从结构体msg_data.data1地址开始后面的数据全部输出,所以msg_data.data2的数据也被输出
ESP_LOGI(TAG, "blob_struct read data1 = %s", msg_data.data1);
ESP_LOGI(TAG, "blob_struct read data2 = %s", &msg_data.data2);
}
//写入大数据,数组
for (int i=0;i<10;i++) nvs_Array[i] = i+'0';
for (int i=10;i<100;i++) nvs_Array[i] = 'A';
err = nvs_set_blob(my_handle, "blob_arrray", nvs_Array, 100);
if (err != ESP_OK) ESP_LOGE(TAG, "blob_arrray set Error");
//写入大数据,结构体
for (int i=0;i<10;i++) msg_data.data1[i] = i+'0';//转换成字符
msg_data.data2 = 'B';
err = nvs_set_blob(my_handle, "blob_struct", &msg_data, sizeof(msg_data));
if (err != ESP_OK) ESP_LOGE(TAG, "blob_struct set Error");
//写字符串
err = nvs_set_str (my_handle, "str_key", "str_value test ok!");
if(err!=ESP_OK) ESP_LOGE(TAG, "str_key set Error");
//提交,必须提交才能写入NVS
err = nvs_commit(my_handle);
if(err!=ESP_OK) ESP_LOGE(TAG, "nvs_commit Error");
}
//关闭
nvs_close(my_handle);
//复位倒计时
for (int i = 10; i >= 0; i--) {
printf("Restarting in %d seconds...\n", i);
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
esp_restart();
}
6、以下是调试的结果:
make menuconfig
在Partition Table->Partition Table (Single factory app, no OTA)
编译过程首先编译的就是分区表,我们在make menuconfig时选择的分区表
系统启动可以看到我们添加的分区“nvs_1”大小4000
更多分区变的讲解,请点赞收藏,【ESP32 IDF开发 系统篇⑪ 系统启动流程及硬件复位问题分析】详细讲解分区的启动流程。
所有文章源代码:https://download.csdn.net/download/lu330274924/88518092