4. 启动设备探测与加载:让BootROM找到正确的启动介质

大家好,我是你们的嵌入式系统讲师。今天我们来聊聊BootROM启动流程中一个非常关键的环节——启动设备探测与加载。

说实话,很多工程师在写应用层代码时,很少会关心BootROM是怎么找到启动设备的。但一旦你开始做底层启动、做bring-up,你就会发现——BootROM的启动设备探测算法,是整个系统能否正常启动的第一步。这一步错了,后面全是白搭。

4.1 启动设备探测算法:BootROM如何识别存储介质?

BootROM在芯片上电后,首先要做的事情就是:找到哪个设备里存了可启动的镜像。它怎么找?靠的就是设备探测算法。

不同的存储介质,探测方式完全不同。我挑两个最常见的来讲:SPI Flash 和 eMMC。

4.1.1 SPI Flash探测:读ID号

SPI Flash的探测,说白了就是发一个命令,让Flash芯片报上名来。这个命令叫 RDID(Read ID),命令码是 0x9F。

BootROM会这样做:

  1. 拉低片选(CS),选中SPI设备
  2. 发送 0x9F 命令
  3. 等待Flash返回3个字节:Manufacturer ID + Device ID
  4. 拉高片选,结束通信

拿到ID后,BootROM会查一张内部表。比如:

Manufacturer ID Device ID 对应型号 容量
0xEF 0x4018 W25Q128JV 16MB
0xC8 0x4015 GD25Q64B 8MB
0x1C 0x3816 EN25QH128 16MB
个人经验: 我在项目中遇到过一款国产Flash,它的Manufacturer ID和某大厂完全一样,但Device ID不同。结果BootROM不认识它,直接启动失败。后来我们只能改BootROM的ID表,或者换Flash型号。所以选型时,一定要确认BootROM是否支持你的Flash。

4.1.2 eMMC探测:CMD0 + 初始化序列

eMMC的探测比SPI复杂得多。它走的是MMC协议,BootROM需要发送一系列命令来完成初始化。

核心流程是这样的:

  1. 发送 CMD0(GO_IDLE_STATE),让eMMC进入空闲状态
  2. 发送 CMD1(SEND_OP_COND),获取OCR寄存器,判断工作电压
  3. 发送 CMD2(ALL_SEND_CID),获取设备CID(唯一标识)
  4. 发送 CMD3(SET_RELATIVE_ADDR),给设备分配一个相对地址(RCA)
  5. 发送 CMD9(SEND_CSD),获取CSD寄存器,了解设备容量、块大小等

嗯,这里要注意:CMD0是必须成功的。如果CMD0没响应,BootROM就会认为这个eMMC不存在,直接跳过。

避坑指南: 我曾经遇到一个案子,eMMC的焊盘虚焊,导致CMD0偶尔能通、偶尔不通。结果BootROM在探测时,有时能识别到eMMC,有时不能。系统启动变得极其不稳定。排查了整整两天,最后发现是焊接问题。所以,硬件上一定要保证eMMC的焊接质量。

4.2 启动头(Boot Header)解析:镜像的身份证

BootROM找到了存储设备,接下来要读数据了。但它不能随便读——它需要先找到启动头(Boot Header)。

启动头是什么?说白了,就是一段固定格式的数据,放在镜像的最开头。它告诉BootROM:

  • 这个镜像是什么格式的
  • 它有多大
  • 它应该被加载到哪个地址
  • 它的校验值是多少

一个典型的Boot Header结构如下:

typedef struct {
    uint32_t magic;          // 魔数,比如 0xAA55AA55
    uint32_t image_size;     // 镜像总大小(字节)
    uint32_t load_addr;      // 加载地址
    uint32_t entry_point;    // 入口地址
    uint32_t checksum;       // 校验和
    uint32_t version;        // 版本号
    uint8_t  reserved[16];   // 保留字段
} boot_header_t;

