4、BootROM与一级Bootloader:芯片上电流程、BootROM角色、一级Bootloader加载与校验
芯片上电后,第一行代码从哪里来?
这个问题,我当年刚入行时也琢磨了很久。你想想看,这时候内存还没初始化,Flash也读不了,CPU怎么知道自己该干什么?
答案就在芯片内部的一块只读存储器里——BootROM。
4.1 芯片上电后的“第一口气”
芯片上电复位后,CPU会从一个固定的物理地址取指执行。这个地址,通常就是BootROM的映射基址。
我习惯把上电流程拆成三个阶段:
- 硬件复位阶段:电源稳定、时钟起振、复位信号释放。CPU内部寄存器被设成默认值,PC指针指向BootROM入口。
- BootROM执行阶段:运行芯片出厂固化的只读代码。完成最基础的硬件初始化,然后寻找并加载一级Bootloader。
- 一级Bootloader阶段:从BootROM手中接过控制权,继续初始化DDR、加载完整固件。
这里有个关键点:BootROM是只读的,出厂后就不能改了。所以它的代码必须极其精简、极其稳定。我在项目中遇到过一颗芯片,BootROM里有个小bug导致某些批次的NAND Flash读时序不对,那真是叫天天不应——因为BootROM没法打补丁。
核心认知:BootROM是芯片上电后第一个执行的软件实体,它定义了整个信任链的起点。如果BootROM被攻破,后面的一切安全措施都是白搭。
4.2 BootROM到底干了什么?
说白了,BootROM就是一个微型启动管理器。它做的事情可以归纳为以下几点:
- 基础硬件初始化:关闭看门狗、设置系统时钟、初始化堆栈指针。有些芯片还会在这里配置管脚复用。
- 启动介质检测:按照预设的优先级顺序,轮询各个启动设备——比如先从SPI NOR Flash读,不行再试NAND,再不行就试SD卡。
- 一级Bootloader加载:从检测到的启动介质中,把一级Bootloader(通常叫SPL或BL1)读到芯片内部的SRAM里。
- 完整性校验:对加载进来的一级Bootloader做签名验证或哈希校验。这一步是可信启动的基石。
- 跳转执行:校验通过后,清理自身占用的资源,跳转到一级Bootloader的入口地址。
嗯,这里要注意:BootROM能用的内存非常有限。大多数芯片内部SRAM只有几十KB到几百KB。所以一级Bootloader必须做得足够小,才能塞进这片“弹丸之地”。
个人经验:我建议你在选型阶段就确认好芯片内部SRAM的大小。曾经有个项目,一级Bootloader编译出来128KB,芯片SRAM只有96KB——最后只能砍功能、改优化选项,折腾了两周才塞进去。
4.3 一级Bootloader的加载流程
BootROM加载一级Bootloader的过程,其实很像一个微型bootloader在做自己的本职工作。我画了一张图,帮你理清这个流程:
流程看起来不复杂,但每个环节都有坑。我挑几个重点说说。
4.4 启动介质检测顺序
BootROM怎么知道该从哪个设备启动?
通常有两种方式:
| 方式 | 原理 | 典型场景 |
|---|---|---|
| 管脚采样 | 芯片上电时采样特定GPIO的电平组合,决定启动介质 | 大多数SoC,如i.MX、STM32MP1 |
| 固定优先级轮询 | BootROM按固定顺序尝试每个设备,直到读到有效数据 | 部分MCU,如某些TI芯片 |
我个人更倾向于管脚采样的方式。为什么?因为固定优先级轮询有个隐患:如果某个设备损坏了,BootROM可能会卡在它上面,导致启动超时。我在调试一块开发板时就遇到过——SD卡槽里插着一张坏卡,BootROM反复尝试读取,等了十几秒才fallback到SPI Flash。
避坑指南:我曾经因为启动管脚的上拉电阻没焊,导致芯片死活不从NAND启动。查了两天才发现是管脚电平不对。所以硬件设计时,启动配置管脚一定要留测试点,方便调试。
4.5 一级Bootloader的签名校验
可信启动的核心就在这里。
BootROM从Flash里读出一级Bootloader的二进制数据后,不会直接跳转。它会先做一件事——校验。
常见的校验方式有两种:
- 哈希校验:BootROM里硬编码了一个哈希值,把加载进来的数据算一遍哈希,比对是否一致。这种方式简单,但哈希值不能更新。
- 签名验证:BootROM里存了公钥,用公钥验证一级Bootloader附带的数字签名。这种方式灵活,公钥可以更换(如果芯片支持OTP写入)。
我参与过的一个车规级项目,用的就是签名验证。BootROM里烧录了根公钥的哈希,一级Bootloader的头部包含签名和证书链。BootROM先验证证书链有效性,再用根公钥验签。通过后才跳转。
关键点:签名校验必须在BootROM内部完成,不能依赖外部代码。因为这时候还没有任何可信代码在运行,BootROM就是唯一的信任锚点。
4.6 一级Bootloader的布局结构
一级Bootloader在Flash里不是随便放的。它有固定的格式要求,BootROM才能正确解析。
我常用的布局是这样的:
+----------------------------+
| Header (64字节) |
| - 魔数 (4字节) |
| - 版本号 (4字节) |
| - 镜像长度 (4字节) |
| - 加载地址 (4字节) |
| - 入口地址 (4字节) |
| - 签名算法ID (4字节) |
| - 保留 (40字节) |
+----------------------------+
| 签名数据 (256/512字节) |
+----------------------------+
| 一级Bootloader镜像体 |
| (压缩或未压缩) |
+----------------------------+
魔数用来让BootROM确认这是一级Bootloader的有效镜像。我见过有人把魔数写错,结果BootROM怎么都认不出来,板子一直起不来。
小技巧:魔数建议用ASCII可读字符,比如“BL01”。调试时用十六进制查看器一眼就能认出来,省心。
4.7 从BootROM到一级Bootloader的交接
校验通过后,BootROM会做最后的清理工作:
- 关闭自身用过的外设时钟
- 设置栈指针到一级Bootloader指定的位置
- 跳转到入口地址
这里有个细节:BootROM跳转前,通常会通过寄存器传递一些信息给一级Bootloader。比如启动介质类型、时钟频率、DDR初始化参数等。我习惯在芯片手册里查清楚这些寄存器的定义,否则一级Bootloader拿不到正确的参数,后面DDR初始化就会出问题。
嗯,交接完成后,BootROM就功成身退了。从此,控制权完全交给了一级Bootloader。而一级Bootloader要做的第一件事,就是初始化DDR,为加载更大的二级Bootloader或操作系统内核做准备。
这部分内容,我们会在后面的章节里详细展开。