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 启动的核心步骤
- 设置异常向量表:CPU 刚上电,先得把中断向量表放到正确位置。
- 初始化 CPU 模式:切换到 SVC 模式,关中断,设栈指针。
- 板级早期初始化:调用
board_init_f(),做时钟、串口、GPIO 等基础外设初始化。 - 初始化 DDR 控制器:这是 SPL 最核心的任务——把内存搞起来。
- 重定位:把 SPL 自身从 SRAM 拷贝到 DDR(如果 SRAM 太小)。
- 加载 U-Boot proper:从 Flash/MMC/SD 读取 U-Boot 镜像到 DDR 指定地址。
- 跳转到 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 进入主循环,每个阶段的关键函数和跳转关系都标清楚了。
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 负责「活好并引导内核」。把这两个阶段的职责分清楚,启动流程的代码就不再是一团乱麻了。