ESP32分区表
ESP32-C3 4MB Flash 完整分区布局详解
一、整体地址分布表
| 序号 | 分区名称 | 类型(Type) | 子类型(SubType) | 起始偏移 | 大小 | 十六进制范围 | 作用说明 |
|---|---|---|---|---|---|---|---|
| 0 | Bootloader | — | — | 0x0000 |
32 KB | 0x0000 ~ 0x7FFF |
第二阶段引导程序,上电后ROM加载它,它再去读分区表、加载app |
| 1 | Partition Table | — | — | 0x8000 |
4 KB | 0x8000 ~ 0x8FFF |
分区表本身,二进制格式,记录后面所有分区的地址和大小 |
| 2 | nvs | Data 0x01 |
NVS 0x02 |
0x9000 |
16 KB | 0x9000 ~ 0xCFFF |
Non-Volatile Storage,非易失存储,存WiFi密码、设备参数、用户配置 |
| 3 | otadata | Data 0x01 |
OTA Data 0x00 |
0xD000 |
8 KB | 0xD000 ~ 0xEFFF |
OTA状态标记区,记录下次启动跑factory还是ota_0/ota_1,OTA核心 |
| 4 | phy_init | Data 0x01 |
PHY Init 0x01 |
0xF000 |
4 KB | 0xF000 ~ 0xFFFF |
WiFi射频校准数据,第一次上电校准后保存,下次直接读取不用重新校准 |
| 5 | factory | App 0x00 |
Factory 0x00 |
0x10000 |
1 MB | 0x10000 ~ 0x10FFFF |
出厂固件分区,初始烧录的程序,默认从这里启动 |
| 6 | ota_0 | App 0x00 |
OTA 0 0x10 |
0x110000 |
1 MB | 0x110000 ~ 0x20FFFF |
OTA升级槽位A,第一次OTA升级时新固件写到这里 |
| 7 | ota_1 | App 0x00 |
OTA 1 0x11 |
0x210000 |
1 MB | 0x210000 ~ 0x30FFFF |
OTA升级槽位B,第二次OTA升级时新固件写到这里(交替使用) |
4MB Flash总地址:
0x000000 ~ 0x3FFFFF
已用空间:到ota_1结束是0x310000(约3.06MB),剩余约0xF0000(约960KB)未分配。
二、每个分区逐个详解
1. Bootloader(引导程序,32KB)
- 不是分区表里的分区,是Flash最开头的固定区域。
- 上电后,芯片内部ROM(一级boot)先运行,它把Flash
0x0的bootloader.bin读到SRAM,然后跳转执行。 - 二级bootloader做的事:
- 初始化SPI Flash、时钟
- 读取
0x8000的分区表 - 打印分区表到串口(就是你开机看到的那堆日志)
- 读取otadata,决定启动factory还是ota_0/ota_1
- 加载对应app分区的固件到内存,跳转运行
2. Partition Table(分区表,4KB)
- 存在
0x8000,是一张二进制地图。 - 每条分区记录固定32字节,记录:名字、类型、子类型、偏移、大小。
- bootloader靠这张表知道:factory在哪、ota_0在哪、nvs在哪。
- 你menuconfig选的
Factory app, two OTA definitions,编译时自动生成这张表烧进去。
3. nvs(非易失存储,16KB)
- 掉电不丢失的键值对存储。
- 存的内容:
- WiFi SSID、密码(你连接过的路由器)
- 设备配置参数
- 用户自定义数据(比如设备ID、运行计数)
- IDF提供
nvs_flashAPI读写,不用自己操作Flash。 - ⚠️ 擦除整个Flash时,nvs会被清掉,WiFi密码丢失,需要重新配网。
4. otadata(OTA状态区,8KB)⭐ OTA最关键
- 记录下次启动从哪个app分区启动。
- 内部结构:两个OTA状态条目(互为备份,防止掉电损坏),每个条目记录:
- 当前选中的OTA槽位(factory / ota_0 / ota_1)
- 该固件的版本号、SHA256校验
- OTA升级流程:
- 当前跑factory,新固件下载到ota_0
- 下载完成校验通过 → 修改otadata,标记下次启动ota_0
- 重启 → bootloader读otadata → 启动ota_0
- 下次升级 → 新固件写到ota_1 → 修改otadata → 启动ota_1
- 交替使用ota_0和ota_1,永远不会覆盖当前正在运行的固件
- ⚠️ 如果otadata损坏或为空,bootloader默认启动factory(你日志里的
Defaulting to factory image)。
5. phy_init(射频校准,4KB)
- 存WiFi/蓝牙射频校准数据。
- 第一次上电,WiFi驱动做射频校准(温度、电压补偿),结果存这里。
- 以后上电直接读取,跳过校准,加快WiFi启动速度。
- 你日志里的
Saving new calibration data due to checksum failure就是第一次上电或校准数据损坏,重新校准并保存。
6. factory(出厂固件,1MB)
- 初始烧录的应用程序,默认启动分区。
- 你的simple_ota_example.bin就烧在这里(
0x10000)。 - OTA升级不会修改factory,factory永远是最原始的出厂固件,作为"救命回退"。
- 如果ota_0/ota_1都损坏或启动失败,bootloader可以回退到factory。
7. ota_0 / ota_1(OTA升级槽,各1MB)
- 两个完全一样的app分区,交替使用。
- 升级时,新固件写到当前没在运行的那个槽。
- 你的app镜像约941KB,1MB槽位刚好放下(bin会有对齐填充,实际接近1MB)。
- 两个槽位的意义:
- 升级过程中掉电 → 当前运行的固件不受影响,重启后还是老固件
- 新固件有bug启动失败 → bootloader检测到启动失败,自动回退到老固件
三、Type和SubType字段含义
| Type值 | 含义 | 常见SubType |
|---|---|---|
0x00 |
Application(应用程序) | 0x00=factory, 0x10=ota_0, 0x11=ota_1, 0x12=ota_2… |
0x01 |
Data(数据) | 0x00=otadata, 0x01=phy, 0x02=nvs, 0x03=coredump, 0x81=fat, 0x82=spiffs |
bootloader根据Type判断这个分区能不能当app启动;只有Type=
0x00的分区才是可执行固件。
四、OTA升级时的地址变化(结合你的日志)
你日志里boot加载app的过程:
I (118) esp_image: segment 0: paddr=00010020 vaddr=3c0c0020 size=24340h (148288) map
paddr=00010020:物理Flash地址,0x10000是factory分区起点,+0x20是app镜像头(256字节appdesc之后的代码起始)。vaddr=3c0c0020:映射到CPU的虚拟地址(Flash XIP映射区)。- 这说明当前启动的是factory分区(
0x10000)。
如果OTA升级后启动ota_0,你会看到:
paddr=00110020 ← ota_0分区起点0x110000 + 0x20
五、一句话总结每个分区的角色
| 分区 | 角色比喻 |
|---|---|
| Bootloader | 电脑的BIOS/UEFI,负责开机自检和加载系统 |
| Partition Table | 硬盘的分区表(MBR/GPT),告诉系统每个盘在哪 |
| nvs | 电脑的CMOS/注册表,存配置参数 |
| otadata | 双系统启动菜单(GRUB),记录下次进哪个系统 |
| phy_init | 显卡/网卡的固件校准数据 |
| factory | 出厂预装的操作系统(C盘) |
| ota_0 | 第二个系统分区(D盘),升级时装新系统 |
| ota_1 | 第三个系统分区(E盘),下次升级用,交替轮换 |
两个核心问题:修改分区表大小、自定义修改 otadata
一、能不能调整分区表各个分区大小?
✅ 可以改,但有前提
你现在用的是IDF内置预定义分区表:
Factory app, two OTA definitions,内置的不能直接改大小。
想要调整分区大小,必须切换到 Custom partition table CSV(自定义csv)。
修改完整步骤
- 复制IDF内置的
factory_two_ota.csv,放到你的工程目录,命名为partitions.csv - menuconfig → Partition table type → 选择
Custom partition table CSV,填入文件名partitions.csv - 编辑csv,修改各个分区的
Size字段,例如:# Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x8000, # 原来16KB,改成32KB otadata, data, ota, 0xd000, 0x2000, phy_init, data, phy, 0xf000, 0x1000, factory, app, factory, 0x10000, 1.5M, # 原来1MB,改成1.5MB ota_0, app, ota_0, 0x190000,1.5M, ota_1, app, ota_1, 0x310000,1.5M,
⚠️重要规则:
- Offset偏移可以留空,IDF会自动计算地址,不要手动乱填偏移,容易地址重叠
- 分区之间不能重叠;总大小不能超过Flash容量(你的是4MB)
- 修改分区表后,必须完整烧录全部镜像:bootloader + partition‑table.bin + app。
- 旧Flash里的NVS、OTA数据全部丢失!改分区表等于重新划分Flash磁盘,原有数据全部清空。
常见修改场景
- 固件越来越大,超过1MB → 把factory/ota_0/ota_1改成1.5MB
- 需要存更多配置,把nvs从16KB改成32KB
- 不需要双OTA槽,可以删掉ota_1,把空间留给nvs或者spiffs
注意:otadata分区大小不建议改,固定8KB(0x2000)就足够,改大没有收益。
二、otadata分区:能不能自定义修改?
otadata:类型
data,ota,8KB,用来保存OTA启动状态。
✅ 可以读写修改,但是有官方API,不建议直接裸写Flash
otadata内部是什么
otadata里面存两套备份的OTA状态记录(双备份,防止掉电损坏)。
每条记录包含:
- 标记当前要启动哪个app分区(factory / ota_0 / ota_1)
- app固件的SHA256校验值
- 状态标记(有效/无效)
bootloader上电读取otadata,根据里面的标记决定启动哪个app。
如果otadata全部是0xFF(空白),bootloader就默认启动factory分区。
方式1:使用IDF官方提供的OTA API(推荐,安全)
#include "esp_ota_ops.h"
// 获取当前运行的app分区
const esp_partition_t *running = esp_ota_get_running_partition();
// 设置下一次启动的分区:比如强制下次启动ota_0
esp_partition_t *ota0 = esp_ota_get_next_update_partition(NULL);
esp_ota_set_boot_partition(ota0);
esp_ota_set_boot_partition()这个函数,内部就是自动修改otadata分区。
它会处理双备份、校验、写入Flash,避免掉电损坏otadata。
方式2:自己直接操作Flash(强烈不推荐)
otadata是Flash的一个数据分区,你可以调用esp_partition_write()直接往0xD000写数据。
⚠️巨大风险:
- otadata内部有固定二进制格式,你手写二进制很容易写错格式
- 如果写的中途掉电,otadata会损坏,bootloader会回退到factory,OTA功能直接失效
- 官方做了双备份容错,自己写会破坏这个容错机制。
👉 业务代码,永远调用
esp_ota_set_boot_partition(),不要直接裸写otadata。
特殊场景:手动强制切换OTA启动槽
比如你现在运行ota_0,想强制下次重启跑factory:
const esp_partition_t *factory_part = esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_FACTORY, NULL);
esp_ota_set_boot_partition(factory_part);
怎么读取otadata当前状态?
esp_ota_state_t state;
esp_ota_get_boot_state(&state);
可以读出当前标记的启动分区、校验信息。
三、关键坑点总结
- 修改分区表大小
- 必须切换到自定义csv,不能改内置分区表;
- 修改后需要完整烧录,Flash原有全部数据丢失;
- 尽量不手动填Offset,留空让IDF自动计算。
- otadata分区
- 可以修改,但不要裸写Flash,使用
esp_ota_set_boot_partition(); - otadata大小建议保持8KB,不需要调大;
- otadata损坏会导致bootloader回退factory。
补充:如何查看otadata当前状态
- 串口boot日志:
Defaulting to factory image代表otadata为空/无效 - 代码调用
esp_ota_get_boot_state()读取; - ESPConnect网页工具也能读取otadata分区原始二进制。
举个实际例子
如果你发现你的固件越来越大,超过1MB,就可以:
复制csv,把factory、ota_0、ota_1全部改为1.5M;
编译,完整烧录;
注意:旧的NVS全部清空,WiFi需要重连。
如果你需要,我可以给你一份修改后的完整partitions.csv模板。
factory分区到底怎么写进去?
factory分区:
0x10000,1MB,出厂固件分区。
不是自动写进去!是编译+烧录这两步完成的。
1、factory是怎么被烧录进去的?
① 编译阶段
你的simple_ota_example工程编译完成,输出两个文件:
build/bootloader/bootloader.bin→ 烧录到0x0(bootloader)build/partition_table/partition-table.bin→ 烧录到0x8000(分区表)build/simple_ota_example.bin→ 这个就是factory的固件!
👉 当你用的是
Factory app, two OTA definitions内置分区表:
编译出来的app bin,默认就是烧写到factory分区(偏移0x10000)。
② 烧录阶段(VSCode ESP‑IDF)
你点烧录(Flash),idf.py自动执行:
esptool.py write_flash
0x0 build/bootloader/bootloader.bin
0x8000 build/partition_table/partition-table.bin
0x10000 build/simple_ota_example.bin ← 这里!写到factory
✅ 所以:你平时点烧录,就自动把程序写到factory分区。
2、OTA升级会不会修改factory?
❗ 正常OTA永远不会改写factory分区!
OTA流程:
- 当前运行:factory(
0x10000) - 下载新固件,写入ota_0(0x110000)
- 修改otadata标记下次启动ota_0
- 重启,bootloader启动ota_0
factory一直保持最原始版本,作为安全备份。
就算ota_0/ota_1固件崩溃、坏掉,bootloader可以回退factory。
只有两种情况factory会被覆盖:
- 你再次执行完整烧录(idf.py flash),新的bin又写到
0x10000; - 你自己代码调用
esp_partition_write()手动往factory分区写数据。
正常OTA业务代码千万不要去写factory!
3、怎么把固件手动写到ota_0 / ota_1?
场景:我想把程序烧到ota_0,而不是factory
两种方式:
方式A:esptool命令行(手动指定地址)
esptool.py write_flash 0x110000 build/simple_ota_example.bin
直接把固件写到ota_0分区,factory保持不动。
方式B:代码OTA下载(你现在simple_ota_example做的事)
程序运行在factory,通过网络下载bin,调用esp_https_ota,内部自动写入ota_0。
4、关键问题:怎么知道现在跑的是factory还是ota_0?
①看串口boot日志(最直观)
I (296) boot: Loaded app from partition at offset 0x10000
0x10000→ 当前运行 factory0x110000→ 当前运行 ota_00x210000→ 当前运行 ota_1
②代码读取当前运行分区
#include "esp_ota_ops.h"
const esp_partition_t *running_part = esp_ota_get_running_partition();
printf("当前运行分区偏移:0x%X\n", running_part->offset);
printf("分区子类型:%d\n", running_part->subtype);
subtype == ESP_PARTITION_SUBTYPE_APP_FACTORY→ factorysubtype == ESP_PARTITION_SUBTYPE_APP_OTA_0→ ota_0
③ESPConnect网页工具
看分区的高亮,高亮的就是当前运行的app分区。
5、重要的工程逻辑:factory的角色
- factory = 出厂保底固件,一般是稳定版本;
- OTA升级只操作ota_0/ota_1,不会碰factory;
- 如果OTA升级后的固件启动失败,bootloader检测到多次启动失败,自动回退到factory;
- 只有你手动烧录,才会更新factory。
生产产品的时候:
出厂烧录:bootloader + partition‑table + factory(出厂固件)。
用户后续OTA,全部写到ota_0/ota_1,factory永远不动。
6、容易踩坑的误区
❌误区1:“OTA会自动更新factory”
不会,OTA只会写ota槽位。
❌误区2:“factory是自动生成的,不用管”
factory是你编译出来的app bin,烧录时指定地址
0x10000才写进去。
❌误区3:“我想把新固件放到factory,直接OTA就行”
不行。想要更新factory,必须重新完整烧录,不能OTA。
简单总结
- factory分区的固件:就是你编译出来的app.bin,烧录的时候写到
0x10000; - 你VSCode点烧录,就自动写factory;
- OTA不会修改factory;factory是保底;
- 想把固件写到ota_0,要么esptool手动写地址,要么通过OTA网络下载写入。
如果你需要,我可以给你一段示例代码,实现:程序运行在ota_0,手动回退到factory。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)