分区表结构解析:从头部到条目的完整拆解

分区表这东西,说白了就是一张「地图」。固件升级能不能成功、回滚能不能安全执行,全看这张地图画得对不对。我刚开始做嵌入式时,觉得分区表不就是几个地址偏移嘛,后来踩了坑才明白——每个字节都藏着设计者的心思。

分区表头部格式

每个分区表都以一个固定格式的头部开始。这个头部就像一本书的扉页,告诉你这本书的版本、大小、校验信息。我习惯把头部看作整个分区表的「身份证」。

偏移 大小 字段 说明
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 展示了分区表的完整结构:

分区表结构总览 分区表结构总览 分区表头部 (12 字节) 魔数 0xAA50 | 子版本 | 偏移 | 大小 | 条目数 | CRC32 分区条目 1 (32 字节) 魔数 0xAA55 | 类型 | 子类型 | 偏移 | 大小 | 标志 | 保留 分区条目 2 (32 字节) 魔数 0xAA55 | 类型 | 子类型 | 偏移 | 大小 | 标志 | 保留 ... 分区条目 N (32 字节) 魔数 0xAA55 | 类型 | 子类型 | 偏移 | 大小 | 标志 | 保留 结束标记 (类型 0xFF) 每个条目 32 字节,条目数由头部 Count 字段指定,最多支持 128 个分区

分区表校验与安全

分区表头部最后 4 字节是 CRC32 校验。这个校验覆盖整个分区表(头部 + 所有条目)。每次系统启动时,Bootloader 都会重新计算 CRC32,与存储的值比对。如果不一致,说明分区表被篡改或损坏。

我的做法:除了 CRC32,我还会在分区表末尾再存一份备份。如果主分区表校验失败,自动切换到备份。这个双保险机制,在我职业生涯中至少挽救了三次因为 Flash 老化导致的启动失败。

分区表与固件升级的关系

固件升级时,分区表是第一个被读取的数据结构。升级工具需要:

  1. 读取分区表头部,确认魔数和版本
  2. 遍历所有条目,找到目标分区(比如 OTA 固件分区)
  3. 检查目标分区的标志位,确认是否允许写入
  4. 如果启用了回滚保护(Bit 4),比较新固件版本与当前版本
  5. 一切通过后,将新固件写入目标分区的偏移地址

嗯,这里要注意:如果分区表本身在升级过程中损坏,整个系统就失去了「地图」。所以升级时,我通常先把新分区表写入备份区域,确认无误后再覆盖主分区表。这个顺序不能乱。

血的教训:我曾经在升级时先擦除了主分区表,然后才写新数据。结果中途断电,主分区表没了,备份还是旧的——系统彻底不知道 Flash 里有什么。从那以后,我坚持「先写备份,再更新主表」的原则。