【ESP32笔记】ESP中的分区表

本节目标:看懂分区表 CSV 的每一列,会给工程添加自定义分区,并用 esp_partition API 完成擦除、写入、读取

一、本节要解决的问题

ESP32 的程序不是"整个 bin 一股脑烧到某个固定地址",而是把整块 flash 划分成多个分区(partition),每个分区承担一种职责:放 bootloader、放分区表自己、放应用程序、存 NVS 配置、存 PHY 校准数据、放 OTA 固件、当文件系统、存用户数据......

这样划分的好处:

  • 好管理:大工程按功能分区,升级程序只烧 app 分区,不会动到配置数据;
  • 功能清晰:bootloader 靠分区表就知道哪块 flash 是"要启动的程序"、哪块是"数据";
  • 避免冲突:擦除 NVS 不会误擦程序,OTA 升级也不会破坏用户数据。

如果不用分区表,所有地址都得写死在代码里,应用一多就极易互相踩踏。本节先把 ESP-IDF 默认分区表逐列拆开分析,再自定义一个分区,并用官方 API 对它擦写、读取。

二、相关知识:ESP32 flash 的固定布局

ESP32 的 flash 从 0x0 开始编址,有几处地址是约定俗成、不能乱动的:

区域 起始地址 大小 说明
Bootloader 0x1000 - 上电后最先运行的程序
分区表 0x8000 0x1000 描述"其余 flash 怎么分"的那张表
第一个可用分区 0x9000 - 除开上面两块,剩下的从 0x9000 开始分配

所以分区表里所有分区的 Offset 都必须大于等于 0x9000;默认分区表的第一个分区(nvs)正是从 0x9000 开始的。

三、知识点

3.1 分区表 CSV 的六列

分区表是一个逗号分隔的文本文件(.csv),# 开头的是注释。列头固定为六列,教程重点要求看懂前五列:

列名 含义 取值说明
Name 分区名称(标签) 如 nvs、factory、user;同一张表内不能重名
Type 分区类型 app(0x00) 放程序;data(0x01) 放数据;用户自定义可写 0x40~0xFE
SubType 子类型,含义随 Type 变化 Type=app:factory(0x00)、ota_0(0x10)~ota_15(0x1F);Type=data:ota(0x00)、phy(0x01)、nvs(0x02)、nvs_keys(0x04) 及组件特定子类型
Offset 分区在 flash 中的起始地址 可以留空,由构建工具自动计算;不能与其他分区重叠;app 分区必须与 0x10000 对齐
Size 分区总大小 支持 1K、1M 这种写法(1M = 0x100000)
Flags 标志位 本节两个例子都留空,入门阶段不用管

逐列再补充几句:

  • Name:教程说它"并不重要",其实它有两个用处------保证表内唯一,便于管理;以及可以被 API 当作 label 按名字查找分区(配套例程后面正是这样用)。
  • Type:只分 app 和 data 两大类,其他值(0x40~0xFE)全部留给用户自定义;0x00~0x3F 是系统保留范围。
  • SubType:它必须结合 Type 理解------同是 0x00,在 app 下是 factory,在 data 下就是 ota;自定义类型的子类型怎么写、怎么解释,完全由用户自己约定。
  • Offset:最灵活也最容易出错的一列,留空时由工具计算,手填时必须自己保证不重叠。

3.2 Offset 留空时的自动计算规则

Offset 不写时,构建工具从 0x9000 开始,把上一个分区的 Offset + Size作为下一个分区的起点,并按类型做对齐(app 补齐到 0x10000,data 只要求 4 字节对齐)。以最简单的默认分区表为例:

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,
分区 计算过程 实际 Offset
nvs 紧接着分区表,从 0x9000 开始 0x9000,大小 0x6000(24KB)
phy_init 0x9000 + 0x6000 0xF000,大小 0x1000
factory 0xF000 + 0x1000,app 对齐到 0x10000 0x10000,大小 1M

3.3 各个分区的作用

分区 Type / SubType 作用
nvs data / nvs 非易失存储区,掉电数据不丢;存放 PHY 校准数据、Wi-Fi 配置、应用自己的键值数据(下一节详讲)
phy_init data / phy 存放每台设备单独的 PHY 初始化数据;默认配置下并不启用,PHY 数据直接编进应用程序,所以这个分区一般用不到、可以删掉
factory app / factory 默认的应用程序分区,上电后 bootloader 从这里启动程序
otadata data / ota 仅 OTA 分区表有,记录当前该从哪个 OTA 分区启动,大小固定 0x2000
ota_0 / ota_1 app / ota_0、ota_1 仅 OTA 分区表有,两个可升级的应用程序分区,bootloader 依据 otadata 的内容决定加载哪一个
自定义分区 0x40~0xFE / 自定义 用户自己存放数据,怎么解读完全由应用程序决定

3.4 为什么 app 分区不能重叠

