4. 启动设备检测与加载:优先级、Boot Header 与完整性校验

设备上电后,BootROM 要做的第一件事,说白了就是回答三个问题:从哪启动?启动什么?启动文件对不对? 这三个问题对应着今天要聊的核心内容——启动设备优先级检测、Boot Header 解析,以及镜像完整性校验。

我个人习惯把这一章叫做“BootROM 的十字路口”。为什么?因为一旦走错,设备要么变砖,要么启动到一半卡死。我在项目中遇到过好几次,就是因为启动优先级没配好,导致量产时一批设备死活起不来。嗯,咱们今天就把这个路口彻底搞清楚。

4.1 启动设备优先级检测

BootROM 怎么知道该从哪个设备加载镜像?它不会猜,而是靠一套固定的优先级列表去轮询。你想想看,如果同时接了 SPI Flash 和 eMMC,总得有个先后顺序吧?

优先级检测的典型流程是这样的:

  1. 读取硬件管脚状态:有些芯片通过 BOOT_MODE[2:0] 管脚的电平组合来决定启动顺序。
  2. 遍历启动设备列表:按照芯片厂商预设的优先级,依次尝试从每个设备读取 Boot Header。
  3. 超时与重试:如果某个设备无响应,BootROM 会等待一段时间(比如 100ms),然后跳到下一个设备。
  4. 进入下载模式:如果所有设备都失败,最后会进入 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 启动设备检测的完整流程。我画了一张图,方便你理解:

BootROM 启动设备检测与加载流程 设备上电 / 复位 遍历启动设备优先级列表 读取 Boot Header 校验 Magic Number & Checksum 通过? 下一个设备 加载镜像到 RAM CRC / SHA 校验 跳转到入口地址 全部失败 → 下载模式 注意 OTA 场景建议使用 SHA256 避免 CRC32 碰撞风险

最后,分享几个我这些年积累的避坑经验:

  • 启动设备优先级别写死:尽量用 OTP 或 eFuse 配置,方便后期调整。我见过一个项目,因为优先级写死在代码里,换 Flash 型号后不得不重新流片。
  • Boot Header 的 Reserved 字段别浪费:放个版本号、时间戳、甚至产品 ID,调试时能省很多时间。
  • 校验失败后要有降级策略:比如尝试从备份分区启动,或者进入恢复模式。别一失败就变砖,那用户体验太差了。
  • 注意大小端:Magic Number 和 Checksum 的大小端一定要和 BootROM 匹配。我帮人排查过一个案例,就是大小端反了,导致 Header 解析永远失败。

嗯,这一章的内容就到这里。启动设备检测和加载是 BootROM 最核心的功能之一,搞懂了它,你就掌握了嵌入式系统启动的第一道关卡。