第2章:启动流程深度解析——从ROM Code到内核的完整链路与调试方法

TI Jacinto平台的启动流程,说实话,是BSP调试中最容易出问题、也最让人头疼的部分。我刚开始接触这个平台时,光是把启动跑通就花了两周。后来摸清了门道,才发现其实每个阶段都有清晰的调试手段。

这一章,我会把整个启动链路拆开来讲。从芯片上电的第一条指令,到Linux内核的start_kernel,每个环节我都会结合自己的踩坑经验,告诉你该怎么看、怎么调。

2.1 整体启动链路概览

Jacinto平台的启动,不是一步到位的。它分了五个阶段:

  1. ROM Code — 芯片内部固化的引导代码
  2. SPL (Secondary Program Loader) — 二级引导程序
  3. U-Boot — 完整的引导加载器
  4. ATF (ARM Trusted Firmware) — ARM安全固件
  5. OP-TEE (Open Portable Trusted Execution Environment) — 可信执行环境
  6. Linux Kernel — 操作系统内核

每个阶段都有明确的入口和出口。我习惯用一张图来理解这个流程:

核心要点:每个阶段都负责初始化一部分硬件,然后把控制权交给下一阶段。任何一个环节出问题,启动都会卡住。

TI Jacinto平台启动链路全景图 ROM Code BootROM SPL MLO U-Boot u-boot.img ATF BL31 OP-TEE BL32 Linux Kernel BL33 关键说明: • ROM Code → SPL:从SD/eMMC/NAND读取MLO到SRAM • SPL → U-Boot:初始化DDR,加载u-boot.img到DDR • U-Boot → ATF:跳转到EL3,启动安全监控 • ATF → OP-TEE:加载TEE OS到安全世界 • OP-TEE → Kernel:返回到非安全世界,启动Linux 调试手段 • JTAG断点 • UART串口日志 • 寄存器dump • 硬件断点 • 时序分析仪 • 内存dump

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的主要任务:

  1. 初始化DDR控制器和PHY
  2. 配置时钟树(设置PLL频率)
  3. 从启动介质加载U-Boot到DDR
  4. 跳转到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 Flash
  • bootm <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内核的入口。内核启动分为两个阶段:

  1. 汇编阶段:设置页表、使能MMU、跳转到start_kernel
  2. C语言阶段:start_kernel初始化所有子系统

内核启动的调试,我建议从以下几个方面入手:

问题现象 可能原因 调试方法
内核无任何输出 串口驱动未初始化 检查earlycon配置
内核panic 设备树配置错误 检查DTB中的reg和interrupts
挂死在某个驱动 驱动probe失败 加initcall_debug参数
文件系统挂载失败 root=参数错误 检查内核命令行

重要:内核启动时,如果设备树中的时钟配置和实际硬件不一致,会导致外设无法正常工作。我建议你在设备树中加入clock-names和assigned-clocks属性,并在驱动中打印实际获取到的时钟频率。

2.7 完整调试链路总结

整个启动链路调试,说白了就是逐级排查。我总结了一个调试清单:

  1. ROM Code:检查电源、复位、时钟,用示波器看波形
  2. SPL:检查DDR初始化,用JTAG设断点
  3. U-Boot:检查环境变量、设备树、加载地址
  4. ATF/OP-TEE:检查版本匹配、加载地址、日志输出
  5. Kernel:检查earlycon、设备树、驱动probe

每个阶段都有对应的调试手段。你只要按照这个顺序排查,90%的启动问题都能定位到。

我的经验:启动调试最忌讳跳步。不要因为内核没起来,就直接去改内核代码。先确认前面的阶段都正常,再往下查。我曾经见过有人花了一周调内核,最后发现是SPL的DDR时序配置错了。嗯,这种亏吃过一次就够了。