Bootloader 基础与分区概念

做嵌入式开发这么多年,我见过不少工程师一上来就撸 Bootloader 代码,结果连分区表都没搞明白。说实话,分区是 Bootloader 的基石——你连数据放哪儿都不知道,怎么优化启动速度?

这一章,咱们先把地基打牢。我会结合自己踩过的坑,把 Bootloader 的角色、分区表的作用、常见分区类型一次讲透。

Bootloader 在嵌入式系统中的角色

Bootloader 是什么?说白了,它就是系统上电后第一个跑的程序。你想想看,CPU 复位后从 Flash 的 0x00000000 地址开始执行,那地方放的就是 Bootloader。

它的核心任务就三个:

  • 硬件初始化:关看门狗、设置时钟、初始化 DDR/Flash
  • 加载系统:从 Flash 或存储介质把内核镜像搬到内存
  • 跳转执行:把控制权交给操作系统

我记得刚入行那会儿,在一款 ARM9 芯片上做项目。Bootloader 里忘了初始化 DDR 的时序参数,结果内核加载到一半就崩了。查了两天才发现是内存没跑稳——从那以后,我每次写 Bootloader 都会先确认硬件初始化顺序。

关键认知:Bootloader 不是操作系统的一部分,它是硬件和系统之间的桥梁。没有它,你的 Linux/RTOS 连启动的机会都没有。

分区表的作用

分区表,你可以把它理解成一张地图。它告诉 Bootloader:

  • Boot 镜像在哪个地址?
  • Recovery 分区有多大?
  • System 分区从哪儿开始?

没有分区表,Bootloader 就像瞎子摸象。我见过一个团队,他们把所有镜像都写死在代码里。后来换了颗 Flash 芯片,地址偏移变了,整个项目重写分区逻辑——这就是典型的「偷懒一时爽,重构火葬场」。

分区表通常放在 Flash 的固定偏移位置,比如偏移 1KB 或 4KB 处。Bootloader 启动后先读分区表,再根据表里的信息去加载对应分区。

我的习惯:分区表结构一定要加版本号和校验和。曾经有个项目,OTA 升级时分区表被写坏了,Bootloader 读到一个全 0xFF 的表,直接死循环。加个 CRC 校验,至少能兜底。

常见分区类型

嵌入式设备的分区类型,其实就那么几种。我整理了一张表,方便你对照:

分区名称 用途 典型大小 是否可读写
Boot 存放内核镜像和 DTB 16MB ~ 64MB 只读(OTA 时写入)
Recovery 备用系统,用于恢复或 OTA 失败回滚 16MB ~ 64MB 只读
System 根文件系统(/system) 256MB ~ 2GB 只读(Android 中可挂载为 rw)
Data 用户数据和应用数据 剩余空间 读写
Cache 系统缓存、OTA 包暂存 64MB ~ 256MB 读写
Misc 存储启动控制信息(如 bootloader_message) 1MB ~ 4MB 读写

每个分区都有自己的脾气。我挑几个重点说说:

Boot 分区

这是 Bootloader 最关心的分区。它里面装着 Linux 内核(zImage/Image)和设备树(DTB)。Bootloader 从分区表里找到 Boot 分区的起始地址,把内核读到内存,然后跳过去。

优化启动速度时,Boot 分区的大小和位置很关键。我建议把它放在 Flash 的前面区域,因为很多 Flash 芯片的前几个块读取速度更快。

Recovery 分区

Recovery 是一个迷你系统。当主系统挂了,用户可以通过按键组合进入 Recovery 模式,刷机或恢复出厂设置。

我曾经踩过一个坑:Recovery 分区和 Boot 分区共用同一个内核镜像。结果 OTA 升级时 Boot 分区写坏了,Recovery 也启动不了——因为内核文件坏了。从那以后,我坚持 Recovery 分区要有独立的内核和 ramdisk。

Misc 分区

这个分区很小,但作用很大。它存的是 Bootloader 和系统之间的通信数据,比如:

  • 「下次启动进入 Recovery 模式」
  • 「OTA 升级状态:成功/失败」
  • 「清除用户数据」

嗯,这里要注意:Misc 分区是 Bootloader 和系统共享的,读写时要加同步机制。我见过一个 Bug:系统写 Misc 分区时突然断电,Bootloader 读到一半的数据,结果误判成「进入 Recovery」,用户一脸懵逼。

Cache 分区

Cache 分区主要给 OTA 升级用。系统把下载好的升级包先放 Cache,然后通知 Bootloader 去读取并刷写。

为什么不用 Data 分区?因为 Data 分区可能被加密,Bootloader 在早期阶段解不了密。Cache 分区通常不加密,Bootloader 可以直接读。

分区布局示意图

下面这张图展示了一个典型的嵌入式设备 Flash 分区布局。我画的时候特意把 Bootloader 放在最前面,因为它最先执行。

典型嵌入式 Flash 分区布局 Bootloader (512KB ~ 2MB) 分区表 (4KB ~ 64KB) Boot (16~64MB) Recovery (16~64MB) System (256MB~2GB) Cache (64~256MB) Misc (1~4MB) Data (剩余空间) Flash 地址从低到高

从这张图你能看到:Bootloader 和分区表在最前面,然后是 Boot、Recovery、System 等分区。Data 分区放在最后,因为它大小可变,方便利用剩余空间。

分区大小怎么定?

这个问题没有标准答案,但我可以给你一些参考:

  • Boot 分区:内核 + DTB 的大小,再加 20% 余量。比如内核 8MB,DTB 2MB,那就分 12MB 以上。
  • Recovery 分区:和 Boot 分区一样大,或者略大一点。因为 Recovery 可能包含额外的 ramdisk。
  • System 分区:根据文件系统大小来。Android 设备通常 1GB 起步,IoT 设备 256MB 就够了。
  • Cache 分区:至少能放下一个完整的 OTA 包。如果 OTA 包 500MB,Cache 就分 600MB。
  • Misc 分区:1MB 绰绰有余。别分太大,浪费空间。

避坑指南:我曾经在一个项目里把 Cache 分区分得太小,结果 OTA 包下载到一半就报空间不足。最后只能改分区表,重新刷机——所有用户都得返厂。所以,分区大小一定要留够余量。

分区表的结构示例

下面是一个简单的分区表结构,用 C 语言定义:

// 分区表项
typedef struct {
    char     name[16];      // 分区名称,如 "boot"
    uint32_t start_addr;    // 起始地址(相对 Flash 基址)
    uint32_t size;          // 分区大小(字节)
    uint32_t flags;         // 属性标志(只读/可升级/加密等)
    uint32_t crc32;         // 分区表项校验
} partition_entry_t;

// 分区表头
typedef struct {
    uint32_t magic;         // 魔数,如 0x50415254 ("PART")
    uint32_t version;       // 版本号
    uint32_t entry_count;   // 表项数量
    uint32_t crc32;         // 整个分区表的校验
    partition_entry_t entries[0]; // 变长数组
} partition_table_t;

这个结构我在好几个项目里用过。魔数用来快速判断分区表是否有效,CRC 用来检测数据完整性。你想想看,如果分区表被破坏了,Bootloader 连该从哪儿启动都不知道——那设备就成砖了。

小结

这一章咱们把 Bootloader 的基础角色、分区表的作用、常见分区类型都过了一遍。说白了,分区就是嵌入式系统的骨架——骨架歪了,后面的优化全是白搭。

我个人习惯,在开始写 Bootloader 之前,先把分区表画出来,和硬件团队对齐 Flash 布局。这一步省了,后面全是坑。