第2章:启动流程深度解析——从ROM Code到内核的完整链路与调试方法
TI Jacinto平台的启动流程,说实话,是BSP调试中最容易出问题、也最让人头疼的部分。我刚开始接触这个平台时,光是把启动跑通就花了两周。后来摸清了门道,才发现其实每个阶段都有清晰的调试手段。
这一章,我会把整个启动链路拆开来讲。从芯片上电的第一条指令,到Linux内核的start_kernel,每个环节我都会结合自己的踩坑经验,告诉你该怎么看、怎么调。
2.1 整体启动链路概览
Jacinto平台的启动,不是一步到位的。它分了五个阶段:
- ROM Code — 芯片内部固化的引导代码
- SPL (Secondary Program Loader) — 二级引导程序
- U-Boot — 完整的引导加载器
- ATF (ARM Trusted Firmware) — ARM安全固件
- OP-TEE (Open Portable Trusted Execution Environment) — 可信执行环境
- Linux Kernel — 操作系统内核
每个阶段都有明确的入口和出口。我习惯用一张图来理解这个流程:
核心要点:每个阶段都负责初始化一部分硬件,然后把控制权交给下一阶段。任何一个环节出问题,启动都会卡住。
2.2 ROM Code阶段
芯片上电后,CPU从复位向量开始执行。这个向量地址是固定的,指向芯片内部的BootROM。ROM Code是芯片出厂时固化的,你改不了它。
ROM Code会做几件事:
- 初始化最基本的硬件(时钟、PLL、引脚复用)
- 检测启动介质(SD卡、eMMC、NAND Flash、UART等)
- 从启动介质读取SPL(MLO)到内部SRAM
- 验证SPL的签名(如果使能了安全启动)
- 跳转到SPL入口
调试技巧:ROM Code阶段出问题,最常见的现象是串口没有任何输出。我建议你第一步检查电源时序和复位信号。用示波器量一下PORz和RESETz的波形,看看是否满足数据手册的要求。
我曾经遇到一个项目,板子死活不启动。查了两天,最后发现是复位信号的上拉电阻虚焊了。ROM Code根本跑不起来,自然什么输出都没有。
2.3 SPL阶段
SPL是TI平台上的二级引导程序,在Jacinto平台上通常叫MLO。它被ROM Code加载到SRAM中执行,因为此时DDR还没有初始化。
SPL的主要任务:
- 初始化DDR控制器和PHY
- 配置时钟树(设置PLL频率)
- 从启动介质加载U-Boot到DDR
- 跳转到U-Boot入口
SPL的代码量很小,通常只有几十KB。它运行在SRAM中,所以调试起来比较麻烦。我一般用两种方法:
- UART输出:在SPL代码中加入debug_uart的打印信息
- JTAG断点:用CCS或OpenOCD在SPL入口处设置断点
注意:SPL阶段如果DDR初始化失败,后续所有阶段都无法进行。我建议你在SPL的DDR初始化代码中加入状态检查,把DDR控制器寄存器的值打印出来。这样能快速定位是时序问题还是配置问题。
2.4 U-Boot阶段
U-Boot是完整的引导加载器,运行在DDR中。它比SPL功能强大得多,支持文件系统、网络、命令行交互等。
U-Boot的启动流程:
| 步骤 | 操作 | 调试方法 |
|---|---|---|
| 1 | 初始化串口、定时器 | 查看串口输出是否正常 |
| 2 | 读取环境变量 | printenv命令查看 |
| 3 | 加载设备树(DTB) | fdt命令检查 |
| 4 | 加载ATF和OP-TEE | 检查加载地址是否正确 |
| 5 | 跳转到ATF | 设置断点在smc指令处 |
我个人习惯在U-Boot阶段做大量调试。因为U-Boot有完整的命令行,你可以手动执行各种命令来验证硬件是否正常工作。
实用命令:
md <addr>— 查看内存内容mw <addr> <value>— 写内存mmc read <addr> <blk> <cnt>— 读MMC设备sf read <addr> <offset> <len>— 读SPI Flashbootm <addr>— 手动启动内核
2.5 ATF与OP-TEE阶段
U-Boot跳转到ATF后,系统进入EL3异常级别。ATF负责初始化安全监控,然后加载OP-TEE到安全世界。
这个阶段的调试比较特殊,因为涉及到安全和非安全两个世界。我常用的调试手段:
- ATF日志:ATF有独立的日志系统,通过UART输出
- OP-TEE日志:OP-TEE的core dump和trace信息
- 共享内存:在安全世界和非安全世界之间共享调试信息
我记得有一次,OP-TEE加载后系统直接挂死。查了半天,发现是OP-TEE的image加载地址和ATF的配置不一致。ATF把OP-TEE加载到0x9e000000,但OP-TEE自己的链接脚本用的是0x9f000000。这种问题,说白了就是地址没对齐。
避坑指南:我曾经因为ATF和OP-TEE的版本不匹配,浪费了整整一天。建议你在移植时,确保ATF、OP-TEE和U-Boot来自同一个版本的SDK。混用不同版本的组件,很容易出现接口不兼容的问题。
2.6 Linux内核启动
OP-TEE初始化完成后,系统返回到非安全世界,跳转到Linux内核的入口。内核启动分为两个阶段:
- 汇编阶段:设置页表、使能MMU、跳转到start_kernel
- C语言阶段:start_kernel初始化所有子系统
内核启动的调试,我建议从以下几个方面入手:
| 问题现象 | 可能原因 | 调试方法 |
|---|---|---|
| 内核无任何输出 | 串口驱动未初始化 | 检查earlycon配置 |
| 内核panic | 设备树配置错误 | 检查DTB中的reg和interrupts |
| 挂死在某个驱动 | 驱动probe失败 | 加initcall_debug参数 |
| 文件系统挂载失败 | root=参数错误 | 检查内核命令行 |
重要:内核启动时,如果设备树中的时钟配置和实际硬件不一致,会导致外设无法正常工作。我建议你在设备树中加入clock-names和assigned-clocks属性,并在驱动中打印实际获取到的时钟频率。
2.7 完整调试链路总结
整个启动链路调试,说白了就是逐级排查。我总结了一个调试清单:
- ROM Code:检查电源、复位、时钟,用示波器看波形
- SPL:检查DDR初始化,用JTAG设断点
- U-Boot:检查环境变量、设备树、加载地址
- ATF/OP-TEE:检查版本匹配、加载地址、日志输出
- Kernel:检查earlycon、设备树、驱动probe
每个阶段都有对应的调试手段。你只要按照这个顺序排查,90%的启动问题都能定位到。
我的经验:启动调试最忌讳跳步。不要因为内核没起来,就直接去改内核代码。先确认前面的阶段都正常,再往下查。我曾经见过有人花了一周调内核,最后发现是SPL的DDR时序配置错了。嗯,这种亏吃过一次就够了。