第2章 启动流程深度解析:ROM Code → SPL → U-Boot → ATF → OP-TEE
做 Jacinto 平台 BSP 开发,第一道坎就是搞懂它的启动链路。说实话,我第一次接触 Jacinto 时也被这一长串名字吓到了——ROM Code、SPL、U-Boot、ATF、OP-TEE,每个阶段都像黑盒子。但后来我发现,只要把这条链路拆开看,每个阶段其实只干三件事:初始化硬件、加载下一级镜像、跳转。
这一章我就带你把这五个阶段彻底捋清楚。顺便聊聊怎么通过打印信息定位启动故障——这可是我踩坑踩出来的经验。
2.1 整体启动链路概览
先看一张整体流程图,心里有个全局印象。
这张图里每个阶段都有明确的分工。我习惯把启动链路比作接力赛——每一棒只跑一段,跑完就把控制权交给下一棒。下面我们逐个阶段拆解。
2.2 ROM Code 阶段
芯片上电后,CPU 会从固定地址取第一条指令。在 Jacinto 平台上,这个地址指向片内的 BootROM。ROM Code 是芯片出厂时固化的,你改不了它。
ROM Code 干了什么?
- 初始化最基本的硬件:ARM 核、L3/L4 互联、系统控制模块
- 读取 BOOTMODE 引脚,确定启动介质(SD卡、eMMC、NAND、UART、USB 等)
- 从指定介质读取 SPL 镜像到 SRAM
- 验证镜像签名(如果使能了安全启动)
- 跳转到 SPL 入口
我的经验:ROM Code 阶段的打印信息很少,基本只有通过 UART 输出的几个字符。如果你连这些字符都看不到,先检查 BOOTMODE 引脚电平——我遇到过两次因为拨码开关接触不良导致启动卡死的案例。
ROM Code 支持的启动介质优先级如下:
| 优先级 | 启动介质 | 典型场景 |
|---|---|---|
| 1 | SD 卡(MMC1) | 开发调试阶段 |
| 2 | eMMC(MMC2) | 量产产品 |
| 3 | NAND Flash | 工业控制 |
| 4 | UART | 紧急恢复 |
| 5 | USB DFU | 工厂烧录 |
2.3 SPL 阶段
SPL(Secondary Program Loader)是 ROM Code 加载的第一个用户程序。在 Jacinto 平台上,它通常叫 MLO 或 x-loader。SPL 跑在 SRAM 里,空间非常有限——一般只有 256KB 左右。
SPL 的核心任务:
- 初始化时钟和 PLL,把 CPU 频率提上去
- 初始化 DDR 控制器和 PHY——这是最关键的一步
- 从启动介质加载 U-Boot 到 DDR
- 跳转到 U-Boot
重点:SPL 阶段最常见的故障就是 DDR 初始化失败。DDR 时序参数配错了,板子就死在这里。我建议你在 SPL 里加一段 DDR 读写测试代码,打印测试结果——这能帮你快速定位是硬件问题还是软件问题。
SPL 的打印信息通常长这样:
U-Boot SPL 2021.01 (Jan 15 2024 - 10:30:00 +0800)
Trying to boot from MMC1
spl: mmc boot mode: fs
spl: loading u-boot.img
spl: image offset 0x80000
spl: jumping to 0x80000000
如果你看到 spl: jumping to 0x80000000 之后没反应了,说明 U-Boot 没跑起来。原因可能是 U-Boot 镜像损坏,或者 DDR 地址映射不对。
2.4 U-Boot 阶段
U-Boot 是启动链路中最复杂的一环。它运行在 DDR 里,空间充裕,功能也最丰富。在 Jacinto 平台上,U-Boot 除了加载内核,还要完成以下工作:
- 解析设备树(DTB),配置外设
- 初始化网络、存储、显示等驱动
- 加载 ATF(BL31)和 OP-TEE(BL32)镜像
- 设置启动参数并跳转到内核
U-Boot 的启动流程可以概括为:
start.S → board_init_f() → board_init_r() → main_loop() → bootm
其中 board_init_f() 在 DDR 里做早期初始化,board_init_r() 做完整初始化。我个人习惯在 board_init_f() 里加一个 GPIO 翻转——用示波器看波形,能直观判断 U-Boot 有没有跑到这个阶段。
注意:U-Boot 阶段如果打印信息卡在 Starting kernel ... 之后,问题通常不在 U-Boot 本身,而是内核或设备树有问题。别在 U-Boot 里死磕,去查内核的早期打印。
2.5 ATF 与 OP-TEE 阶段
这两个阶段涉及 ARM 的安全扩展(TrustZone)。ATF(ARM Trusted Firmware)运行在 EL3 安全世界,OP-TEE 运行在 S-EL1。U-Boot 在跳转内核前,会先加载 ATF 和 OP-TEE。
ATF(BL31)负责:
- 安全监控模式调用(SMC)处理
- 电源管理(PSCI)
- 安全中断路由
OP-TEE(BL32)负责:
- 提供可信执行环境(TEE)
- 安全密钥存储、加密运算
- 安全外设访问控制
ATF 的打印信息通常通过 console_init() 输出,格式如下:
NOTICE: BL31: v2.6(release):v2.6-rc0-123-gabcdef
NOTICE: BL31: Built : 10:30:00, Jan 15 2024
INFO: BL31: Initializing runtime services
INFO: BL31: Preparing for EL3 exit to normal world
INFO: BL31: Next image address = 0x82000000
如果 ATF 阶段卡住,最常见的原因是安全配置问题——比如 TZC(TrustZone Controller)配置错误,导致非安全世界访问了安全内存区域。
避坑指南:我曾经在调试 OP-TEE 时,发现内核启动后某些外设无法访问。查了两天才发现是 ATF 的 TZC 配置把外设地址空间划给了安全世界。解决办法是在 ATF 的 plat_tzc_regions_config() 里把非安全外设的地址设为 NS 属性。
2.6 通过打印信息定位启动故障
启动故障定位,说白了就是看打印信息停在哪一行。我把常见故障按阶段整理成了一张表:
| 打印信息卡住的位置 | 可能原因 | 排查方向 |
|---|---|---|
| ROM Code 无输出 | BOOTMODE 配置错误、电源不稳 | 检查拨码开关、测量电源纹波 |
| SPL 加载镜像失败 | MMC 初始化失败、镜像损坏 | 检查 SD 卡格式、重新烧录 MLO |
| SPL DDR 测试失败 | DDR 时序参数错误、硬件焊接不良 | 核对 DDR 数据手册、检查焊接 |
| U-Boot 启动卡死 | 设备树错误、驱动初始化失败 | 检查 DTB 编译、加 debug 打印 |
| ATF 无输出 | BL31 镜像未加载、安全配置错误 | 检查 U-Boot 的 ATF 加载地址 |
| OP-TEE 初始化失败 | TZC 配置冲突、TEE 内存不足 | 检查 TEE 内存大小、TZC 区域 |
| Kernel 启动后无控制台 | 内核参数错误、串口驱动未使能 | 检查 bootargs 中的 console 参数 |
定位故障时,我一般遵循这个思路:
- 看最后一行打印——它告诉你问题出在哪个阶段
- 加打印信息——在关键函数入口加
printf()或debug() - 二分法缩小范围——在怀疑的代码段中间加打印,看前半段还是后半段卡住
- 对比正常板子——拿一块能正常启动的板子对比打印信息
我的调试习惯:在 SPL 和 U-Boot 的 board_init_f()、board_init_r()、bootm 入口各加一个 GPIO 翻转。用逻辑分析仪抓波形,比看串口打印更可靠——因为串口驱动本身可能还没初始化好。
2.7 一个实际故障案例
说个我实际遇到的案例。有块 Jacinto 板子,上电后串口只输出一个字符 U 就停了。这是典型的 ROM Code 阶段问题——U 是 ROM Code 输出的第一个字符,表示它已经开始执行,但后续初始化失败了。
排查过程:
- 检查 BOOTMODE 引脚——正常
- 测量电源——1.8V 和 3.3V 都正常
- 用示波器看时钟——发现 RTC 时钟没有起振
最后发现是 RTC 晶振虚焊。换了个晶振,板子就正常启动了。你看,有时候问题不在软件,在硬件。