编译期 ,ESP-IDF 会用脚本把 CSV 转成二进制分区表,脚本会检查每个分区的地址范围:只要某个分区的起点落进了前一个分区的区间,就直接报 Partitions overlap 并中止编译。这是第一道保险。

运行期更要命:bootloader 按分区表把 app 分区里的程序加载到内存执行,OTA 升级时还会往 ota_x 分区写新固件。两个 app 分区一旦重叠,新固件就会覆盖另一个分区里的程序;下次 bootloader 切回旧分区,读到的就是被破坏的内容,设备可能无法启动。数据分区重叠也是同理------擦其中一块,另一块的数据跟着一起没了。所以手工填 Offset 时一定要自己把地址算清楚。

顺带解释另一个问题:app 分区为什么必须 0x10000 对齐?因为 bootloader 按 64KB 为单位做 flash 到内存的地址映射,不满足对齐要求的分区无法被正常映射执行,工具链会直接拒绝。

3.5 ESP-IDF 内置的默认分区表

ESP-IDF 自带几张常用分区表,放在 IDF 安装目录的 components/partition_table/ 下,在 idf.py menuconfig 的 Partition Table 菜单里选择:

选项 对应文件 内容
Single factory app partitions_singleapp.csv nvs 0x6000 + phy_init 0x1000 + factory 1M,最简单的三行
Single factory app, large partitions_singleapp_large.csv 与上面相同,只是 factory 放大到 1.5M
Factory app, two OTA partitions_two_ota.csv 在单 app 基础上增加 otadata、ota_0、ota_1,支持 OTA 升级
Custom partition table CSV 工程自己的 CSV 勾选后由 CONFIG_PARTITION_TABLE_CUSTOM_FILENAME 指定工程根目录下的 CSV 文件名

两者区别一句话:内置表是"选现成的",自定义表是"自己写一张 CSV 交给编译系统"。不需要额外分区时用内置的最省事;一旦要加 spiffs、自定义数据区(比如本节例程)就必须切到 Custom。

OTA 表里为什么至少要有两个 OTA app 分区?因为 OTA 的基本玩法是:把新固件写进当前没在运行的那个分区,写完整、校验通过后再让 otadata 指向它重启。只有一个分区的话没有"备胎",升级失败就没有退路了。

3.6 本仓库的分区表使用现状

仓库 27 个工程里,有 23 个工程的 sdkconfig 都含有 CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" 这一行,但工程目录里并不存在 partitions.csv;而且这些工程真正的开关是 CONFIG_PARTITION_TABLE_SINGLE_APP=y(CONFIG_PARTITION_TABLE_CUSTOM is not set)。也就是说那行文件名只是一个没被启用的默认值,编译时实际用的仍是 ESP-IDF 内置的 partitions_singleapp.csv。

真正启用了自定义分区表的只有 4 个工程:partition(partitions_user.csv)、spiffs(partitions_example.csv)、web_monitor(partitions_webserver.csv)、onenet(partitions_two_ota_nofactory.csv)。想让自己的工程用上自定义分区表,光改文件名不够,必须在 menuconfig 里把 Partition Table 选项切到 Custom partition table CSV。

四、代码分析

4.1 工程结构

复制代码
partition/
├── CMakeLists.txt
├── partitions_user.csv     # 自定义分区表(本节重点)
├── sdkconfig               # 已启用自定义分区表
└── main/
    ├── CMakeLists.txt
    └── main.c              # 找分区 → 擦除 → 写入 → 读取

sdkconfig 里与本节有关的关键几行(说明自定义分区表已经启用,并指向 partitions_user.csv):

复制代码
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions_user.csv"
CONFIG_PARTITION_TABLE_FILENAME="partitions_user.csv"
CONFIG_PARTITION_TABLE_OFFSET=0x8000

4.2 自定义分区表 partitions_user.csv

在默认三行之后追加了一行 user 分区:Type=0x40(自定义类型)、SubType=0x01(自己约定的标记)、Offset 留空自动计算、Size=0x1000(4KB)。

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,

各行的落点:nvs 在 0x9000、phy_init 在 0xF000、factory 在 0x10000 且到 0x110000 结束,所以 user 自动排到 0x110000,大小 0x1000。因为 Type 是自定义的 0x40,SubType 写 0x01 只是自己作标记,编译系统不会按它做特殊处理;但代码里必须用相同的 0x40/0x01 才能找到这个分区。

4.3 main.c:找分区、擦除、写入、读取

第一步是按类型、子类型和名字找到分区,拿到一个指向分区描述符的指针,后续所有操作都基于它:

c 复制代码
#define USER_PARTITION_TYPE     0x40        //自定义的分区类型
#define USER_PARTITION_SUBTYPE  0x01        //自定义的分区子类型

//分区指针
static const esp_partition_t* partition_res=NULL;

//读取缓存
static char g_esp_buf[1024];