BootROM读到这个结构后,会做几件事:

  1. 验证魔数:如果魔数不对,说明这不是一个合法的启动镜像,直接跳过
  2. 检查版本:有些BootROM只支持特定版本的Header格式
  3. 记录加载地址:后面要把镜像拷贝到内存的这个位置
  4. 记录入口地址:跳转时要跳到这里
关键点: 魔数(Magic Number)是BootROM判断镜像是否合法的第一道关卡。我见过有人把魔数写反了(大小端问题),结果BootROM死活不认。所以写镜像生成工具时,一定要注意字节序。

4.3 启动镜像完整性校验:CRC / Checksum

BootROM把镜像读到内存后,不能直接跳过去执行。它得先确认:这个镜像有没有被损坏?

怎么确认?靠校验。常用的校验方式有两种:

4.3.1 简单校验和(Checksum)

就是把镜像的所有字节加起来,取低32位。实现简单,但碰撞概率高。适合对安全性要求不高的场景。

uint32_t calculate_checksum(uint8_t *data, uint32_t len) {
    uint32_t sum = 0;
    for (uint32_t i = 0; i < len; i++) {
        sum += data[i];
    }
    return sum;
}

4.3.2 CRC校验

CRC(循环冗余校验)比Checksum可靠得多。BootROM里常用的是CRC32。它用多项式除法来计算,能检测出大部分常见的错误。

uint32_t calculate_crc32(uint8_t *data, uint32_t len) {
    // 这里省略标准CRC32实现,实际项目中用查表法效率更高
    // 我建议直接用硬件CRC模块,速度比软件快10倍以上
    return crc32_hardware(data, len);
}
我的建议: 如果你的芯片有硬件CRC模块,一定要用。我在一个项目中,软件CRC32校验一个16MB的镜像,花了将近2秒。换成硬件CRC后,只要几十毫秒。启动速度提升非常明显。

4.4 加载地址与跳转逻辑:最后一步

校验通过后,BootROM要做最后两件事:加载 和 跳转。

4.4.1 加载地址

BootROM从Boot Header里读到了 load_addr,然后它会把整个镜像从存储设备拷贝到内存的这个地址。

这里有个坑:加载地址必须是内存的有效地址。如果加载地址指向了未初始化的DDR区域,或者指向了只读区域,那系统就会崩溃。

我曾经: 遇到过一个情况,DDR初始化代码在BootROM里跑得没问题,但加载地址设在了DDR的某个保留区域(被其他外设占用了)。结果镜像拷贝过去后,数据被外设覆盖了,启动到一半就挂了。后来我们调整了加载地址,问题解决。

4.4.2 跳转逻辑

拷贝完成后,BootROM会跳转到 entry_point 地址执行。跳转前,它通常会做几件事:

  • 关闭中断(防止跳转过程中被打断)
  • 设置栈指针(SP)
  • 清空一些状态寄存器
  • 最后,用一条跳转指令(比如 BX 或 BL)跳过去
// 伪代码:BootROM跳转逻辑
void boot_jump(uint32_t entry_point) {
    __disable_irq();          // 关中断
    __set_MSP(entry_point);   // 设置主栈指针(有些架构需要)
    // 清空一些关键寄存器
    // ...
    // 跳转
    ((void (*)(void))entry_point)();
}

跳转之后,BootROM的使命就完成了。控制权交给了下一级启动程序(比如U-Boot SPL或裸机程序)。

4.5 本章知识体系总览

为了让你更直观地理解整个流程,我画了一张流程图:

BootROM启动设备探测与加载流程 芯片上电复位 探测启动设备(SPI ID / eMMC CMD0) 读取Boot Header(魔数/大小/地址) 完整性校验(CRC / Checksum) 加载到内存 → 跳转到入口地址 SPI Flash: 发0x9F读ID eMMC: 发CMD0初始化 魔数验证、版本检查 CRC32或简单Checksum 设置SP、关中断、跳转

这张图把整个流程串起来了。你想想看,从芯片上电到最终跳转到用户程序,BootROM做了多少事?每一步都不能出错。

好了,这一章的内容就到这里。启动设备探测与加载,是BootROM最核心的功能之一。理解了它,你就掌握了底层启动的钥匙。

可可精品资料库