4. U-Boot 源码结构与启动流程:SPL、TPL、U-Boot proper 与启动三阶段

U-Boot 的源码结构,说实话,第一次接触的人很容易懵。目录多,文件杂,光顶层目录就有几十个。但你别怕,它的设计思路其实很清晰——一切都是为了「在硬件上电后,把系统引导起来」这个终极目标服务的。

我个人习惯把 U-Boot 的源码结构分成三大块:平台相关、通用框架、工具与脚本。搞清楚这三块,你就能在源码里快速定位到想要的东西。

4.1 顶层目录结构速览

我们先看几个最关键的目录:

目录/文件 作用 我的经验
arch/ CPU 架构相关代码(arm、riscv、x86 等) 移植新板子,90% 的时间都在这里
board/ 具体板级初始化代码 每个厂商一个子目录,比如 board/freescale/
common/ 通用命令和框架代码 你敲的 printenv、bootm 都在这里
drivers/ 各种外设驱动(串口、网卡、MMC、NAND 等) 我踩过最多的坑就在这——驱动时序不对
include/ 头文件,尤其是 configs/ 下的板级配置 改配置先找这里,别瞎翻
dts/ 设备树源文件 现在新板子都靠它描述硬件
tools/ 镜像打包、签名等工具 比如 mkimage,生成 U-Boot 镜像必备

嗯,这里要注意:arch/arm/ 下面还有 cpu/、dts/、include/ 等子目录。你想想看,ARM 架构下有那么多 SoC 厂商(NXP、TI、Rockchip、Allwinner 等),每个厂商的启动流程都不一样,所以 arch/arm/mach-xxx/ 就是存放这些差异的地方。

4.2 启动三阶段:SPL、TPL、U-Boot proper

为什么 U-Boot 要分成三个阶段?说白了,就是「空间不够」。

早期的嵌入式芯片,内部 SRAM 可能只有 4KB 或 8KB。你想想看,完整的 U-Boot 编译出来动辄几百 KB,根本放不下。所以就有了 SPL(Secondary Program Loader)——一个迷你版的 U-Boot,负责初始化 DDR,然后把完整的 U-Boot proper 从存储设备加载到内存里。

那 TPL(Tertiary Program Loader)呢?这是后来某些更复杂的 SoC 才引入的。比如有些芯片,内部 SRAM 只有 2KB,连 SPL 都塞不下。那就再加一层:TPL 负责最基础的初始化(比如时钟、电源),然后加载 SPL,SPL 再加载 U-Boot proper。

三阶段关系总结:

  • TPL(可选):最精简的初始化,加载 SPL
  • SPL(必须):初始化 DDR,加载 U-Boot proper
  • U-Boot proper:完整的引导加载程序,加载内核

我曾经在一个 i.MX6 的项目上,因为 SPL 的 DDR 初始化参数配错了,导致 U-Boot proper 加载到内存后跑飞。查了两天才发现是时序参数里一个 CAS latency 写错了。嗯,从那以后我每次调 DDR 参数都格外小心。

4.3 启动流程详解

我们以典型的 ARM 芯片为例,看看上电后到底发生了什么:

  1. ROM code:芯片内部固化的代码,上电后最先执行。它会根据启动模式(eFuse、GPIO 电平)选择启动设备(SD 卡、NAND、SPI NOR 等),然后把 TPL 或 SPL 加载到内部 SRAM 中。
  2. TPL 阶段(如果有):初始化最基本的硬件(比如 PLL、时钟),然后从启动设备加载 SPL 到 SRAM。
  3. SPL 阶段:初始化 DDR 控制器、串口(方便调试)、存储设备驱动。然后从存储设备读取 U-Boot proper 到 DDR 中,跳转执行。
  4. U-Boot proper 阶段:初始化所有外设(网卡、USB、MMC 等),解析环境变量,执行 bootcmd 命令,最终加载并启动内核。

调试小技巧:SPL 阶段可以通过串口输出调试信息。在配置中开启 CONFIG_SPL_SERIAL_SUPPORT 和 CONFIG_SPL_DEBUG,就能看到 SPL 的执行过程。我每次移植新板子,第一件事就是确保 SPL 的串口能打印东西——否则就像瞎子摸象。

下面这张图可以帮你更直观地理解整个流程:

