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做的事:
    1. 初始化SPI Flash、时钟
    2. 读取 0x8000 的分区表
    3. 打印分区表到串口(就是你开机看到的那堆日志)
    4. 读取otadata,决定启动factory还是ota_0/ota_1
    5. 加载对应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升级流程:
    1. 当前跑factory,新固件下载到ota_0
    2. 下载完成校验通过 → 修改otadata,标记下次启动ota_0
    3. 重启 → bootloader读otadata → 启动ota_0
    4. 下次升级 → 新固件写到ota_1 → 修改otadata → 启动ota_1
    5. 交替使用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)

修改完整步骤

  1. 复制IDF内置的factory_two_ota.csv,放到你的工程目录,命名为partitions.csv
  2. menuconfig → Partition table type → 选择 Custom partition table CSV,填入文件名partitions.csv
  3. 编辑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,
    

⚠️重要规则:

  1. Offset偏移可以留空,IDF会自动计算地址,不要手动乱填偏移,容易地址重叠
  2. 分区之间不能重叠;总大小不能超过Flash容量(你的是4MB)
  3. 修改分区表后,必须完整烧录全部镜像:bootloader + partition‑table.bin + app
  4. 旧Flash里的NVS、OTA数据全部丢失!改分区表等于重新划分Flash磁盘,原有数据全部清空

常见修改场景

  1. 固件越来越大,超过1MB → 把factory/ota_0/ota_1改成1.5MB
  2. 需要存更多配置,把nvs从16KB改成32KB
  3. 不需要双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写数据。
⚠️巨大风险:

  1. otadata内部有固定二进制格式,你手写二进制很容易写错格式
  2. 如果写的中途掉电,otadata会损坏,bootloader会回退到factory,OTA功能直接失效
  3. 官方做了双备份容错,自己写会破坏这个容错机制。

👉 业务代码,永远调用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);

可以读出当前标记的启动分区、校验信息。


三、关键坑点总结

  1. 修改分区表大小
  • 必须切换到自定义csv,不能改内置分区表;
  • 修改后需要完整烧录,Flash原有全部数据丢失
  • 尽量不手动填Offset,留空让IDF自动计算。
  1. otadata分区
  • 可以修改,但不要裸写Flash,使用esp_ota_set_boot_partition()
  • otadata大小建议保持8KB,不需要调大;
  • otadata损坏会导致bootloader回退factory。

补充:如何查看otadata当前状态

  1. 串口boot日志:Defaulting to factory image 代表otadata为空/无效
  2. 代码调用esp_ota_get_boot_state()读取;
  3. ESPConnect网页工具也能读取otadata分区原始二进制。

举个实际例子

如果你发现你的固件越来越大,超过1MB,就可以:
复制csv,把factory、ota_0、ota_1全部改为1.5M
编译,完整烧录;
注意:旧的NVS全部清空,WiFi需要重连。

如果你需要,我可以给你一份修改后的完整partitions.csv模板




factory分区到底怎么写进去?

factory分区:0x10000,1MB,出厂固件分区
不是自动写进去!是编译+烧录这两步完成的。

1、factory是怎么被烧录进去的?

① 编译阶段

你的simple_ota_example工程编译完成,输出两个文件:

  1. build/bootloader/bootloader.bin → 烧录到0x0(bootloader)
  2. build/partition_table/partition-table.bin → 烧录到0x8000(分区表)
  3. 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流程:

  1. 当前运行:factory(0x10000
  2. 下载新固件,写入ota_0(0x110000)
  3. 修改otadata标记下次启动ota_0
  4. 重启,bootloader启动ota_0

factory一直保持最原始版本,作为安全备份
就算ota_0/ota_1固件崩溃、坏掉,bootloader可以回退factory。

只有两种情况factory会被覆盖:

  1. 你再次执行完整烧录(idf.py flash),新的bin又写到0x10000
  2. 你自己代码调用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 → 当前运行 factory
  • 0x110000 → 当前运行 ota_0
  • 0x210000 → 当前运行 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 → factory
  • subtype == ESP_PARTITION_SUBTYPE_APP_OTA_0 → ota_0

③ESPConnect网页工具

看分区的高亮,高亮的就是当前运行的app分区。


5、重要的工程逻辑:factory的角色

  1. factory = 出厂保底固件,一般是稳定版本;
  2. OTA升级只操作ota_0/ota_1,不会碰factory;
  3. 如果OTA升级后的固件启动失败,bootloader检测到多次启动失败,自动回退到factory
  4. 只有你手动烧录,才会更新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。


简单总结

  1. factory分区的固件:就是你编译出来的app.bin,烧录的时候写到0x10000
  2. 你VSCode点烧录,就自动写factory;
  3. OTA不会修改factory;factory是保底;
  4. 想把固件写到ota_0,要么esptool手动写地址,要么通过OTA网络下载写入。

如果你需要,我可以给你一段示例代码,实现:程序运行在ota_0,手动回退到factory

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