void app_main(void)
{
    //找到自定义分区,返回分区指针,后续用到这个指针进行各种操作
    partition_res=esp_partition_find_first(USER_PARTITION_TYPE,USER_PARTITION_SUBTYPE,"user");
    if(partition_res == NULL)
    {
        ESP_LOGI(TAG,"Can't find partition,return");
        return;
    }

esp_partition_find_first 的三个参数与 CSV 一一对应:Type=0x40、SubType=0x01、Name="user";找不到返回 NULL,所以必须判空。

接着就是 flash 的经典三步------先擦、再写、后读:

c 复制代码
    //擦除
    ESP_ERROR_CHECK(esp_partition_erase_range(partition_res,0x0,0x1000));
    //测试字符串
    const char* test_str = "this is for test string";
    //从分区偏移位置0x0写入字符串
    ESP_ERROR_CHECK(esp_partition_write(partition_res,0x00, test_str, strlen(test_str)));
    //从分区偏移位置0x0读取字符串
    ESP_ERROR_CHECK(esp_partition_read(partition_res,0x00, g_esp_buf, strlen(test_str)));
    ESP_LOGI(TAG,"Read partition str:%s",g_esp_buf);
  • esp_partition_erase_range(分区, 偏移, 大小):这里从 0x0 开始擦满整个 4KB 分区,偏移和大小都必须 0x1000 对齐;
  • esp_partition_write(分区, 偏移, 数据, 长度):写之前必须先擦除,否则写不进去(flash 只能把 1 变 0,擦除才能把 0 恢复成 1);
  • esp_partition_read(分区, 偏移, 缓存, 长度):按字节读回。

最后 while(1) 里延时 1 秒,让程序一直挂着不退出。

与教程不一致的地方:PDF 里 esp_partition_find_first 的第三个参数写的是 NULL(只按类型/子类型查找),仓库代码改成了 "user",即同时用 CSV 中的 Name 作为标签。这样更严格------如果 CSV 里分区名被改动,会直接找不到分区而报错,问题能更早暴露。

另外 PDF 文字说"esp_partition_write 第四个参数是写入的数据内容",这是笔误:第四个参数是长度,数据内容在第三个参数。

五、关键 API 速查

API 作用
esp_partition_find_first(type, subtype, label) 按类型/子类型/标签查找第一个分区,返回 esp_partition_t*,找不到返回 NULL
esp_partition_erase_range(part, offset, size) 擦除分区内一段区域,offset 与 size 必须 0x1000 对齐
esp_partition_write(part, offset, src, size) 向分区写数据,写前必须先擦除
esp_partition_read(part, offset, dst, size) 从分区读数据
ESP_ERROR_CHECK() 检查返回值,出错时打印文件名和行号并重启

以上 esp_partition API 都声明在 esp_partition.h 中。

六、编译与现象验证

bash 复制代码
cd esp32-board/partition
idf.py build
idf.py -p COM<端口> flash monitor

工程 sdkconfig 的目标芯片是 esp32;如果重新指定过目标或清空过配置,先执行 idf.py set-target esp32。

正常运行时串口打印一行关键日志:

复制代码
I (xxx) main: Read partition str:this is for test string

说明写进去的字符串被原样读回来了。

验证"数据真的在 flash 里"的小实验:把 esp_partition_erase_range 和 esp_partition_write 两行注释掉,重新编译烧录,重启后仍然能读出这句话------数据不是存在 RAM 里,而是上次烧录时写进 flash 的。

相关推荐
权球物联分享物联网连接服务1 小时前
农业无人机低空巡检:机库物联网卡联网逻辑与选型要点
物联网·无人机
做萤石二次开发的哈哈1 小时前
海康测温球机接入实战:测温阈值、船只检测、烟火预警事件的ISAPI对接方案
物联网·二次开发·热成像·蓝海aiot一站式工作台·aiot开发·萤石云开放平台·测温球机
Xudde.1 小时前
Billu: box 靶机渗透测试(Vulnhub Writeup)
笔记·学习·安全·web安全
亿道电子Emdoor1 小时前
【Arm】访问陷阱简介
arm开发·单片机·mcu
ouynagda1 小时前
iMX6ull Linux 嵌入式驱动开发笔记(LCD / ADXL345 / SPI 子系统)
linux·arm开发·驱动开发·笔记·嵌入式硬件
qq_284274052 小时前
信号分析·傅里叶级数与频谱(单边谱·双边谱·傅里叶变换)CSDN发布稿
笔记·学习·信号处理·傅里叶分析
xian_wwq2 小时前
【学习笔记】pgvector 入门:用 PostgreSQL 直接实现向量检索-13
笔记·学习·postgresql
一直C2 小时前
嵌入式 Linux 驱动开发学习笔记(三)设备树、GPIO子系统、中断与外设驱动实战
驱动开发·单片机·嵌入式硬件·嵌入式·arm·imx6ull
程序猿乐锅2 小时前
【从0-1学习Python | 第四篇】Python 字符串和元组
java·开发语言·笔记·后端·python·学习