4、BootROM与SPL:芯片内部BootROM执行流程、SPL加载与初始化

好,我们进入第四章。这一章聊的是芯片上电后,最早期的那些事儿。

很多人觉得启动流程就是“上电 -> 跑Linux”,其实没那么简单。芯片刚上电时,内存都还没初始化,代码跑在哪儿?谁把引导程序搬进来的?

答案就是今天的主角:BootROMSPL

4.1 BootROM:芯片出厂就焊死的“第一行代码”

BootROM,全称 Boot Read-Only Memory。它是芯片内部的一块只读存储器,出厂时就已经固化好了代码。你改不了,也删不掉。

它的任务很纯粹: 把下一级引导程序(通常是SPL)从存储介质里搬进SRAM,然后跳过去执行。

我个人习惯把BootROM比作“芯片的助产士”。它不负责养孩子,只负责把孩子安全接生出来。

4.1.1 BootROM的执行流程

芯片上电后,CPU从复位向量地址开始执行。这个地址通常指向BootROM的入口。流程大致如下:

  1. 硬件初始化:关闭中断、设置栈指针、配置基础时钟。这一步很轻量,只够让CPU跑起来。
  2. 启动介质检测:BootROM会按照预设的优先级,去检测各个启动介质。比如先看eMMC有没有镜像,再看SD卡,最后看UART。
  3. 读取头部信息:找到介质后,读取前几个扇区。这里面有镜像的签名、大小、加载地址、校验值。
  4. 校验与加载:做CRC或哈希校验。通过后,把SPL镜像拷贝到芯片内部的SRAM里。
  5. 跳转执行:修改PC指针,跳到SRAM中的SPL入口。

关键点: BootROM运行期间,外部DDR还没初始化。所以它只能用芯片内部的SRAM。SRAM通常很小,几十KB到几百KB不等。这就是为什么SPL必须做得非常精简。

4.1.2 我在项目中遇到的坑

我曾经在一个车规级项目里,遇到一个诡异的问题:芯片偶尔启动不起来。查了很久,最后发现是BootROM检测eMMC时,时序没对上。

嗯,这里要注意:BootROM的检测时序是固定的,但不同批次的eMMC,上电准备时间有差异。如果eMMC还没准备好,BootROM就认为它不存在,直接跳到下一个介质去了。

解决方案?要么换eMMC型号,要么在硬件上拉长eMMC的复位时间。说白了,就是让BootROM等一等。

4.2 SPL:第二级引导程序

SPL,全称 Secondary Program Loader。它是BootROM加载进来的“小个子”,但责任重大。

SPL的主要任务就两个:初始化DDR加载U-Boot

4.2.1 SPL的加载过程

BootROM把SPL搬进SRAM后,SPL开始执行。它的代码量很小,通常只有几十KB。你想想看,这么点空间,能干多少事?

  • 时钟与PLL配置:把CPU和总线频率提上去。BootROM用的是慢速时钟,SPL要切换到高速模式。
  • DDR控制器初始化:这是SPL最核心的工作。配置DDR的时序参数、刷新率、驱动强度。这一步错了,后面全白搭。
  • 存储驱动初始化:初始化eMMC、NAND Flash或SD卡控制器,准备读取U-Boot镜像。
  • 加载U-Boot:从存储介质中读取U-Boot镜像,放到DDR的指定地址。
  • 跳转:跳到U-Boot入口,把控制权交出去。

我的建议: 调试SPL时,多用串口打印。在关键节点加几个puts(),能帮你快速定位问题。我习惯在DDR初始化前后各打一条信息,这样一眼就能看出DDR有没有挂掉。

4.2.2 SPL的代码结构

以U-Boot SPL为例,它的代码结构大致如下:

// 入口函数,由BootROM跳转过来
void board_init_f(ulong boot_flags) {
    // 1. 设置栈指针
    // 2. 初始化时钟
    // 3. 初始化串口(用于调试输出)
    // 4. 初始化DDR
    // 5. 返回DDR的起始地址和大小
}

