分区表结构解析:从头部到条目的完整拆解
分区表这东西,说白了就是一张「地图」。固件升级能不能成功、回滚能不能安全执行,全看这张地图画得对不对。我刚开始做嵌入式时,觉得分区表不就是几个地址偏移嘛,后来踩了坑才明白——每个字节都藏着设计者的心思。
分区表头部格式
每个分区表都以一个固定格式的头部开始。这个头部就像一本书的扉页,告诉你这本书的版本、大小、校验信息。我习惯把头部看作整个分区表的「身份证」。
| 偏移 | 大小 | 字段 | 说明 |
|---|---|---|---|
| 0x00 | 2 字节 | 魔数 (Magic) | 固定为 0xAA50,用于识别分区表 |
| 0x02 | 2 字节 | 子版本 (SubType) | 标识分区表格式版本 |
| 0x04 | 4 字节 | 偏移 (Offset) | 分区条目数组的起始偏移 |
| 0x08 | 2 字节 | 大小 (Size) | 分区条目数组的总字节数 |
| 0x0A | 2 字节 | 条目数 (Count) | 分区条目数量 |
| 0x0C | 4 字节 | CRC32 | 整个分区表的校验和 |
关键点:魔数 0xAA50 是硬编码的。如果读到的魔数不对,说明分区表已损坏或根本不是分区表。我在项目中就遇到过 Flash 某位被写坏,魔数变成 0xAA51,整个系统直接拒绝启动——这其实是好事,总比用错误的分区表强。
分区条目定义
头部之后就是分区条目数组。每个条目固定 32 字节,描述一个分区的全部信息。你可以把它想象成一张「房产证」——写明了这块地(Flash 区域)归谁用、用来干什么、边界在哪里。
typedef struct {
uint16_t magic; // 魔数,固定 0xAA55
uint8_t type; // 分区类型
uint8_t subtype; // 子类型
uint32_t offset; // 分区起始偏移(相对 Flash 基址)
uint32_t size; // 分区大小(字节)
uint8_t flags; // 标志位
uint8_t reserved[15]; // 保留字节
} partition_entry_t;
我的习惯:保留字节不要浪费。我会用其中 2 个字节存版本号,1 个字节存升级计数。这样即使分区表头部损坏,也能从条目本身恢复部分信息。
分区属性详解
类型 (Type) 与子类型 (SubType)
类型决定了分区的大类,子类型则进一步细化。举个例子:类型 0x01 表示「应用分区」,子类型 0x00 可能是工厂固件,0x01 是 OTA 升级固件。为什么这么分?因为升级时要区分哪个是当前运行版本,哪个是待升级版本。
| 类型值 | 含义 | 常见子类型 |
|---|---|---|
| 0x00 | 系统分区 | 0x00: Bootloader, 0x01: 分区表备份 |
| 0x01 | 应用分区 | 0x00: 工厂固件, 0x01: OTA 固件 A, 0x02: OTA 固件 B |
| 0x02 | 数据分区 | 0x00: 配置, 0x01: 日志, 0x02: 文件系统 |
| 0xFF | 结束标记 | 无 |
避坑指南:我曾经把两个应用分区的子类型设成一样的,结果升级时固件写到了错误的位置,系统变砖。后来我强制要求:同一类型下,子类型必须唯一。这个规则救了我好几次。
偏移 (Offset) 与大小 (Size)
偏移是分区在 Flash 中的起始地址,大小是分区占用的字节数。这两个字段必须满足:
- 偏移必须是 Flash 擦除块大小的整数倍(通常是 4KB 对齐)
- 大小必须是 Flash 写入单元大小的整数倍(通常是 256 字节对齐)
- 分区之间不能重叠,也不能留空洞
你想想看,如果偏移不对齐,擦除时可能会误伤相邻分区。我见过一个案例,就是因为偏移没对齐,升级时把 Bootloader 给擦了一半——那场面,只能用「灾难」来形容。
标志 (Flags)
标志位是分区属性的「开关集合」。每个 bit 代表一个独立属性:
| 位 | 名称 | 含义 |
|---|---|---|
| Bit 0 | 可读 | 1 表示分区可读,0 表示只写 |
| Bit 1 | 可写 | 1 表示分区可写,0 表示只读 |
| Bit 2 | 可执行 | 1 表示分区包含可执行代码 |
| Bit 3 | 加密 | 1 表示分区内容已加密 |
| Bit 4 | 回滚保护 | 1 表示该分区禁止回滚到旧版本 |
| Bit 5-7 | 保留 | 留作未来扩展 |
重点:Bit 4 回滚保护标志是本章的核心。当这个位被置 1 时,固件升级流程必须检查:如果新固件版本低于当前版本,直接拒绝写入。这个机制能防止意外降级导致的安全漏洞。
分区表结构总览
说了这么多,不如画张图来得直观。下面这张 SVG 展示了分区表的完整结构:
分区表校验与安全
分区表头部最后 4 字节是 CRC32 校验。这个校验覆盖整个分区表(头部 + 所有条目)。每次系统启动时,Bootloader 都会重新计算 CRC32,与存储的值比对。如果不一致,说明分区表被篡改或损坏。
我的做法:除了 CRC32,我还会在分区表末尾再存一份备份。如果主分区表校验失败,自动切换到备份。这个双保险机制,在我职业生涯中至少挽救了三次因为 Flash 老化导致的启动失败。
分区表与固件升级的关系
固件升级时,分区表是第一个被读取的数据结构。升级工具需要:
- 读取分区表头部,确认魔数和版本
- 遍历所有条目,找到目标分区(比如 OTA 固件分区)
- 检查目标分区的标志位,确认是否允许写入
- 如果启用了回滚保护(Bit 4),比较新固件版本与当前版本
- 一切通过后,将新固件写入目标分区的偏移地址
嗯,这里要注意:如果分区表本身在升级过程中损坏,整个系统就失去了「地图」。所以升级时,我通常先把新分区表写入备份区域,确认无误后再覆盖主分区表。这个顺序不能乱。
血的教训:我曾经在升级时先擦除了主分区表,然后才写新数据。结果中途断电,主分区表没了,备份还是旧的——系统彻底不知道 Flash 里有什么。从那以后,我坚持「先写备份,再更新主表」的原则。