前言
个人邮箱:zhangyixu02@gmail.com 关于分区表,很多人看了很多资料很可能依旧是一脸懵逼。不知道各位有没有玩过 EEPROM,他可以断电保存数据。这里你也可以理解为分区表 将 Flash 中划分出来了一个 EEPROM。 虽然这样说从专业的角度是毫无疑问大错特错,但是你可以这样理解。 关于各种存储器相关内容,可以阅读这篇博客 : 半岛体存储器常见类型简介 这里需要注意的一点是,当前介绍的函数并不是文件系统,整体而言是简陋和底层的。ESP32 的文件系统如果你感兴趣,会发现本质就是调用的本篇博客所介绍的函数,进行了一层封装。
CSV文件介绍
语法介绍
如下为分区表的类型介绍。 需要注意的是,当 Type 被指定为 app 类型时,flags 会被强制加密。
类型 分区属性 值类型 Name 分区名称 用于标识分区 Type 类型 app、data、0x40-0xFE(自定义) SubType 子类型 Type=app(可选 factory、ota0 ~ ota15) Type=data(可选 ota、phy、nvs等) Offset 偏移地址 分区在 Flash 中的起始地址 Size 分区大小 分区占用空间 flags 标志 可选 加密(encrypted)和 仅可读(readonly)
如下为一个常见的分区表。一般来说,只需要在这三个分区后面追加你想要添加的内容即可。
nvs : 用来存储想断电保存的数据 。例如每台设备的 wifi 数据,当芯片上电后,会查看这里有没有 wifi 数据,如果有就会直接连接网络,如果没有就需要进行配网。 phy_init : 用于存储 wifi 物理层初始化数据 ,这样可以保证每个设备单独配置 wifi 物理层数据,优化 wifi 性能。 factory : 默认的 APP 程序分区。二级 Bootloader 执行完成后立刻执行这个程序,但是需要注意的是,如果 SubType 中存在 ota 的分区,那么 Bootloader 将会检查 ota 分区内容再决定启动哪个分区里面的内容,主要是为了做 OTA 升级使用。
# Name, Type, SubType, Offset, Size, Flags
# Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap
nvs, data, nvs, , 0x6000,
phy_init, data, phy, , 0x1000,
factory, app, factory, , 1M,
我们看到上面的 offset 并没有写上偏移地址,这是为什么呢?因为有一个默认的二级 Bootloader 会存储在起始地址为 0x1000 的地方,大小 0x7000。同时,我们的分区表 也需要占用空间,紧跟在二级 Bootloader 之后,起始地址为 0x8000,大小为0x1000。因此 nvs 起始地址为 0x9000。 想必这个时候有人可能会问了,二级 Bootloader 为什么起始地址是 0x1000 呢?这个是由 ROM 引导程序决定的,我们在 ESP-IDF 中无法修改。而且这个不同的芯片型号 二级 Bootloader 并不是固定为 0x1000 ,这个由不同的芯片型号决定。如下图所言。
芯片型号 二级 Bootloader 起始地址 ESP32/ESP32S2 0x1000 ESP32P4 0x2000 其他芯片 0x0000
虽然说,二级 Bootloader 的起始地址是固定的,大小可以通过设置分区表 的起始地址 来配置。我们进入 menuconfig -> Partition Table -> Offset of partition table 即可。 这里需要注意,如果设置的起始地址必须是 0x1000 的倍数 ,因为 ESP32 的闪存扇区(最小可擦除单元) 为 0x1000(4KB)。因此,分区表虽然大概率用不上 4KB 这么大的内存,依旧给它分配这么多空间,就是因为需要进行对齐操作。
8. 虽然 ESP32 的闪存 扇区为 4KB,但是为了优化性能 、简化分区管理 ,所以 APP 程序必须与 块 (Block) 0x10000(64KB) 对齐。 9. 这里在总结:
偏移地址 : app 分区必须与 0x10000 (64 KB) 对齐,其他分区与 0x1000 (4 KB) 对齐。 大小:如果没有启用安全启动 V1 ,那么 app 分区大小需要与 0x1000 (4 KB) 对齐。否则 app 分区需要与 0x10000 (64 KB) 对齐。其他分区与 0x1000 (4 KB) 对齐。
这个时候我们需要思考一个问题了,不知道各位是否遇到过一个问题。如果你程序有配网相关的程序,如果配网失败,整个程序就会重启。这个时候,你在配网 的时候,内容写错了,最终导致程序反复重启。之后你重新烧录程序,发现程序依旧反复重启。这个是为什么呢? 我们这个时候就可以结合上面的内容了,因为 ESP32 的 app 是需要和 0x10000(64KB) 对齐 ,为了提高程序烧录效率,程序实际是从 0x10000 开始 擦写。因此,存储配网信息 的 nvs 区域并没有被擦除,你代码中可能是设置的三,如果检测到 nvs 有配网信息,那么就不再次配网直接连接,因此导致了反复配网失败,然后重启。
# shell 中调用该命令将闪存全部擦除
idf.py erase-flash
# 代码中调用该函数将 nvs 区内存闪存
nvs_flash_erase();
我们可以输入如下命令看看最终分区表的内容是否符合我上述所说的预期。可以发现,结果是符合的。
➜ sample_project idf.py partition-table
*******************************************************************************
nvs,data,nvs,0x9000,24K,
phy_init,data,phy,0xf000,4K,
factory,app,factory,0x10000,1M,
user,64,1,0x110000,4K,
*******************************************************************************
默认分区表和自定义分区表
乐鑫官方的 partition_table 在如下路径中可以找到。
partitions_singleapp_coredump.csv : 定义了一个单应用程序的分区表,其中包含一个用于核心转储(coredump)的分区。核心转储用于在设备崩溃时保存内存内容,以便进行故障排查。 partitions_singleapp.csv : 定义了一个单应用程序的分区表。适用于没有启用 OTA(Over-The-Air)更新的设备。 partitions_singleapp_encr_nvs.csv : 定义了一个单应用程序的分区表,并启用了 NVS(非易失性存储)加密。适用于需要保护 NVS 数据的场景。 partitions_singleapp_large_coredump.csv : 定义了一个单应用程序的分区表,包含一个较大的核心转储(coredump)分区。适用于需要更大核心转储空间的应用场景。 partitions_singleapp_large.csv : 定义了一个单应用程序的分区表,适用于需要较大分区空间的应用。没有启用 OTA 更新或 NVS 加密。 partitions_singleapp_large_encr_nvs.csv : 定义了一个单应用程序的分区表,并启用了 NVS 加密,同时分配了较大的应用程序分区。 partitions_two_ota_coredump.csv : 定义了一个支持双 OTA 更新的分区表,同时包含一个用于核心转储的分区。适用于需要 OTA 更新和核心转储功能的设备。 partitions_two_ota.csv : 定义了一个支持双 OTA 更新的分区表。不包含核心转储分区。适用于需要 OTA 更新的设备。 partitions_two_ota_encr_nvs.csv : 定义了一个支持双 OTA 更新的分区表,并启用了 NVS 加密。适用于需要 OTA 更新和保护 NVS 数据的设备。
${esp-idf} /components/partition_table
我们可以进入 menuconfig 找到 (Top) → Partition Table → Partition Table 路径配置自己希望的分区表类型。
Single factory app, no OTA : 使用上述的 partitions_singleapp.csv 分区表 Single factory app (large), no OTA : 使用上述的 partitions_singleapp_large.csv 分区表 Factory app, two OTA definitions : 使用上述的 partitions_two_ota.csv 分区表 Custom partition table CSV : 自定义分区表
如果是采用的自定义分区,我们可以在 menuconfig 的 (Top) → Partition Table -> Custom partition CSV file 中配置自定义分区表文件名称 。
常见 API 介绍
寻找分区
esp_partition_find
根据给定的分区类型、子类型和标签查找符合条件的所有分区。他最终返回的是一个迭代器。
esp_partition_iterator_t esp_partition_find ( esp_partition_type_t type, esp_partition_subtype_t subtype, const char * label) ;
用术语解释可能会比较麻烦,这里直接上代码会方便一点。假设现在我们有两个 Type 和 SubType 一样的数据,我想将两个都给找到。那么就可以使用如下方法
# Name, Type, SubType, Offset, Size, Flags
# Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap
nvs, data, nvs, , 0x6000,
phy_init, data, phy, , 0x1000,
factory, app, factory, , 1M,
user, 0x40, 0x01, , 0x1000,
user1, 0x40, 0x01, , 0x1000,
esp_partition_iterator_t it = esp_partition_find ( USER_PARTITION_TYPE, USER_PARTITION_SUBTYPE, NULL ) ;
if ( it == NULL ) {
ESP_LOGI ( TAG, "esp_partition_find err" ) ;
return ;
}
const esp_partition_t * partition;
while ( ( partition = esp_partition_get ( it) ) != NULL ) {
ESP_LOGI ( TAG, "Found partition: %s\n" , partition-> label) ;
it = esp_partition_next ( it) ;
if ( it == NULL ) {
break ;
}
}
esp_partition_iterator_release ( it) ;
最终打印内容
I ( 429 ) main: Found partition: user
I ( 429 ) main: Found partition: user1
esp_partition_find_first
找到指定分区,与上面的区别在于,如果有两块 Type 、 SubType 和 label 一样的,那么他将只会找到第一个数据。如果你指定了 Type、SubType 和 label,我个人建议使用这个函数,因为他找到对应的数据之后会立刻返回,并不会浪费时间继续往下执行。
const esp_partition_t * esp_partition_find_first ( esp_partition_type_t type, esp_partition_subtype_t subtype, const char * label) ;
代码
# Name, Type, SubType, Offset, Size, Flags
# Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap
nvs, data, nvs, , 0x6000,
phy_init, data, phy, , 0x1000,
factory, app, factory, , 1M,
user, 0x40, 0x01, , 0x1000,
user1, 0x40, 0x01, , 0x1000,
partition_res = esp_partition_find_first ( USER_PARTITION_TYPE, USER_PARTITION_SUBTYPE, NULL ) ;
if ( partition_res == NULL )
{
ESP_LOGI ( TAG, "Can't find partition,return" ) ;
return ;
}
ESP_LOGI ( TAG, "esp_partition_find_first Found partition: %s\n" , partition_res-> label) ;
最终打印内容
I ( 439 ) main: esp_partition_find_first Found partition: user
迭代器进行的操作
esp_partition_get
当我们调用 esp_partition_find()
函数获取到迭代器了,这时就需要得到迭代器中的分区信息,此时就可以调用当前函数。使用方法参考 esp_partition_find()
介绍的例程。
const esp_partition_t * esp_partition_get ( esp_partition_iterator_t iterator) ;
esp_partition_next
在讲解 esp_partition_find()
函数的时候,我们需要依次打印所有符合条件的分区信息,那么就需要调用当前函数进行移动。
esp_partition_iterator_t esp_partition_next ( esp_partition_iterator_t iterator) ;
esp_partition_iterator_release
当我们使用完迭代器后,就需要调用当前函数释放迭代器。
void esp_partition_iterator_release ( esp_partition_iterator_t iterator) ;
分区中常见操作 API
esp_partition_erase_range
这个是进行擦除操作,你需要传入需要擦除的分区。需要注意,因为扇区为 0x1000(4kb) 因此你的偏移地址和擦写范围需要和 0x1000(4kb) 对齐。
esp_err_t esp_partition_erase_range ( const esp_partition_t * partition,
size_t offset, size_t size) ;
如下为打印扇区大小和进行擦写的示例。
ESP_LOGI ( TAG, "partition->erase_size : 0x%lx" , partition_res-> erase_size) ;
ESP_ERROR_CHECK ( esp_partition_erase_range ( partition_res, 0 * partition_res-> erase_size, 1 * partition_res-> erase_size) ) ;
esp_partition_write
这里向指定的分区写入数据,需要注意,如果是对标有**加密(encryption)**标志的区域,该函数将会变成 esp_flash_write_encrypted()
函数自动写入,此时这里的 dst_offset 和 size 要求 16 字节的倍数 。 如果是没有加密的分区,那么将不会存在这样的限制。
esp_err_t esp_partition_write ( const esp_partition_t * partition,
size_t dst_offset, const void * src, size_t size) ;
esp_partition_read
该函数将会从分区表指定的区域读取数据。操作的单位为字节。
esp_err_t esp_partition_read ( const esp_partition_t * partition,
size_t src_offset, void * dst, size_t size) ;
示例
修改 menuconfig
进入 menuconfig 找到 (Top) → Partition Table → Partition Table 设置为 Custom partition table CSV。 进入 menuconfig 找到 (Top) → Partition Table → Custom partition CSV file 中配置自定义分区表文件名称 。
修改 csv 文件
因为上面我们设置的自定义分区表文件名称 为 partitions_user.csv,因此我们需要创建一个名称为 partitions_user.csv 的文件,然后加入如下内容。
# Name, Type, SubType, Offset, Size, Flags
# Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap
nvs, data, nvs, , 0x6000,
phy_init, data, phy, , 0x1000,
factory, app, factory, , 1M,
user, 0x40, 0x01, , 0x1000,
user1, 0x40, 0x01, , 0x1000,
调整 c 文件
如下代码为上述内容的集合。
# include <stdio.h>
# include <string.h>
# include "freertos/FreeRTOS.h"
# include "freertos/task.h"
# include "esp_log.h"
# include "esp_partition.h"
static const char * TAG = "main" ;
# define USER_PARTITION_TYPE 0x40
# define USER_PARTITION_SUBTYPE 0x01
static char g_esp_buf[ 1024 ] ;
void app_main ( void )
{
esp_partition_iterator_t it = esp_partition_find ( USER_PARTITION_TYPE, USER_PARTITION_SUBTYPE, NULL ) ;
if ( it == NULL ) {
ESP_LOGI ( TAG, "esp_partition_find err" ) ;
return ;
}
const esp_partition_t * partition;
while ( ( partition = esp_partition_get ( it) ) != NULL ) {
ESP_LOGI ( TAG, "Found partition: %s\n" , partition-> label) ;
it = esp_partition_next ( it) ;
if ( it == NULL ) {
break ;
}
}
esp_partition_iterator_release ( it) ;
static const esp_partition_t * partition_res = NULL ;
partition_res = esp_partition_find_first ( USER_PARTITION_TYPE, USER_PARTITION_SUBTYPE, NULL ) ;
if ( partition_res == NULL )
{
ESP_LOGI ( TAG, "Can't find partition,return" ) ;
return ;
}
ESP_LOGI ( TAG, "esp_partition_find_first Found partition: %s\n" , partition_res-> label) ;
ESP_LOGI ( TAG, "partition->erase_size : 0x%lx" , partition_res-> erase_size) ;
ESP_ERROR_CHECK ( esp_partition_erase_range ( partition_res, 0 * partition_res-> erase_size, 1 * partition_res-> erase_size) ) ;
const char * test_str = "this is for test string" ;
ESP_ERROR_CHECK ( esp_partition_write ( partition_res, 5 , test_str, strlen ( test_str) ) ) ;
ESP_ERROR_CHECK ( esp_partition_read ( partition_res, 10 , g_esp_buf, strlen ( test_str) - 5 ) ) ;
ESP_LOGI ( TAG, "Read partition str:%s" , g_esp_buf) ;
while ( 1 )
{
vTaskDelay ( pdMS_TO_TICKS ( 1000 ) ) ;
}
}
参考
idf.py menuconfig 乐鑫官方文档 : 引导加载程序(Bootloader) 乐鑫官方文档 : 分区表 B站:【2024最新版 ESP32教程(基于ESP-IDF)】ESP32入门级开发课程 更新中 中文字幕