3、U-Boot启动流程分析:第一阶段(SPL/TPL)详解、第二阶段(U-Boot proper)详解、启动流程中的关键函数

U-Boot 的启动流程,说白了就是一段从「裸金属」到「引导系统」的接力赛。我刚开始接触这块时,总觉得代码跳来跳去很乱,后来把 SPL、TPL、proper 这三个阶段理清楚,整个脉络就通了。

你想想看,一块刚上电的 SoC,片内 SRAM 可能只有几十 KB,连个完整的 U-Boot 都放不下。怎么办?分阶段来。先跑一个极小的 loader,初始化 DDR,再把完整的 U-Boot 从 Flash 搬到内存里。这就是 SPL 和 TPL 存在的意义。

核心概念速览

  • TPL(Tertiary Program Loader):最早期初始化,通常只做时钟、电源、基础外设
  • SPL(Secondary Program Loader):初始化 DDR,加载 U-Boot proper 到内存
  • U-Boot proper:完整的引导加载程序,负责加载内核、设备树、ramdisk

3.1 第一阶段:SPL/TPL 启动详解

SPL 的入口点通常是 arch/arm/lib/crt0.S 里的 _main。嗯,这里要注意,不同架构的入口略有差异,但 ARM 平台最典型。我个人习惯从 _main 开始追踪,因为它把整个 SPL 的生命周期串起来了。

3.1.1 SPL 启动的核心步骤

  1. 设置异常向量表:CPU 刚上电,先得把中断向量表放到正确位置。
  2. 初始化 CPU 模式:切换到 SVC 模式,关中断,设栈指针。
  3. 板级早期初始化:调用 board_init_f(),做时钟、串口、GPIO 等基础外设初始化。
  4. 初始化 DDR 控制器:这是 SPL 最核心的任务——把内存搞起来。
  5. 重定位:把 SPL 自身从 SRAM 拷贝到 DDR(如果 SRAM 太小)。
  6. 加载 U-Boot proper:从 Flash/MMC/SD 读取 U-Boot 镜像到 DDR 指定地址。
  7. 跳转到 U-Boot proper:清理环境,跳转到 _main 的 proper 版本。

我的经验:我在调试一块 i.MX6ULL 板子时,SPL 死活加载不了 U-Boot。后来发现是 DDR 时序参数配错了,SPL 初始化 DDR 时 CRC 校验没通过。建议你在 board_init_f() 里加个串口打印,确认 DDR 初始化成功再往下走。

3.1.2 TPL 的特殊角色

TPL 不是所有平台都用。它出现在一些内存极度受限的场景,比如某些 SoC 的 SRAM 只有 4KB。TPL 只做最必要的事:初始化时钟、电源管理 IC、基础 IO,然后立刻加载 SPL 到 SRAM。说白了,TPL 就是个「启动加载器的加载器」。

特性 TPL SPL U-Boot proper
代码大小 通常 < 4KB 通常 < 64KB 几百 KB 到几 MB
运行位置 片内 SRAM 片内 SRAM 或 DDR DDR
主要任务 时钟、电源、加载 SPL DDR 初始化、加载 proper 加载内核、文件系统
配置宏 CONFIG_TPL CONFIG_SPL 默认启用

3.2 第二阶段:U-Boot proper 启动详解

当 SPL 把 U-Boot proper 加载到 DDR 并跳转过去后,真正的「大块头」开始运行。proper 阶段的入口同样是 _main,但此时它运行在 DDR 里,有充足的内存空间。

3.2.1 proper 启动的关键阶段

  • board_init_f():初始化串口、计时器、GPIO,建立全局数据结构 gd_t。
  • relocate_code():把 U-Boot 自身从加载地址重定位到 DDR 末尾附近。为什么要重定位?因为加载地址可能被内核覆盖,得挪个安全位置。
  • relocate_vectors():更新异常向量表,指向重定位后的新地址。
  • board_init_r():这是 proper 的核心初始化函数。它初始化所有驱动:MMC、网络、USB、文件系统、环境变量等。
  • 进入主循环:调用 main_loop(),等待用户输入或自动启动内核。

