4、启动流程第一阶段:ROM Code/BootROM:芯片内部固化程序、从存储介质加载SPL/MLO

好,咱们正式开始聊嵌入式Linux启动的第一阶段。这一阶段,芯片一上电,最先跑起来的代码,就是芯片内部固化的ROM Code,也叫BootROM。

说白了,这是芯片出厂前就焊死在片内ROM里的一段小程序。你改不了它,也删不掉它。它的任务只有一个:从指定的存储介质里,把下一级引导程序(SPL或MLO)加载到片内SRAM里,然后跳过去执行。

我刚开始接触这块时,总觉得ROM Code离自己很远,反正又改不了。后来踩了几个坑才明白——不理解ROM Code的行为,你连板子为什么起不来都查不出原因。

4.1 ROM Code到底干了什么?

芯片上电后,CPU从复位向量(通常是0x00000000或0xFFFF0000)开始执行。但这时候DDR还没初始化,NAND/NOR/eMMC的控制器也没配好。ROM Code就是来解决这个“先有鸡还是先有蛋”的问题的。

它的核心流程,我总结成下面几步:

  1. 硬件初始化:关看门狗、关中断、设置CPU时钟(通常跑一个很低的频率)、初始化片内SRAM。
  2. 检测启动介质:根据芯片的Boot Pin(启动引脚)电平,判断从哪个设备启动。比如eMMC、SD卡、NAND Flash、SPI NOR Flash、UART等。
  3. 读取启动头:从选中的介质头部,读取一段固定格式的启动头(Header)。这个Header里包含了SPL/MLO的加载地址、大小、校验信息等。
  4. 加载SPL/MLO:把SPL/MLO从存储介质拷贝到片内SRAM里。注意,这时候DDR还没起来,只能用SRAM。
  5. 校验并跳转:做CRC或ECC校验。通过后,CPU跳转到SRAM里的SPL入口地址,开始执行SPL。

关键点:ROM Code只负责加载SPL/MLO,它不负责初始化DDR。DDR初始化是SPL的工作。所以SPL必须足够小,小到能塞进片内SRAM里。

4.2 启动介质的选择与检测

不同芯片厂商的ROM Code,支持的启动介质列表不一样。我列一个常见的对照表:

芯片厂商 常见启动介质 Boot Pin数量 典型SRAM大小
TI(AM335x) MMC/SD、NAND、SPI、UART、USB 5个(SYSBOOT[4:0]) 64KB ~ 128KB
NXP(i.MX6) eMMC/SD、NAND、SPI NOR、SATA 4个(BOOT_CFG[3:0]) 96KB ~ 256KB
Rockchip(RK3288) eMMC、SD、SPI NOR、NAND 3个(BOOT[2:0]) 128KB ~ 192KB
Allwinner(H3) MMC、NAND、SPI、USB 4个(BOOT_SEL[3:0]) 32KB ~ 64KB

你想想看,Boot Pin的数量直接决定了你能支持多少种启动方式。TI的AM335x有5个引脚,可以组合出32种启动模式。我当年做项目时,就因为Boot Pin上拉电阻虚焊,导致板子一直从UART启动,串口疯狂打印乱码。查了两天才发现是硬件问题。

个人经验:拿到新板子,第一件事就是量Boot Pin电平,确认芯片认为它该从哪个设备启动。这能帮你排除一半的启动问题。

4.3 SPL与MLO:它们是什么关系?

很多初学者会问:SPL和MLO到底是不是同一个东西?

嗯,这里要说明一下。SPL是Secondary Program Loader的缩写,是U-Boot源码里编译出来的一个精简版U-Boot。MLO是TI平台对SPL的特定称呼,全称是Mmc LOader。说白了,MLO就是TI平台上的SPL,只是名字不同。

它们的共同特点:

  • 体积小:必须能放进片内SRAM。TI的AM335x SRAM只有64KB,SPL编译出来一般30~50KB。
  • 功能单一:只做两件事——初始化DDR,然后从存储介质加载完整的U-Boot到DDR里。
  • 位置固定:必须放在存储介质的固定偏移位置。比如SD卡,SPL放在第1个扇区(偏移0x200),MLO放在第1~3个扇区。

注意:SPL/MLO的加载位置和格式,必须严格遵循芯片ROM Code的要求。比如TI要求MLO前512字节是Header,包含CRC和加载信息。如果你自己编译的SPL没有生成正确的Header,ROM Code会直接拒绝加载。

4.4 启动流程的完整时序图

下面这张图,把从芯片上电到SPL开始执行的完整流程画出来了。我建议你仔细看,尤其是箭头上的条件判断。

ROM Code启动流程时序图 ROM Code启动流程时序图 芯片上电 / 复位 CPU从复位向量开始执行 ROM Code:关看门狗、初始化SRAM 读取Boot Pin电平 选择启动介质(eMMC/SD/NAND/SPI) 读取启动头(Header) 加载SPL/MLO到片内SRAM 校验CRC → 跳转到SPL入口 说明: • 整个过程由ROM Code自动完成 • 开发者无法修改ROM Code • SPL必须放在固定偏移位置 常见问题: • Boot Pin虚焊 → 启动介质错误 • Header格式不对 → 加载失败

4.5 避坑指南:我踩过的三个坑

做U-Boot移植这么多年,ROM Code相关的坑我踩了不少。挑三个最典型的说说:

坑一:SPL体积超了SRAM

我曾经在AM335x上编译SPL,加了几个调试驱动,结果体积飙到72KB。而芯片SRAM只有64KB。ROM Code加载到一半就卡死了,串口没有任何输出。查了两天才发现是SPL太大了。

解决办法:编译SPL时加-Os优化,去掉不必要的驱动。用size命令检查编译结果,确保小于SRAM大小。

坑二:启动头CRC计算错误

TI的MLO要求前512字节包含CRC校验。我手动改了MLO的加载地址,但忘了重新计算CRC。结果ROM Code加载完MLO后校验失败,直接死循环。板子就像砖头一样,毫无反应。

解决办法:用TI官方工具mkimage生成MLO,不要手动改二进制。或者用tools/mkimage重新计算CRC。

坑三:Boot Pin电平被忽略

有一次我明明把Boot Pin拉高到3.3V,想让芯片从eMMC启动。但板子一直从UART启动。后来发现是Boot Pin的上拉电阻选错了阻值,导致电平没达到芯片的阈值。芯片认为引脚是低电平,所以选了UART。

解决办法:查芯片手册,确认Boot Pin的阈值电压。上拉电阻一般选4.7kΩ~10kΩ,不要随便用1kΩ或100kΩ。

4.6 小结:这一阶段你该记住什么?

ROM Code/BootROM这一阶段,说白了就是芯片的“自举”过程。它不需要你写代码,但你必须理解它的行为。否则,板子起不来,你连从哪查起都不知道。

我建议你记住三点:

  • ROM Code不可修改,但你可以通过Boot Pin选择启动介质。
  • SPL/MLO必须足够小,小到能塞进片内SRAM。
  • 启动头格式必须正确,否则ROM Code拒绝加载。