4、启动参数的传递机制:参数存储位置与解析顺序
各位同学,今天我们来聊聊启动参数是怎么从“藏身之处”跑到 BootROM 手里的。这个问题,说白了就是回答三个“W”:参数存在哪(Where)、谁先谁后读(Which order)、BootROM 怎么拿(How)。
我在做第一颗自研 SoC 时,就因为在参数传递顺序上踩了坑,导致芯片在低温下启动失败。从那以后,我对这套机制就格外上心。你想想看,BootROM 是芯片上电后第一个执行的代码,它连内存都还没初始化,怎么拿到启动参数?
4.1 参数的三个“老巢”
启动参数不会凭空出现。它们被固化在三个地方,每个地方都有各自的脾气。
| 存储位置 | 存储介质 | 特点 | 典型用途 |
|---|---|---|---|
| 寄存器 | 芯片内部寄存器 | 易失、速度快、容量小 | 复位向量、启动模式选择 |
| 固定内存地址 | ROM / SRAM | 非易失(ROM)、可映射 | 启动配置表、设备树片段 |
| OTP | 一次性可编程存储 | 非易失、不可改写、容量有限 | 芯片唯一 ID、安全密钥、启动优先级 |
关键点:BootROM 在访问这些位置时,不能依赖外部 DDR。它只能使用芯片内部的 SRAM 或寄存器。所以,参数存储位置必须能在上电瞬间被直接寻址。
4.2 参数解析顺序:谁先说话?
BootROM 不是一股脑把所有参数都读进来。它有严格的优先级。我个人习惯把这个顺序叫做“硬-软-默”三级策略。
- 第一级:硬件强制(寄存器/硬件引脚)
芯片复位后,某些寄存器会被硬件自动加载。比如复位向量地址、启动介质选择(SD卡/NAND/SPI)。这些值来自芯片的硬件配置引脚或复位默认值。BootROM 第一条指令就会读它们。 - 第二级:OTP 配置
如果硬件寄存器没有明确指定(或者指定为“由 OTP 决定”),BootROM 就会去读 OTP 区域。OTP 里存的是出厂时烧录的“硬规矩”,比如安全启动开关、调试接口是否禁用。 - 第三级:固定内存地址的默认参数
如果前两级都没给出有效值,BootROM 会 fallback 到芯片内部 ROM 里预置的默认参数表。这个表通常放在一个固定的物理地址上,比如 0x0000_0100。
我曾经遇到过一个坑:某款芯片的 OTP 区域在出厂时被误烧录了一个无效的启动介质编号。结果 BootROM 读 OTP 后直接跳到了错误的分支,连 fallback 都没执行。后来我们加了一个“OTP 有效性校验”步骤,如果 OTP 值超出合法范围,就跳过它直接走默认参数。
4.3 BootROM 如何读取参数?
读取过程其实不复杂,但细节决定成败。BootROM 的代码通常用汇编或极简 C 写成,因为它不能依赖任何复杂的运行时环境。
大致流程如下:
- 步骤 1:读取硬件寄存器
直接通过地址访问。比如ldr r0, =0xE000_ED00,然后读这个寄存器的特定位。 - 步骤 2:解析 OTP
OTP 控制器通常有专门的接口寄存器。BootROM 需要先初始化 OTP 控制器(发送时钟、解除锁定),然后按地址读取。OTP 读取往往有延迟,需要轮询状态位。 - 步骤 3:校验参数合法性
每个参数都有预期的取值范围或魔数(Magic Number)。BootROM 会检查读到的值是否合法。比如启动介质编号只能是 0~3,如果读到 0xFF,就视为无效。 - 步骤 4:组合参数表
把从不同来源读到的参数合并成一张“启动参数表”,放在内部 SRAM 的固定位置。后续的启动阶段(如 SPL、U-Boot)就可以直接访问这张表。
// 伪代码示例:BootROM 参数读取逻辑
void bootrom_param_init(void) {
// 1. 读硬件寄存器
uint32_t hw_cfg = *(volatile uint32_t*)0xE000_ED00;
boot_params.boot_mode = (hw_cfg >> 4) & 0x03;
// 2. 读 OTP(如果硬件允许)
if (boot_params.boot_mode == 0x03) { // OTP 决定模式
otp_init();
boot_params.boot_mode = otp_read(OTP_BOOT_MODE_ADDR);
}
// 3. 合法性校验
if (boot_params.boot_mode > 3) {
boot_params.boot_mode = 0; // fallback 到默认
}
// 4. 写入 SRAM 参数表
param_table_store(&boot_params);
}
小技巧:我在项目中习惯在 OTP 里存一个 CRC 校验值。BootROM 读取 OTP 参数后先算 CRC,对不上就直接走默认参数。这样可以避免 OTP 位翻转导致的“玄学”启动失败。
4.4 参数传递的“最后一公里”
BootROM 把参数整理好之后,怎么交给下一级?这里有两种主流做法:
- 寄存器传参:BootROM 在跳转到下一级代码前,把参数表地址放在某个通用寄存器里(比如 r0 或 x0)。下一级代码直接读寄存器就能拿到参数指针。
- 固定地址传参:BootROM 把参数表放在一个约定好的 SRAM 地址上。下一级代码直接去那个地址读。这种方式更简单,但需要两段代码事先约定好地址。
我个人更推荐寄存器传参。为什么?因为固定地址容易冲突。我记得有一次,BootROM 和 SPL 用了不同的链接脚本,结果参数表地址对不上,SPL 读到的全是垃圾数据。改成寄存器传参后,问题就再没出现过。
4.5 整体流程一览
下面这张图把整个参数传递过程串起来了。你可以看到,从硬件复位到参数表就绪,每一步都有明确的输入和输出。
嗯,到这里你应该明白了。启动参数的传递不是简单的“读一下就行”,而是一个有优先级、有校验、有 fallback 的严谨过程。下一级代码拿到的参数表,是 BootROM 经过层层筛选后的“最终答案”。
我个人建议你在设计自己的启动流程时,也按照这个思路来:硬件决定上限,OTP 决定底线,默认参数兜底。这样既灵活又可靠。