当前位置: 首页 > news >正文

ESP32 分区表介绍

前言

  1. 个人邮箱:zhangyixu02@gmail.com
  2. 关于分区表,很多人看了很多资料很可能依旧是一脸懵逼。不知道各位有没有玩过 EEPROM,他可以断电保存数据。这里你也可以理解为分区表将 Flash 中划分出来了一个 EEPROM。
  3. 虽然这样说从专业的角度是毫无疑问大错特错,但是你可以这样理解。
  4. 关于各种存储器相关内容,可以阅读这篇博客 : 半岛体存储器常见类型简介
  5. 这里需要注意的一点是,当前介绍的函数并不是文件系统,整体而言是简陋和底层的。ESP32 的文件系统如果你感兴趣,会发现本质就是调用的本篇博客所介绍的函数,进行了一层封装。

CSV文件介绍

语法介绍

  1. 如下为分区表的类型介绍。
  2. 需要注意的是,当 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)
  1. 如下为一个常见的分区表。一般来说,只需要在这三个分区后面追加你想要添加的内容即可。
  • 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,
  1. 我们看到上面的 offset 并没有写上偏移地址,这是为什么呢?因为有一个默认的二级 Bootloader会存储在起始地址为 0x1000 的地方,大小 0x7000。同时,我们的分区表也需要占用空间,紧跟在二级 Bootloader之后,起始地址为 0x8000,大小为0x1000。因此 nvs 起始地址为 0x9000。
  2. 想必这个时候有人可能会问了,二级 Bootloader 为什么起始地址是 0x1000 呢?这个是由 ROM 引导程序决定的,我们在 ESP-IDF 中无法修改。而且这个不同的芯片型号 二级 Bootloader 并不是固定为 0x1000 ,这个由不同的芯片型号决定。如下图所言。
芯片型号二级 Bootloader 起始地址
ESP32/ESP32S20x1000
ESP32P40x2000
其他芯片0x0000

在这里插入图片描述

  1. 虽然说,二级 Bootloader 的起始地址是固定的,大小可以通过设置分区表起始地址来配置。我们进入 menuconfig -> Partition Table -> Offset of partition table 即可。
  2. 这里需要注意,如果设置的起始地址必须是 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) 对齐。
  1. 这个时候我们需要思考一个问题了,不知道各位是否遇到过一个问题。如果你程序有配网相关的程序,如果配网失败,整个程序就会重启。这个时候,你在配网的时候,内容写错了,最终导致程序反复重启。之后你重新烧录程序,发现程序依旧反复重启。这个是为什么呢?
  2. 我们这个时候就可以结合上面的内容了,因为 ESP32 的 app 是需要和 0x10000(64KB) 对齐 ,为了提高程序烧录效率,程序实际是从 0x10000 开始 擦写。因此,存储配网信息的 nvs 区域并没有被擦除,你代码中可能是设置的三,如果检测到 nvs 有配网信息,那么就不再次配网直接连接,因此导致了反复配网失败,然后重启。
# shell 中调用该命令将闪存全部擦除
idf.py erase-flash
# 代码中调用该函数将 nvs 区内存闪存
nvs_flash_erase();
  1. 我们可以输入如下命令看看最终分区表的内容是否符合我上述所说的预期。可以发现,结果是符合的。
➜  sample_project idf.py partition-table*******************************************************************************
# ESP-IDF Partition Table
# Name, Type, SubType, Offset, Size, Flags
nvs,data,nvs,0x9000,24K,
phy_init,data,phy,0xf000,4K,
factory,app,factory,0x10000,1M,
user,64,1,0x110000,4K,
*******************************************************************************

默认分区表和自定义分区表

  1. 乐鑫官方的 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
  1. 我们可以进入 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 : 自定义分区表

在这里插入图片描述

  1. 如果是采用的自定义分区,我们可以在 menuconfig 的 (Top) → Partition Table -> Custom partition CSV file 中配置自定义分区表文件名称

在这里插入图片描述

常见 API 介绍

寻找分区

esp_partition_find

  1. 根据给定的分区类型、子类型和标签查找符合条件的所有分区。他最终返回的是一个迭代器。
