4. 启动设备检测与加载:优先级、Boot Header 与完整性校验
设备上电后,BootROM 要做的第一件事,说白了就是回答三个问题:从哪启动?启动什么?启动文件对不对? 这三个问题对应着今天要聊的核心内容——启动设备优先级检测、Boot Header 解析,以及镜像完整性校验。
我个人习惯把这一章叫做“BootROM 的十字路口”。为什么?因为一旦走错,设备要么变砖,要么启动到一半卡死。我在项目中遇到过好几次,就是因为启动优先级没配好,导致量产时一批设备死活起不来。嗯,咱们今天就把这个路口彻底搞清楚。
4.1 启动设备优先级检测
BootROM 怎么知道该从哪个设备加载镜像?它不会猜,而是靠一套固定的优先级列表去轮询。你想想看,如果同时接了 SPI Flash 和 eMMC,总得有个先后顺序吧?
优先级检测的典型流程是这样的:
- 读取硬件管脚状态:有些芯片通过 BOOT_MODE[2:0] 管脚的电平组合来决定启动顺序。
- 遍历启动设备列表:按照芯片厂商预设的优先级,依次尝试从每个设备读取 Boot Header。
- 超时与重试:如果某个设备无响应,BootROM 会等待一段时间(比如 100ms),然后跳到下一个设备。
- 进入下载模式:如果所有设备都失败,最后会进入 USB/UART 下载模式,等待主机下发镜像。
关键点:优先级不是死的。很多芯片允许通过 OTP(一次性可编程)或 eFuse 来固化启动顺序,量产时一定要确认这个配置。
我曾经踩过一个坑:某款芯片默认先从 NAND Flash 启动,但我们的设计只接了 SPI NOR。结果第一批样机全部卡在 NAND 检测上,等了 5 秒超时才切到 SPI。后来我建议在 OTP 里把 SPI 设为第一优先级,启动时间直接从 5 秒降到了 200 毫秒。
4.2 Boot Header 解析
BootROM 找到启动设备后,第一件事就是读取Boot Header。这个 Header 通常放在存储设备的固定偏移位置(比如偏移 0x0 或 0x200),大小一般是 256 字节或 512 字节。
一个典型的 Boot Header 长这样:
| 偏移 | 字段 | 大小 | 说明 |
|---|---|---|---|
| 0x00 | Magic Number | 4 字节 | 固定值,如 0xAA55AA55,用于快速识别 |
| 0x04 | Image Type | 1 字节 | 0x01=BootLoader, 0x02=Kernel, 0x03=DTB |
| 0x05 | Load Address | 4 字节 | 镜像在 RAM 中的加载地址 |
| 0x09 | Image Size | 4 字节 | 镜像总大小(含 Header) |
| 0x0D | Checksum | 4 字节 | Header 自身的校验和 |
| 0x11 | Hash Type | 1 字节 | 0x00=无, 0x01=CRC32, 0x02=SHA256 |
| 0x12 | Hash Value | 32 字节 | 镜像数据的哈希值 |
| 0x32 | Reserved | 206 字节 | 保留字段,可放版本号、时间戳等 |
BootROM 解析 Header 时,会先检查 Magic Number。如果不对,直接跳过这个设备。我见过有人把 Magic Number 写反了(大小端问题),结果 BootROM 死活不认,折腾了两天才找到原因。
小技巧:在 Reserved 字段里放一个版本号,方便后期排查问题。比如 0x32 处放 4 字节的镜像版本号,调试时一眼就能看出 Flash 里烧的是哪个版本。
4.3 镜像完整性校验
Header 解析通过后,BootROM 会把镜像从存储设备读到 RAM 里,然后做完整性校验。这一步不能省——你想想看,如果 Flash 里有个 bit 翻转了,启动到一半崩溃,那可就麻烦了。
常见的校验方式有两种:
- CRC32:速度快,适合资源受限的 BootROM。但 CRC 是线性校验,抗恶意篡改能力弱。
- SHA256:安全性高,适合需要防篡改的场景。但计算量大,BootROM 里实现起来要小心优化。
我个人的建议是:OTA 场景下必须用 SHA256。为什么?因为 OTA 升级包可能通过网络传输,CRC32 太容易被碰撞了。我在一个物联网项目里就吃过这个亏——攻击者伪造了一个 CRC32 相同的坏镜像,导致设备升级后变砖。后来全部换成 SHA256,再没出过问题。
下面是一个简化的 BootROM 校验流程:
// 伪代码:BootROM 镜像加载与校验
uint32_t boot_device_check() {
for (int i = 0; i < MAX_DEVICES; i++) {
device_t *dev = &boot_device_list[i];
if (dev->probe() != OK) continue; // 设备不存在或不可用
header_t hdr;
if (dev->read(0, &hdr, sizeof(hdr)) != OK) continue;
if (hdr.magic != EXPECTED_MAGIC) continue;
// 校验 Header 自身的 Checksum
if (crc32(&hdr, sizeof(hdr)) != 0) {
// Header 损坏,尝试下一个设备
continue;
}
// 读取完整镜像到 RAM
uint8_t *buf = (uint8_t*)hdr.load_addr;
if (dev->read(0, buf, hdr.image_size) != OK) continue;
// 根据 Hash Type 做完整性校验
if (hdr.hash_type == HASH_CRC32) {
if (crc32(buf, hdr.image_size) != hdr.hash_value) {
continue; // 校验失败
}
} else if (hdr.hash_type == HASH_SHA256) {
uint8_t calc_hash[32];
sha256(buf, hdr.image_size, calc_hash);
if (memcmp(calc_hash, hdr.hash_value, 32) != 0) {
continue; // 校验失败
}
}
// 全部通过,跳转到镜像入口
jump_to(hdr.load_addr + sizeof(hdr));
return OK;
}
// 所有设备都失败,进入下载模式
enter_download_mode();
return FAIL;
}
注意:BootROM 的代码通常放在内部 ROM 里,不能修改。所以校验算法必须是芯片厂商预置的。选型时一定要确认 BootROM 支持你需要的哈希算法,否则后期只能靠 BootLoader 二次校验,增加了复杂度。
4.4 整体流程与避坑指南
把上面三个步骤串起来,就是 BootROM 启动设备检测的完整流程。我画了一张图,方便你理解:
最后,分享几个我这些年积累的避坑经验:
- 启动设备优先级别写死:尽量用 OTP 或 eFuse 配置,方便后期调整。我见过一个项目,因为优先级写死在代码里,换 Flash 型号后不得不重新流片。
- Boot Header 的 Reserved 字段别浪费:放个版本号、时间戳、甚至产品 ID,调试时能省很多时间。
- 校验失败后要有降级策略:比如尝试从备份分区启动,或者进入恢复模式。别一失败就变砖,那用户体验太差了。
- 注意大小端:Magic Number 和 Checksum 的大小端一定要和 BootROM 匹配。我帮人排查过一个案例,就是大小端反了,导致 Header 解析永远失败。
嗯,这一章的内容就到这里。启动设备检测和加载是 BootROM 最核心的功能之一,搞懂了它,你就掌握了嵌入式系统启动的第一道关卡。