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的典型布局,以及各分区之间的关系:

ESP32 Flash分区布局与启动流程 ESP32 Flash分区布局与启动流程 Flash 地址空间 (0x0000 ~ 0x3FFFFF, 4MB) 0x1000 bootloader (固定地址,不在分区表内) 0x8000 分区表 (partition_table.bin, 4KB) nvs (16KB) otadata (8KB) app0 (1.75MB) app1 (1.75MB) spiffs / littlefs 文件系统 (剩余空间) 启动流程: ① 上电 ② 加载bootloader ③ 读取otadata ④ 选择app分区 ⑤ 启动固件 OTA升级时,新固件写入非当前启动的app分区,然后翻转otadata标记

3.7 分区表调试——烧录后怎么确认分区生效了?

烧录完新分区表,别急着跑业务代码。先确认分区表有没有正确写入。我常用的方法:

  1. 用esptool读取分区表:esptool.py --port COMx read_flash 0x8000 0x1000 partition_dump.bin,然后用十六进制编辑器打开,看看分区信息对不对。
  2. 用idf.py打印分区信息:在代码里调用 esp_partition_dump() 或 esp_partition_find_first() 遍历所有分区,打印出来。
  3. 检查启动日志: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分钟做一次完整检查——对齐、容量、类型、名字,一个都不能少。