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 份分区表。一份在主区域,一份在备份区域。每次修改分区表时,先写备份,再写主表。如果写主表时断电,下次启动还能从备份恢复。

下面这张图展示了分区表校验与备份的典型流程:

分区表校验与备份流程 主分区表 备份分区表 写入时先写备份 启动时 CRC32 校验 校验失败 校验成功 从备份分区表恢复 正常启动系统 恢复后重新校验

在实际项目中,我还会加一个“分区表版本号”。每次更新分区表时,版本号加 1。这样,如果备份比主表新,说明主表写入失败,自动用备份覆盖主表。这个小技巧,帮我避免了好几次因断电导致的变砖事故。

总结一下分区表的选择建议:

场景 推荐分区表类型 原因
小容量 Flash(< 4MB) 自定义硬编码 节省空间,简单可靠
大容量 eMMC/NAND(> 4GB) GPT 支持多分区,有备份,灵活
兼容老旧系统 MBR 兼容性好,工具支持多
需要动态调整分区 GPT + 配置文件 分区表可在线更新