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 放在最前面,因为它最先执行。
从这张图你能看到: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 布局。这一步省了,后面全是坑。