/*** @brief 根据一个或多个参数查找分区** @param type 分区类型,可以是 esp_partition_type_t 的值或 8 位无符号整数。*             要查找所有类型的分区,可以使用 ESP_PARTITION_TYPE_ANY,并将*             subtype 参数设置为 ESP_PARTITION_SUBTYPE_ANY。* @param subtype 分区子类型,可以是 esp_partition_subtype_t 的值或 8 位无符号整数。*                要查找所有给定类型的分区,可以使用 ESP_PARTITION_SUBTYPE_ANY。* @param label (可选)分区标签。如果要查找特定名称的分区,请设置此值。*             否则传递 NULL。** @return 可以用于枚举所有找到的分区的迭代器,如果未找到任何分区,则返回 NULL。*         通过此函数获得的迭代器在不再使用时必须使用 esp_partition_iterator_release 释放。*/
esp_partition_iterator_t esp_partition_find(esp_partition_type_t type, esp_partition_subtype_t subtype, const char* label);
  1. 用术语解释可能会比较麻烦,这里直接上代码会方便一点。假设现在我们有两个 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); // 释放迭代器
  1. 最终打印内容
I (429) main: Found partition: userI (429) main: Found partition: user1

esp_partition_find_first

  1. 找到指定分区,与上面的区别在于,如果有两块 Type 、 SubType 和 label 一样的,那么他将只会找到第一个数据。如果你指定了 Type、SubType 和 label,我个人建议使用这个函数,因为他找到对应的数据之后会立刻返回,并不会浪费时间继续往下执行。
/*** @brief 根据一个或多个参数查找第一个分区** @param type 分区类型,可以是 esp_partition_type_t 的值或 8 位无符号整数。*             要查找所有类型的分区,可以使用 ESP_PARTITION_TYPE_ANY,并将*             subtype 参数设置为 ESP_PARTITION_SUBTYPE_ANY。* @param subtype 分区子类型,可以是 esp_partition_subtype_t 的值或 8 位无符号整数。*                要查找所有给定类型的分区,可以使用 ESP_PARTITION_SUBTYPE_ANY。* @param label (可选)分区标签。如果要查找特定名称的分区,请设置此值。*             否则传递 NULL。** @return 指向 esp_partition_t 结构的指针,如果未找到分区,则返回 NULL。*         该指针在应用程序的生命周期内有效。*/
const esp_partition_t* esp_partition_find_first(esp_partition_type_t type, esp_partition_subtype_t subtype, const char* label);
  1. 代码
# 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);
  1. 最终打印内容
I (439) main: esp_partition_find_first Found partition: user

迭代器进行的操作

esp_partition_get

  1. 当我们调用 esp_partition_find() 函数获取到迭代器了,这时就需要得到迭代器中的分区信息,此时就可以调用当前函数。使用方法参考 esp_partition_find() 介绍的例程。
/*** @brief 获取给定分区的 esp_partition_t 结构** @param iterator 使用 esp_partition_find 获得的迭代器。必须非 NULL。** @return 指向 esp_partition_t 结构的指针。该指针在应用程序的生命周期内有效。*/
const esp_partition_t* esp_partition_get(esp_partition_iterator_t iterator);

esp_partition_next

  1. 在讲解 esp_partition_find() 函数的时候,我们需要依次打印所有符合条件的分区信息,那么就需要调用当前函数进行移动。
/*** @brief 移动分区迭代器到下一个找到的分区** 这个调用后的迭代器副本将失效。** @param iterator 使用 esp_partition_find 获得的迭代器。必须非 NULL。** @return 如果未找到分区,则返回 NULL,否则返回有效的 esp_partition_iterator_t。*/
esp_partition_iterator_t esp_partition_next(esp_partition_iterator_t iterator);

esp_partition_iterator_release

  1. 当我们使用完迭代器后,就需要调用当前函数释放迭代器。
/*** @brief 释放分区迭代器** @param iterator 使用 esp_partition_find 获得的迭代器。*                 迭代器可以是 NULL,因此在调用此函数之前不需要检查其值。**/
void esp_partition_iterator_release(esp_partition_iterator_t iterator);

