3. 分区表核心概念
分区表这东西,说白了就是一张“地图”。它告诉固件和操作系统:你的存储芯片上,哪块区域归谁管,从哪里开始,到哪里结束。
我刚开始做嵌入式时,觉得分区表不就是几个偏移地址嘛,随便填填就行。直到有一次,我把一个产品的分区表写错了,导致 OTA 升级后设备变砖,整批货都得返厂重刷。嗯,从那以后,我对分区表的态度就变成了:敬畏。
这一章,咱们就把 MBR、GPT 和嵌入式里常见的自定义分区表掰开揉碎。你想想看,搞懂了这些,以后烧录固件、做 OTA 升级,心里就有底了。
3.1 MBR 分区表:老当益壮,但局限明显
MBR(Master Boot Record)诞生于上世纪 80 年代,是 PC 世界的标准。嵌入式设备里,很多老芯片或小容量 Flash 还在用它。
MBR 的结构很简单:
- 引导代码(446 字节):存放启动程序。嵌入式里通常不用,直接填 0。
- 分区表(64 字节):4 个分区表项,每个 16 字节。
- 结束标志(2 字节):固定为
0x55AA。
每个分区表项包含:
- 引导标志(1 字节):0x80 表示可引导,0x00 表示不可引导。
- 起始 CHS 地址(3 字节):老式寻址方式,现在基本用 LBA。
- 分区类型(1 字节):比如 0x83 是 Linux 文件系统,0x0C 是 FAT32。
- 结束 CHS 地址(3 字节)。
- 起始 LBA 地址(4 字节):分区在磁盘上的起始扇区号。
- 分区大小(4 字节):占用的扇区数。
MBR 的硬伤:
- 最大支持 2TB 磁盘(因为 LBA 地址用 32 位表示)。
- 最多 4 个主分区。想分更多?得用扩展分区和逻辑分区,麻烦。
- 没有备份机制。MBR 坏了,整个盘的数据就全丢了。
我记得有一次调试一个老款工控机,Flash 只有 256MB,用的就是 MBR。当时客户要求分 3 个区:boot、rootfs、data。MBR 刚好够用。但如果你要分 5 个区,那就得用 GPT 了。
3.2 GPT 分区表:现代标准,嵌入式也适用
GPT(GUID Partition Table)是 UEFI 时代的产物。它解决了 MBR 的几乎所有痛点。
GPT 的结构:
- 保护 MBR(LBA 0):一个假的 MBR,防止老工具误认为磁盘未分区。
- GPT 头(LBA 1):包含分区表的位置、大小、校验和等信息。
- 分区表项(LBA 2 开始):每个分区表项 128 字节,默认支持 128 个分区。
- 备份 GPT 头(磁盘末尾):主 GPT 坏了,用备份恢复。
- 备份分区表项(备份 GPT 头之前):分区表项的完整副本。
每个分区表项包含:
- 分区类型 GUID(16 字节):比如
C12A7328-F81F-11D2-BA4B-00A0C93EC93B是 EFI 系统分区。 - 唯一分区 GUID(16 字节):全球唯一,用于标识这个分区。
- 起始 LBA(8 字节):64 位,支持超大磁盘。
- 结束 LBA(8 字节)。
- 属性标志(8 字节):比如只读、隐藏等。
- 分区名称(72 字节):UTF-16LE 编码,可以起个名字。
嵌入式里用 GPT 的好处:
- 分区数量不受限,适合复杂的多分区方案(比如 bootloader、kernel、dtb、rootfs、recovery、data、cache...)。
- 有备份,安全性高。我曾经遇到过 Flash 坏块导致主 GPT 损坏,靠备份 GPT 救回了一台设备。
- 支持大于 2TB 的存储,虽然嵌入式里少见,但未来可期。
不过,GPT 也有缺点:占用空间大。一个 GPT 头加 128 个分区表项,差不多要 16KB。对于只有 4MB Flash 的 MCU 来说,太奢侈了。
3.3 嵌入式设备中的自定义分区表
在嵌入式世界里,很多时候我们不用 MBR 也不用 GPT。为什么?因为太复杂,太浪费。
嵌入式设备的分区表,往往是写在代码里的一个结构体,或者放在 Flash 的固定偏移处。我称之为“硬编码分区表”。
常见的自定义分区表形式:
- 结构体数组:在 C 代码里定义一个分区表数组,编译时确定。
- 配置文件:比如
partition.xml或partition.cfg,由烧录工具解析。 - 头部信息:放在固件镜像的开头,包含分区信息。
举个例子,一个典型的嵌入式分区表结构体:
typedef struct {
const char *name; // 分区名称
uint32_t offset; // 起始偏移(字节)
uint32_t size; // 分区大小(字节)
uint32_t flags; // 标志位(只读、可升级等)
} partition_t;
// 分区表定义
const partition_t g_partition_table[] = {
{"bootloader", 0x00000000, 0x00040000, 0}, // 256KB
{"env", 0x00040000, 0x00020000, PART_READONLY}, // 128KB
{"kernel", 0x00060000, 0x00100000, 0}, // 1MB
{"dtb", 0x00160000, 0x00010000, PART_READONLY}, // 64KB
{"rootfs", 0x00170000, 0x00500000, 0}, // 5MB
{"data", 0x00670000, 0x00190000, 0}, // 1.6MB
};
这种方式的优点是:简单、高效、不占额外存储空间。缺点是:改分区表要重新编译固件,不够灵活。
避坑指南:
我曾经在一个项目里,把分区表硬编码在 bootloader 中。后来产品要增加一个 recovery 分区,只能升级 bootloader。结果升级过程中断电,bootloader 写坏了,整机变砖。从那以后,我建议把分区表放在 Flash 的独立区域,或者用 GPT 的备份机制。
3.4 分区表校验与备份
分区表一旦损坏,设备就找不到分区了。轻则无法启动,重则数据全丢。所以,校验和备份是必须的。
校验方法:
- CRC32:对分区表数据计算 CRC32,存储在分区表头或末尾。每次读取分区表时校验。
- 校验和(Checksum):简单的累加和,适合资源受限的 MCU。
- 双重校验:主分区表和备份分区表互相校验。
备份策略:
- GPT 自带备份:磁盘末尾有备份 GPT,主 GPT 损坏时自动使用备份。
- 自定义备份:在 Flash 的另一个区域(比如最后几个扇区)存储分区表的副本。
- 冗余存储:分区表存 3 份,启动时多数表决。
我个人的习惯是:在嵌入式设备中,至少保留 2 份分区表。一份在主区域,一份在备份区域。每次修改分区表时,先写备份,再写主表。如果写主表时断电,下次启动还能从备份恢复。
下面这张图展示了分区表校验与备份的典型流程:
在实际项目中,我还会加一个“分区表版本号”。每次更新分区表时,版本号加 1。这样,如果备份比主表新,说明主表写入失败,自动用备份覆盖主表。这个小技巧,帮我避免了好几次因断电导致的变砖事故。
总结一下分区表的选择建议:
| 场景 | 推荐分区表类型 | 原因 |
|---|---|---|
| 小容量 Flash(< 4MB) | 自定义硬编码 | 节省空间,简单可靠 |
| 大容量 eMMC/NAND(> 4GB) | GPT | 支持多分区,有备份,灵活 |
| 兼容老旧系统 | MBR | 兼容性好,工具支持多 |
| 需要动态调整分区 | GPT + 配置文件 | 分区表可在线更新 |