3. Flash分区表详解:分区表结构、自定义与地址对齐
分区表这东西,说白了就是一张地图。ESP32的Flash里住着bootloader、app固件、配置数据、证书文件……它们各自住在哪、占多大地方,全靠分区表来规划。
我刚接触ESP32时,觉得默认分区表够用就行。直到有一次OTA升级总失败,查了三天才发现是app分区太小,新固件写不进去。嗯,从那以后我再也不敢轻视分区表了。
3.1 分区表长什么样?
ESP32的分区表是一个CSV文件,放在工程目录的 partitions.csv 里。编译时,这个CSV会被转换成二进制格式,烧录到Flash的0x8000偏移地址。
每一行代表一个分区,格式固定:
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x4000,
otadata, data, ota, 0xd000, 0x2000,
app0, app, ota_0, 0x10000, 0x1C0000,
app1, app, ota_1, 0x1D0000, 0x1C0000,
spiffs, data, spiffs, 0x390000, 0x40000,
我来拆解一下每个字段:
| 字段 | 说明 | 我踩过的坑 |
|---|---|---|
| Name | 分区名字,随便起,但别重复 | 名字别超过16字节,否则编译报错 |
| Type | app 或 data,系统靠这个区分固件和配置 | 自定义Type要用0x40~0xFE,别乱用 |
| SubType | 细分类型,比如ota_0、nvs、spiffs | OTA分区必须用ota_0和ota_1 |
| Offset | 分区起始地址,十六进制 | 必须4KB对齐,否则烧录失败 |
| Size | 分区大小,十六进制 | 别把Flash算超了,留点余量 |
| Flags | 可选,encrypted表示加密分区 | 加密分区要配合Flash加密功能 |
3.2 默认分区里都有谁?
ESP32出厂默认分区表包含这几个关键角色:
- bootloader:它不在分区表里,固定在0x1000地址。负责加载app分区里的固件。
- nvs:Non-Volatile Storage,存WiFi密码、校准数据。我遇到过nvs被写爆导致WiFi连不上的情况,后来把nvs分区从4KB扩到8KB就解决了。
- otadata:OTA状态标记,记录当前该从哪个app分区启动。只有2个扇区大小(8KB),千万别动它。
- app0 / app1:两个固件分区,OTA升级就是在这两个分区之间来回切换。
- spiffs / littlefs:文件系统分区,存网页、证书、日志等。
核心概念:OTA升级的本质,就是往“非当前启动”的app分区里写新固件,写完后把otadata里的启动标记翻转,下次重启就从新分区启动了。
3.3 自定义分区表——什么时候需要自己动手?
默认分区表给app0和app1各分了1.75MB。但有些场景下,这个分配并不合理:
- 固件太大:你的固件超过1.5MB,默认分区装不下。
- 文件系统太小:需要存大量网页资源或日志,默认的spiffs只有512KB。
- 不需要OTA:产品只通过串口烧录,不需要远程升级,可以把两个app分区合并成一个。
- 需要多个数据分区:比如单独划一个分区存证书,一个分区存用户配置。
我自己做过一个项目,固件只有800KB,但需要存2MB的字体文件和语音提示。我把app0和app1各缩到1MB,省出来的2MB全给了spiffs。效果很好,OTA照常跑,文件系统也够用。
自定义分区表示例
# 单app分区,无OTA,适合量产烧录
nvs, data, nvs, 0x9000, 0x4000,
otadata, data, ota, 0xd000, 0x2000,
app, app, factory, 0x10000, 0x1E0000,
spiffs, data, spiffs, 0x1F0000, 0x200000,
小技巧:如果你不需要OTA,把app的SubType设为factory,系统会直接从factory分区启动,不再检查otadata。这样省掉了otadata的读写开销,启动速度能快几十毫秒。
3.4 分区大小与地址对齐——为什么必须4KB对齐?
ESP32的Flash擦除操作以扇区为单位,一个扇区是4KB(0x1000)。分区起始地址和大小必须是4KB的整数倍,否则:
- 烧录工具会报错,直接拒绝写入。
- 即使强行烧进去,读写时可能擦掉相邻分区的数据。
我曾经犯过一个低级错误:把nvs分区偏移写成0x9000,大小写成0x4100(多写了256字节)。烧录时没报错,但运行一段时间后WiFi突然连不上。查了三天,发现nvs写越界,把otadata给覆盖了。从那以后,我每次改分区表都会用Python算一遍对齐:
# 检查分区是否4KB对齐
def check_alignment(offset, size):
if offset % 0x1000 != 0:
print(f"偏移 {hex(offset)} 未对齐!")
if size % 0x1000 != 0:
print(f"大小 {hex(size)} 未对齐!")
地址对齐的硬性规则
| 项目 | 对齐要求 | 说明 |
|---|---|---|
| 分区起始偏移 | 4KB (0x1000) | 必须能被0x1000整除 |
| 分区大小 | 4KB (0x1000) | 不够就向上取整到4KB倍数 |
| app分区 | 建议64KB对齐 | OTA升级时写性能更好 |
| 文件系统分区 | 4KB对齐即可 | spiffs/littlefs内部会再管理 |
警告:分区表本身占用0x8000地址,大小固定为0x1000(4KB)。你自定义分区时,第一个分区的偏移不能小于0x9000,否则会覆盖分区表数据。
3.5 分区表与Flash容量——别把Flash算爆了
ESP32常见的Flash容量有2MB、4MB、8MB、16MB。分区表里所有分区的偏移+大小不能超过Flash总容量。我见过有人把4MB的Flash分区表配到4.5MB,编译能过,但烧录时直接报错。
一个简单的检查方法:看最后一个分区的结束地址。比如上面那个单app的例子:
spiffs 偏移 0x1F0000,大小 0x200000
结束地址 = 0x1F0000 + 0x200000 = 0x3F0000
Flash总大小 = 4MB = 0x400000
剩余空间 = 0x400000 - 0x3F0000 = 0x10000 (64KB)
留64KB余量,够用。我一般会留至少128KB的空闲空间,给以后扩展用。
3.6 分区表结构总览
下面这张图展示了ESP32 Flash的典型布局,以及各分区之间的关系:
3.7 分区表调试——烧录后怎么确认分区生效了?
烧录完新分区表,别急着跑业务代码。先确认分区表有没有正确写入。我常用的方法:
- 用esptool读取分区表:
esptool.py --port COMx read_flash 0x8000 0x1000 partition_dump.bin,然后用十六进制编辑器打开,看看分区信息对不对。 - 用idf.py打印分区信息:在代码里调用
esp_partition_dump()或esp_partition_find_first()遍历所有分区,打印出来。 - 检查启动日志:ESP32启动时会在串口输出分区表信息,看到类似
I (30) esp_image: segment 0: ...就说明分区加载正常。
我的习惯:每次改完分区表,都会在代码里加一段自检逻辑——遍历所有分区,检查偏移和大小是否4KB对齐,以及总大小是否超过Flash容量。如果检查失败,直接进入错误处理,不启动业务。这个习惯帮我避免了好几次线上事故。
3.8 分区表常见问题与避坑
- 分区表烧录后设备变砖?——八成是偏移写错了,覆盖了bootloader。用esptool重新烧录bootloader和分区表就能救回来。
- OTA升级后启动失败?——检查app分区大小是否足够容纳新固件。我曾经把app分区设成1MB,固件长到1.1MB,OTA永远失败。
- nvs数据丢失?——nvs分区太小,或者被其他分区越界覆盖。用
esp_partition_erase_range()时一定要确认范围。 - 文件系统挂载失败?——spiffs/littlefs分区偏移或大小不对。用
make_flash_fs.py工具重新格式化一下。
分区表看似简单,但它是整个固件系统的地基。地基没打好,上面盖的房子再漂亮也白搭。我建议你每次改分区表时,都花5分钟做一次完整检查——对齐、容量、类型、名字,一个都不能少。