分区中常见操作 API

esp_partition_erase_range

  1. 这个是进行擦除操作,你需要传入需要擦除的分区。需要注意,因为扇区为 0x1000(4kb) 因此你的偏移地址和擦写范围需要和 0x1000(4kb) 对齐。
/*** @brief 擦除分区的一部分** @param partition 使用 esp_partition_find_first 或 esp_partition_get 获取的分区结构的指针。*                  必须非空。* @param offset 擦除操作的起始偏移量,从分区开始处计算。*               必须与 partition->erase_size 对齐。* @param size 要擦除的范围大小,以字节为单位。*             必须是 partition->erase_size 的整数倍。** @return 如果范围成功擦除,返回 ESP_OK;*         如果迭代器或 dst 为 NULL,返回 ESP_ERR_INVALID_ARG;*         如果擦除超出了分区范围,返回 ESP_ERR_INVALID_SIZE;*         如果分区为只读,返回 ESP_ERR_NOT_ALLOWED;*         或者返回来自底层闪存驱动程序的错误代码之一。*/
esp_err_t esp_partition_erase_range(const esp_partition_t* partition,size_t offset, size_t size);
  1. 如下为打印扇区大小和进行擦写的示例。
    // 打印扇区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

  1. 这里向指定的分区写入数据,需要注意,如果是对标有**加密(encryption)**标志的区域,该函数将会变成 esp_flash_write_encrypted() 函数自动写入,此时这里的 dst_offset 和 size 要求 16 字节的倍数
  2. 如果是没有加密的分区,那么将不会存在这样的限制。
/*** @brief 向分区中写入数据** 在将数据写入闪存之前,需要先擦除闪存的相应区域。* 可以使用 esp_partition_erase_range 函数完成此操作。** 标记为加密的分区将自动通过 esp_flash_write_encrypted() 函数进行写入。如果写入加密分区,所有写入偏移量和长度必须是 16 字节的倍数。有关详细信息,请参见 esp_flash_write_encrypted() 函数。未加密的分区没有此限制。** @param partition 指向通过 esp_partition_find_first 或 esp_partition_get 获得的分区结构的指针。必须非 NULL。* @param dst_offset 数据应写入的地址,相对于分区的开始位置。* @param src 指向源缓冲区的指针。指针必须非 NULL,且缓冲区至少要有 'size' 字节长。* @param size 要写入的数据大小,以字节为单位。** @note 在将数据写入闪存之前,请确保通过 esp_partition_erase_range 调用擦除闪存。** @return ESP_OK,表示数据写入成功;*         ESP_ERR_INVALID_ARG,表示 dst_offset 超过分区大小;*         ESP_ERR_INVALID_SIZE,表示写入超出分区边界;*         ESP_ERR_NOT_ALLOWED,表示分区为只读;*         或来自低级闪存驱动程序的错误代码之一。*/
esp_err_t esp_partition_write(const esp_partition_t* partition,size_t dst_offset, const void* src, size_t size);

esp_partition_read

  1. 该函数将会从分区表指定的区域读取数据。操作的单位为字节。
/*** @brief 从分区中读取数据** 标记为加密的分区将自动通过缓存映射进行读取和解密。** @param partition 指向通过 esp_partition_find_first 或 esp_partition_get 获得的分区结构的指针。必须非 NULL。* @param dst 指向应存储数据的缓冲区的指针。指针必须非 NULL,且缓冲区至少要有 'size' 字节长。* @param src_offset 要读取的数据的地址,相对于分区的开始位置。* @param size 要读取的数据大小,以字节为单位。** @return ESP_OK,表示数据读取成功;*         ESP_ERR_INVALID_ARG,表示 src_offset 超过分区大小;*         ESP_ERR_INVALID_SIZE,表示读取超出分区边界;*         或来自低级闪存驱动程序的错误代码之一。*/
esp_err_t esp_partition_read(const esp_partition_t* partition,size_t src_offset, void* dst, size_t size);

示例