// 第二阶段初始化
void board_init_r(gd_t *new_gd, ulong dest_addr) {
    // 1. 初始化存储设备驱动
    // 2. 从存储介质读取U-Boot镜像
    // 3. 校验镜像完整性
    // 4. 跳转到U-Boot
    jump_to_image_no_args(&spl_image);
}

你看,代码逻辑很清晰。board_init_f做早期初始化,board_init_r做后期加载。中间用DDR初始化作为分界线。

4.3 BootROM与SPL的分工协作

为什么要把启动分成两段?直接让BootROM加载U-Boot不行吗?

原因很简单:BootROM太小了。芯片内部的ROM空间有限,放不下完整的U-Boot驱动。而且BootROM不能改,万一U-Boot升级了,BootROM跟不上。

SPL的存在,相当于加了一层“适配层”。它可以根据不同的硬件平台,灵活配置DDR参数和存储驱动。BootROM只管搬砖,SPL负责干活。

特性 BootROM SPL
存储位置 芯片内部ROM 外部存储介质
可修改性 不可修改 可更新
运行环境 SRAM SRAM(初始化DDR后可用DDR)
主要任务 检测介质、加载SPL 初始化DDR、加载U-Boot
代码大小 几KB到几十KB 几十KB到几百KB

4.4 性能调优:让启动再快一点

座舱系统对启动时间有硬性要求。很多车厂要求冷启动到显示Logo不超过2秒。BootROM和SPL阶段,是优化的重点。

4.4.1 减少BootROM检测时间

BootROM会轮询多个启动介质。如果某个介质不存在,它要等超时才会跳过。这个超时时间,有的芯片默认设置得很长。

我曾经遇到过,BootROM检测USB设备时,等了3秒钟才超时。3秒啊,整个启动预算才2秒!

优化方法: 通过eFuse或OTP,把不需要的启动介质关掉。比如只用eMMC启动,就把SD卡、UART、USB的检测都禁用掉。

4.4.2 优化SPL的DDR初始化

DDR初始化是SPL里最耗时的操作。因为DDR控制器需要做校准,比如读写训练、ZQ校准。这些步骤不能省,但可以优化。

  • 使用预置参数:如果硬件设计固定,可以把DDR参数写死,跳过自动训练。能省几十毫秒。
  • 减少训练次数:有些芯片支持“快速训练”模式,精度稍低但速度快。量产阶段可以试试。
  • 并行初始化:如果有多片DDR,看看控制器是否支持并行初始化。能省一半时间。

注意: DDR参数写死有风险。如果换了DDR颗粒批次,或者PCB走线有差异,可能会不稳定。量产前一定要做高低温测试。

4.4.3 压缩SPL镜像

SPL是从存储介质读到SRAM的。读得越快,启动越快。如果SPL镜像太大,读的时间就长。

我建议把SPL做压缩。BootROM先读一个很小的解压程序,然后解压出完整的SPL。虽然多了解压时间,但读取时间减少更多。整体上能快不少。

4.5 避坑指南

最后,分享几个我踩过的坑:

  • SRAM溢出:SPL代码太大,超过了SRAM容量。编译时没报错,运行时直接跑飞。解决办法:加编译选项限制代码大小,或者用链接脚本把数据段放到DDR。
  • DDR时序不对:DDR初始化成功了,但读写不稳定。表现为偶尔启动失败,或者运行中死机。我后来用示波器量了DDR时钟,发现时钟抖动超标。换了晶振就好了。
  • BootROM版本差异:同一款芯片,不同批次BootROM可能有细微差异。比如检测介质的顺序不同。换芯片后一定要重新验证启动流程。

好了,这一章就到这里。BootROM和SPL是启动流程的基石,理解透了,后面的U-Boot和内核启动才能顺风顺水。