4、SPL与TPL详解:TPL的作用(DDR初始化)、SPL的作用(加载U-Boot)、Rockchip TPL/SPL源码分析(spl.c, tpl.c)、调试方法
好,咱们进入第四章。这一章是Rockchip平台移植的核心环节——SPL和TPL。很多刚接触RK平台的工程师,上来就被这两个东西搞晕了。我当年第一次看RK3399的启动流程时,也花了整整两天才理清楚。
说白了,TPL和SPL就是U-Boot为了适应现代SoC复杂启动流程,拆出来的两个"小前导"阶段。为什么这么拆?因为芯片上电后,内部SRAM太小了,装不下完整的U-Boot。必须分步走,一步做一件事。
4.1 TPL的作用:DDR初始化
TPL,全称是Tertiary Program Loader。名字挺绕口,但它的任务非常单一——初始化DDR内存。
你想想看,芯片刚上电时,内部只有几十KB到几百KB的SRAM可用。U-Boot本体动不动几百KB甚至上MB,根本放不下。所以第一步,得先把DDR跑起来,把"大仓库"建好。
TPL就是干这个的。它被BootROM加载到SRAM中,执行DDR的初始化流程。初始化完成后,TPL就把控制权交给SPL。
核心要点:TPL的生命周期极短。它只在SRAM中运行,完成DDR初始化后就"功成身退"了。你甚至可以在调试完成后,把TPL的调试信息关掉,它不影响后续流程。
我在项目中遇到过一个问题:某款RK3568的板子,DDR颗粒型号比较偏门,TPL默认的DDR参数配不上。结果就是SPL根本加载不到DDR里,串口连个影子都没有。后来我手动改了TPL里的DDR时序参数,才把板子救活。
4.2 SPL的作用:加载U-Boot
SPL,Secondary Program Loader。它的任务也很明确——从存储介质(eMMC、SD卡、SPI Flash等)中加载完整的U-Boot到DDR中,然后跳转过去。
SPL比TPL稍微"胖"一点,因为它要处理文件系统、分区表、设备树等逻辑。但它依然受限于SRAM大小,所以功能上做了精简。
| 阶段 | 运行位置 | 主要任务 | 大小限制 |
|---|---|---|---|
| BootROM | 内部ROM | 初始化时钟、加载TPL到SRAM | 几十KB |
| TPL | SRAM | 初始化DDR | 通常 < 32KB |
| SPL | SRAM | 加载U-Boot到DDR | 通常 < 64KB |
| U-Boot | DDR | 完整引导流程 | 几百KB ~ 几MB |
这张表我建议你存下来。调试时遇到"卡在某一步",先对照一下当前代码运行在哪个阶段、哪个内存区域,问题往往就清晰了。
4.3 Rockchip TPL/SPL源码分析
源码分析,咱们挑两个关键文件:spl.c 和 tpl.c。这两个文件都在 arch/arm/mach-rockchip/ 目录下。
4.3.1 tpl.c 分析
tpl.c 的代码量很少,核心就一个函数——board_init_f()。嗯,这里要注意,U-Boot的初始化流程里,board_init_f 是"前阶段初始化",board_init_r 是"后阶段初始化"。TPL只用到前者。
// arch/arm/mach-rockchip/tpl.c (简化示意)
void board_init_f(ulong dummy)
{
// 1. 初始化调试串口(如果开启)
debug_uart_init();
// 2. 调用DDR初始化驱动
// 这里会根据板级配置,调用对应的ddr_*_init()函数
ret = sdram_init();
// 3. 如果DDR初始化成功,返回给BootROM
// BootROM会接着加载SPL
if (ret)
hang();
}
你看,就这么几行。TPL的使命就是调用 sdram_init()。这个函数的具体实现,在 drivers/ram/rockchip/ 下,不同芯片有不同文件,比如 rk3568_ddr.c、rk3588_ddr.c。
调试小技巧:如果你在TPL阶段就卡住了,串口没有任何输出,先检查两件事:一是DDR电源是否正常,二是时钟配置是否正确。我遇到过一块板子,DDR的VDDQ电压只有1.1V(标准是1.2V),结果TPL死活跑不过去。
4.3.2 spl.c 分析
spl.c 比 tpl.c 复杂一些。它同样有 board_init_f(),但做的事情更多。
// arch/arm/mach-rockchip/spl.c (简化示意)
void board_init_f(ulong dummy)
{
// 1. 初始化调试串口
debug_uart_init();
// 2. 初始化时钟(如果需要)
// 有些板子在TPL阶段时钟没配全,SPL要补上
// 3. 初始化存储控制器(eMMC/SDMMC)
// 这样才能从存储介质读取U-Boot
// 4. 调用spl_early_init()
// 初始化一些基础数据结构
// 5. 返回,由SPL框架继续执行
// 框架会调用spl_load_image()加载U-Boot
}
SPL的 board_init_f() 执行完后,控制权回到SPL框架(common/spl/spl.c)。框架会调用 spl_load_image(),根据你配置的启动介质,加载U-Boot到DDR中。
加载完成后,SPL会执行 jump_to_image_no_args(),跳转到U-Boot的入口点。至此,SPL的任务结束。
4.4 调试方法
调试TPL和SPL,最头疼的就是"串口没输出"。因为这两个阶段太早了,很多外设还没初始化。我总结了一套调试流程,分享给你。
4.4.1 开启早期调试输出
在U-Boot的配置中,开启以下选项:
CONFIG_DEBUG_UART=y
CONFIG_DEBUG_UART_ROCKCHIP=y
CONFIG_DEBUG_UART_BASE=0xFF1A0000 // 根据你的芯片修改
CONFIG_DEBUG_UART_CLOCK=24000000 // 串口时钟频率
开启后,TPL和SPL就能在初始化串口后立即输出信息。这是你调试的第一道防线。
4.4.2 使用GPIO指示灯
如果串口完全没反应,别慌。用GPIO点灯法。在 tpl.c 或 spl.c 的关键位置,加上GPIO操作代码:
// 在某个关键步骤后点亮LED
writel(0x1, GPIO_BASE + GPIO_SWPORTA_DR);
我曾经用这个方法,在RK3399的板子上定位到一个问题:TPL的DDR初始化其实成功了,但SPL加载时因为eMMC的时钟没配好,导致读取失败。LED在TPL阶段亮了,但在SPL阶段没亮,问题范围一下子就缩小了。
4.4.3 分析启动日志
当串口能输出后,你会看到类似这样的日志:
U-Boot TPL 2024.01 (Jan 01 2024 - 12:00:00)
Channel 0: LPDDR4, 2133MHz
BW=32 Col=10 Bk=8 CS0 Row=16 CS1 Row=16
Trying to boot from BOOTROM
Returning to boot ROM...
U-Boot SPL 2024.01 (Jan 01 2024 - 12:00:00)
Trying to boot from MMC1
U-Boot 2024.01 (Jan 01 2024 - 12:00:00)
注意看"Trying to boot from ..."这一行。它告诉你SPL尝试从哪个设备加载U-Boot。如果这里显示"MMC1"但你的U-Boot放在SD卡(通常是MMC0),那就要检查启动设备配置了。
常见坑点:Rockchip的启动设备选择,由芯片的BOOT引脚电平决定。但SPL里还有一个独立的配置,叫 CONFIG_SYS_MMC_ENV_DEV 和 CONFIG_SYS_MMC_ENV_PART。这两个配置如果不匹配实际硬件,SPL会去错误的分区找U-Boot,然后卡死。
4.4.4 使用JTAG调试
如果以上方法都搞不定,那就得上JTAG了。我个人习惯用OpenOCD + GDB。在TPL或SPL的入口处打断点,单步执行,观察寄存器值。
JTAG调试时,重点关注这几个寄存器:
- DDR控制器状态寄存器:确认DDR初始化是否完成
- 时钟寄存器:确认CPU、总线、DDR时钟是否正确
- 存储控制器寄存器:确认eMMC/SD卡是否识别
我记得有一次,用JTAG调试发现SPL在读取eMMC时,返回的数据全是0xFF。后来查了半天,发现是eMMC的复位引脚没拉高。这种问题,靠串口日志根本看不出来,只有JTAG能抓到。
4.5 本章知识结构图
下面这张图,把TPL和SPL在整个启动流程中的位置、各自的任务、以及调试手段串起来了。建议你对照着看源码,会清晰很多。
这张图把整个流程和调试手段都串起来了。你调试时遇到问题,先看卡在哪个阶段,然后选择对应的调试方法。串口能出信息最好,出不了就用GPIO点灯,还不行就上JTAG。三板斧下去,大部分问题都能定位到。