避坑指南:我曾经在重定位环节栽过跟头。U-Boot 重定位后,所有全局变量和函数指针的地址都变了。如果你在 board_init_f() 里注册了回调函数,重定位后必须重新计算地址。U-Boot 用 gd->reloc_off 来修正,但你自己写的汇编代码很容易忽略这一点。

3.3 启动流程中的关键函数

我把整个启动流程中最重要的几个函数拎出来,你对照着源码看会更清楚。

函数名 所在文件 作用
_main arch/arm/lib/crt0.S SPL 和 proper 的公共入口,设置栈、调用初始化序列
board_init_f() common/board_f.c 早期板级初始化:时钟、串口、GD 结构体
init_sequence_f[] common/board_f.c 一个函数指针数组,定义了 board_init_f 里要调用的所有子函数
relocate_code() arch/arm/lib/relocate.S 把 U-Boot 自身拷贝到 DDR 高位地址
board_init_r() common/board_r.c 后期板级初始化:驱动、环境变量、控制台
init_sequence_r[] common/board_r.c board_init_r 的子函数序列,包括 mmc、net、usb 等初始化
main_loop() common/main.c U-Boot 主循环:处理命令、启动内核

这里我特别想提一下 init_sequence_f[] 和 init_sequence_r[]。这两个数组是 U-Boot 初始化框架的精髓。你想想看,每个板子需要初始化的外设都不一样,但 U-Boot 用这种「函数指针数组」的方式,把初始化步骤做成了可配置的列表。你只需要在数组里增删函数指针,就能定制启动流程。

调试技巧:当 U-Boot 启动到一半卡住时,我通常会在 init_sequence_f[] 的每个函数入口加串口打印。哪个函数没打印出来,问题就出在哪。别小看这个笨办法,它帮我解决过至少 10 次启动失败的问题。

3.4 整体启动流程 SVG 框架图

下面这张图把整个启动流程串起来了。从 CPU 上电到 U-Boot proper 进入主循环,每个阶段的关键函数和跳转关系都标清楚了。

U-Boot 启动流程总览 CPU 上电 / Reset Vector _main (SPL 入口) board_init_f() DDR 初始化 加载 U-Boot proper 跳转 _main (proper 入口) board_init_f() relocate_code() board_init_r() main_loop() 说明: SPL 阶段运行在 SRAM,负责 DDR 初始化和加载 proper;proper 阶段运行在 DDR,负责驱动初始化和内核加载。 SPL 关键函数: _main → board_init_f → DDR init → load proper Proper 关键函数: _main → board_init_f → relocate → board_init_r → main_loop

3.5 启动流程中的几个关键细节

最后聊几个容易踩坑的细节。这些是我在实际项目中反复确认过的。

  • SPL 的栈大小:SPL 运行在 SRAM,栈空间非常有限。我见过有人把大数组定义在局部变量里,直接导致栈溢出。建议 SPL 里尽量用全局变量或动态分配。
  • DDR 时序参数:DDR 初始化是 SPL 最敏感的部分。参数配错了,轻则启动失败,重则损坏 Flash。我习惯先在参考板上用 DDR 训练工具跑一遍,拿到稳定参数再写进代码。
  • 重定位后的链接脚本:U-Boot proper 重定位后,链接脚本里的地址和实际运行地址不一致。如果你在 board_init_r() 里直接引用某个符号的地址,记得加上 gd->reloc_off 偏移。
  • 环境变量的存储:U-Boot proper 启动时会从 Flash/MMC 加载环境变量。如果环境变量校验失败,U-Boot 会使用默认值。我在调试网络启动时经常遇到这个问题——环境变量里配错了服务器 IP,但默认值是对的,导致我排查了半天。

一句话总结:SPL 负责「活下来并找到 proper」,proper 负责「活好并引导内核」。把这两个阶段的职责分清楚,启动流程的代码就不再是一团乱麻了。