修改 menuconfig

  1. 进入 menuconfig 找到 (Top) → Partition Table → Partition Table 设置为 Custom partition table CSV。
    在这里插入图片描述
  2. 进入 menuconfig 找到 (Top) → Partition Table → Custom partition CSV file 中配置自定义分区表文件名称
    在这里插入图片描述

修改 csv 文件

  1. 因为上面我们设置的自定义分区表文件名称为 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 文件

  1. 如下代码为上述内容的集合。
#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";// 从分区偏移位置 5 写入字符串ESP_ERROR_CHECK(esp_partition_write(partition_res,5, test_str, strlen(test_str)));// 从分区偏移位置 10 读取字符串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));}
}

参考

  1. idf.py menuconfig
  2. 乐鑫官方文档 : 引导加载程序(Bootloader)
  3. 乐鑫官方文档 : 分区表
  4. B站:【2024最新版 ESP32教程(基于ESP-IDF)】ESP32入门级开发课程 更新中 中文字幕

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 通配符证书:轻松管理您的子域名安全
  • Java中实现一个定时任务并在特定时刻弹出窗口提醒用户需要放松休息
  • 大模型19:微调大模型方法
  • 《黑神话.悟空》:一场跨越神话与现实的深度探索
  • RTC时钟测试
  • C# Queue 队列
  • 【RabbitMQ】高级特性
  • Linux: 忘记密码的解决方法,passwd
  • ROUTE_STATUS
  • C# 一个队列两个线程,一个线程入,一个线程出,数据不一致的原因
  • centos 服务器之间实现免密登录
  • CSS的层叠和继承
  • C#学习之路day1
  • 【Python进阶】学习Python必备的练习题,学会这些,说明你对Python已经基本了解了!!!
  • Node.js 安装教程
  • (ckeditor+ckfinder用法)Jquery,js获取ckeditor值
  • 【跃迁之路】【444天】程序员高效学习方法论探索系列(实验阶段201-2018.04.25)...
  • 07.Android之多媒体问题
  • CentOS7简单部署NFS
  • E-HPC支持多队列管理和自动伸缩
  • Flannel解读
  • isset在php5.6-和php7.0+的一些差异
  • quasar-framework cnodejs社区
  • Vue UI框架库开发介绍
  • 测试如何在敏捷团队中工作?
  • 第十八天-企业应用架构模式-基本模式
  • 短视频宝贝=慢?阿里巴巴工程师这样秒开短视频
  • 个人博客开发系列:评论功能之GitHub账号OAuth授权
  • 利用阿里云 OSS 搭建私有 Docker 仓库
  • 深度学习入门:10门免费线上课程推荐
  • 视频flv转mp4最快的几种方法(就是不用格式工厂)
  • 试着探索高并发下的系统架构面貌
  • 我有几个粽子,和一个故事
  • - 语言经验 - 《c++的高性能内存管理库tcmalloc和jemalloc》
  • 1.Ext JS 建立web开发工程
  • 阿里云移动端播放器高级功能介绍
  • # 执行时间 统计mysql_一文说尽 MySQL 优化原理
  • #100天计划# 2013年9月29日
  • #pragma pack(1)
  • #我与Java虚拟机的故事#连载05:Java虚拟机的修炼之道
  • (2024.6.23)最新版MAVEN的安装和配置教程(超详细)
  • (Redis使用系列) Springboot 使用redis实现接口Api限流 十
  • (办公)springboot配置aop处理请求.
  • (编译到47%失败)to be deleted
  • (二)原生js案例之数码时钟计时
  • (附源码)springboot学生选课系统 毕业设计 612555
  • (论文阅读23/100)Hierarchical Convolutional Features for Visual Tracking
  • (每日持续更新)信息系统项目管理(第四版)(高级项目管理)考试重点整理第3章 信息系统治理(一)
  • (生成器)yield与(迭代器)generator
  • (十八)Flink CEP 详解
  • (限时免费)震惊!流落人间的haproxy宝典被找到了!一切玄妙尽在此处!
  • (自适应手机端)行业协会机构网站模板
  • ******之网络***——物理***
  • .net core Swagger 过滤部分Api
  • .NET Core 实现 Redis 批量查询指定格式的Key