U-Boot 启动三阶段流程图 U-Boot 启动三阶段流程图 ① ROM Code(芯片固化) 读取启动设备,加载 TPL/SPL 到 SRAM ② TPL(可选阶段) 初始化时钟/电源,加载 SPL 到 SRAM ③ SPL(必须阶段) 初始化 DDR,加载 U-Boot proper 到 DDR ④ U-Boot proper 初始化外设,加载并启动内核 ⑤ Linux Kernel SRAM 容量决定是否需要 TPL 通常 4KB 以下需要 TPL SPL 必须包含 DDR 初始化 这是移植中最容易出错的环节

4.4 SPL 与 U-Boot proper 的代码差异

SPL 和 U-Boot proper 虽然共享同一套源码,但编译出来的镜像大小天差地别。原因在于 SPL 通过配置选项裁剪掉了大量功能。

我列个对比表,你一看就明白:

特性 SPL U-Boot proper
典型大小 16KB ~ 128KB 200KB ~ 1MB+
运行位置 内部 SRAM 外部 DDR
命令行 无(或极简) 完整命令行
文件系统支持 通常不支持 支持 ext4、FAT、btrfs 等
网络协议栈 无 完整 TCP/IP
设备树 可选(精简版) 完整设备树
主要任务 初始化 DDR,加载 proper 加载内核、设备树、ramdisk

你可能会问:SPL 怎么知道自己该加载哪个 U-Boot proper 镜像?答案在 CONFIG_SPL_FS_LOAD_PAYLOAD_NAME 或 CONFIG_SPL_NAND_LOAD 这类配置里。SPL 根据这些配置,从存储设备的固定偏移位置读取 U-Boot proper。

常见坑点:

  • SPL 的 DDR 初始化参数必须和硬件完全匹配,否则 U-Boot proper 加载后跑飞
  • U-Boot proper 在存储设备上的位置必须和 SPL 中配置的加载地址一致
  • 如果启用了校验(如 FIT 镜像),SPL 还需要集成对应的哈希算法

4.5 从源码角度看启动入口

我们来看看代码层面是怎么串联起来的。以 ARM64 为例:

// 1. 上电后第一个入口(ROM code 跳转过来)
// arch/arm/cpu/armv8/start.S
_start:
    // 设置异常向量表
    // 初始化栈指针
    // 跳转到 main 函数

// 2. SPL 的 main 函数
// common/spl/spl.c
void board_init_f(ulong dummy)
{
    // 初始化时钟、串口、DDR
    // 调用 spl_load_image() 加载 U-Boot proper
}

// 3. U-Boot proper 的入口
// common/board_f.c
void board_init_f(ulong boot_flags)
{
    // 初始化各种外设
    // 调用 board_init_r() 进入命令行循环
}

嗯,这里有个细节:board_init_f 和 board_init_r 这两个函数,在 SPL 和 U-Boot proper 里都有,但做的事情完全不同。SPL 版本的 board_init_f 只做最基本的初始化,而 U-Boot proper 版本会初始化所有外设。

我个人习惯在 board_init_f 里加一些调试打印,比如打印当前运行的阶段和关键寄存器值。这样一旦启动卡住,我能立刻知道是哪个阶段出了问题。

4.6 配置系统:如何控制三阶段的编译

U-Boot 使用 Kconfig 来管理配置。你运行 make menuconfig 时看到的界面,就是 Kconfig 生成的。

控制三阶段的关键配置项:

  • CONFIG_SPL:启用 SPL 构建
  • CONFIG_TPL:启用 TPL 构建
  • CONFIG_SPL_BUILD:编译时自动定义,用于区分当前编译的是 SPL 还是 U-Boot proper

在源码里,你经常能看到这样的条件编译:

#ifdef CONFIG_SPL_BUILD
    // 这段代码只在 SPL 中编译
    ddr_init();
    serial_init();
#else
    // 这段代码只在 U-Boot proper 中编译
    net_initialize();
    usb_start();
#endif

我曾经接手过一个项目,前任工程师在 SPL 里调用了 usb_start(),结果 SPL 镜像直接超了 SRAM 大小。嗯,这就是没搞清楚 SPL 和 U-Boot proper 的编译边界。

我的建议:在移植新板子时,先确保 SPL 能正常打印信息并初始化 DDR。这一步通了,后面的事情就好办了。你可以先在 SPL 里只保留串口和 DDR 初始化,其他驱动统统关掉——越小越容易调试。

4.7 小结

U-Boot 的三阶段设计,本质上是嵌入式系统资源受限的产物。但随着芯片越来越强,有些 SoC 已经开始尝试把 SPL 和 U-Boot proper 合并了。不过目前来看,SPL + U-Boot proper 仍然是主流方案。