news 2026/10/1 20:20:07

深入理解U-Boot的SPL与TPL:源码、启动流程与调试实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
深入理解U-Boot的SPL与TPL:源码、启动流程与调试实践

搞嵌入式Linux的,尤其是玩过裸机或者做过ARM平台Bring-up的朋友,几乎都会在U-Boot这个“最后一道bootloader”上花不少时间。但很多人一开始接触U-Boot,就被SPL和TPL这两个名字绕晕了:明明是两级加载器,为什么有的平台只有SPL,有的平台要SPL+TPL一起上?它们到底从哪来、往哪去、源码在什么位置?我之前在啃u-boot 2024.07这套源码的时候,特意把SPL/TPL的执行路径从头到尾捋了一遍,这篇就把整理的结果分享出来,希望能帮到同样在这块犯迷糊的人。

1. SPL与TPL是什么,为什么会有这俩Loader

1.1 从Boot ROM的局限性说起

要搞懂SPL和TPL,得先理解一个现实约束:SoC内部的Boot ROM并不是万能的。芯片出厂时,Boot ROM里固化了一段极小的引导代码,它的主要任务是从外部存储介质(比如eMMC、SD卡、SPI NOR、NAND)里读取第一段可执行程序,然后加载到片内SRAM并跳转执行。问题在于,Boot ROM对读取介质的方式、镜像大小、甚至文件系统格式都非常挑剔,往往只能读取一个固定偏移处的一小块二进制,大小通常被限制在几十KB到一两百KB。

而完整的U-Boot(也就是我们常说的u-boot.bin或者u-boot.itb)经过各种功能裁剪,体量动辄几百KB甚至上MB,SRAM根本塞不下,而且U-Boot还要初始化DDR控制器才能让大容量内存可用。Boot ROM不可能把初始化DDR的复杂逻辑都塞进来,于是就需要一个“过渡程序”先把DDR等关键硬件准备好,再把完整U-Boot搬运到DDR里运行。这个过渡程序,就是SPL(Secondary Program Loader,二级程序加载器)。

那TPL又是哪里冒出来的?有一类情况很特殊:某些SoC的SRAM空间实在太小(比如只有几十KB),连一个像样的SPL都放不下,但U-Boot偏偏又需要做复杂的DDR训练、或者要跑安全启动校验、又或者要支持多种存储介质枚举。这时候如果把“初始化DDR”和“加载完整U-Boot”这两件事塞进一个SPL,体积就不达标。解决办法是再拆一级:用最小的TPL(Tertiary Program Loader,三级程序加载器)只做DDR初始化和极少量硬件配置,然后从存储介质里加载SPL到DDR,SPL再负责加载最大号的完整U-Boot。所以你能看到,有些平台启动链是Boot ROM → TPL → SPL → U-Boot proper,有些则是Boot ROM → SPL → U-Boot proper,还有些干脆不需要SPL直接Boot ROM → U-Boot(这种情况一般是小系统或者U-Boot本身裁剪得极小)。

1.2 SPL和TPL的分工明细

为了更直观地看清这俩Loader的分工,我整理了一张对比表,这也是我在阅读源码时反复对照的:

维度SPLTPL
中文名二级程序加载器三级程序加载器
主要职责初始化DDR、时钟、串口等关键外设,枚举启动介质,加载下一级镜像最小化初始化(尤其是DDR训练),加载SPL
代码规模相对较大,可包含存储驱动、FIT镜像解析等极小,通常只有几KB到十几KB
运行位置片内SRAM(一般也被Link到SRAM地址)片内SRAM,且比SPL占用更小
典型应用大多数主流平台(如i.MX、ZynqMP、部分瑞萨平台)Rockchip系列、部分需要安全启动的复杂SoC
编译产物u-boot-spl.binu-boot-tpl.bin
核心源码位置common/spl/spl.c同样是common/spl/spl.c(通过宏区分)

别看SPL和TPL名字差了一级,它们本质上都是U-Boot这套源码编译出来的,共用大量代码,只是通过不同的编译宏裁剪掉不同的功能。这一点在源码里体现得特别明显——我在第2节详细给你拆。

1.3 实际SoC上的部署差异

举个我跑过的具体例子。Rockchip RK3399这块芯片,启动顺序就是Boot ROM → TPL → SPL → U-Boot proper。为什么会这样?因为RK3399上DDR初始化代码固定且体积不小,TPL把DDR训练做完后,SPL才能舒服地加载U-Boot。而反观Xilinx Zynq UltraScale+ MPSoC,Boot ROM直接加载SPL到OCM,SPL完成DDR初始化后再加载ATF和U-Boot,并不需要TPL。

这里还有个小误区要澄清:很多文章把SPL称为“mini U-Boot”,但其实SPL不仅仅是U-Boot的缩水版。它的启动流程虽然和完整U-Boot有共同祖先,很多函数名甚至都一样(比如board_init_f、board_init_r),但在内部逻辑上有大量专门的实现。比如common/spl/目录下那一堆spl_mmc.c、spl_spi_nor.c、spl_nand.c、spl_fit.c,才是SPL赖以“找镜像、读镜像”的本事。TPL虽然也挂在同一个目录下,但TPL能用的驱动和功能被限制得更死,通常连文件系统支持都被砍掉,只保留最原始的“读块、加载”能力。

2. 源码目录与核心文件盘点

2.1 2024.07源码树相关目录梳理

如果你下载的是u-boot 2024.07这个版本,解开压缩包后第一件事,建议先把和SPL/TPL相关的目录装进脑子里。很多新手一上来就冲进arch/arm/mach-xxx里找启动代码,结果越看越乱。我建议按下面这个顺序来:

  • common/spl/:这是SPL和TPL的家。几乎所有SPL/TPL特有的代码都在这里,包括核心的spl.c、spl_mmc.c、spl_nand.c、spl_fit.c、spl_net.c等等。
  • arch/arm/cpu/armv8/(或armv7):CPU级别启动入口,比如armv8的start.S,SPL的入口点也在这里,不过会加上宏开关。
  • include/spl.h:SPL相关的数据结构和函数接口声明都在这,看这个文件能快速了解SPL内部有哪些组件。
  • board/<厂商>/<具体板子>/:板级初始化代码,很多板子的SPL配置(比如板级DDR参数)也会出现在这里,比如rk3399-evb这类目录下就有专门的DDR初始化文件。
  • configs/<板子>的defconfig:板级默认配置,决定编译时开不开SPL、TPL,以及各自用什么镜像格式。

还有一个很容易混淆的点:你在编译完成后,会在源码根目录看到spl/和tpl/这两个新的产物文件夹,但它们不是源码目录,而是编译中间文件和产物存放目录。真正的源码在common/spl/。这点最好先分清楚,不然找源码时容易扑空。

2.2 common/spl/spl.c —— 调度核心长什么样

如果你只打算读一个文件来理解SPL/TPL,那就读common/spl/spl.c。这个文件里的board_init_r()做完一系列初始化后,会进入核心调度逻辑,最终调用根据启动设备选出的加载函数。我摘一段简化后的关键流程,大家感受一下:

void board_init_r(gd_t *gd, ulong dest_addr) { spl_common_init(); board_boot_order(spl_boot_list, ARRAY_SIZE(spl_boot_list)); if (IS_ENABLED(CONFIG_SPL_RAM_SUPPORT)) { spl_set_ram_size(); } if (!spl_next_phase()) { /* NOTREACHED */ } }

这段代码看着简单,但信息量不小。spl_common_init()会初始化堆、串口、驱动模型(dm)等;board_boot_order()让每个板子决定优先从哪个设备启动;spl__next_phase()是跳转入口,它内部会先看当前是不是最后一级,如果是,就用jump_to_image_no_args()之类的函数跳到U-Boot proper或Linux,如果还不是最后一级,就加载下一级Loader。

真正触发加载的核心逻辑其实是_spl_load()。它会根据boot_device来选驱动:MMC、SPI NOR、NAND还是网络等。2024.07版本在加载逻辑上已经大量使用FIT镜像格式(flattened image tree),也就是说SPL不再单纯地从一个固定扇区地址读裸二进制,而是会解析FIT里存放的多段镜像(比如ATF、U-Boot proper、甚至DTB),按需加载。这也是SPL里spl_fit.c越来越重要的原因。

2.3 XPL_BUILD与代码复用魔法

很多人在阅读源码时会看到大量CONFIG_XPL_BUILD这样的宏,这个XPL(eXtra Program Loader)是U-Boot社区后来引入的一个统一概念,把SPL和TPL“合并称呼”。也就是说,只要代码在SPL或TPL环境里编译,CONFIG_XPL_BUILD就会被定义。具体区分时,判断CONFIG_SPL_BUILD还是CONFIG_TPL_BUILD即可。

我举个例子,在common/spl/spl_mmc.c这类文件顶部,经常能看到:

#if CONFIG_IS_ENABLED(MMC) ... #endif

这里的CONFIG_IS_ENABLED(...)宏特别神奇,它会根据当前编译的是SPL还是TPL,自动去匹配CONFIG_SPL_MMC或CONFIG_TPL_MMC。比如在TPL里编译时,CONFIG_IS_ENABLED(MMC)就等价于判断CONFIG_TPL_MMC有没有定义。这个机制让同一份驱动代码可以同时在SPL和TPL里被复用,同时又在配置层面严格隔离:你可以让TPL不带MMC驱动,SPL带MMC驱动,互不干扰。

理解了这一层,再看源码就通透了:spl_mmc.c并不是只属于SPL的,它在TPL下也可能被编译进去,前提是TPL使能了对应配置。所以找源码的时候别被文件名带spl前缀误导,TPL用的也可能是这些文件,只是编译出来的功能尺寸完全不同。

3. 从ROM到Linux的执行路径拆解

3.1 两级Loader的完整启动链路

现在我们把视角提高到宏观层面,把从芯片上电到Linux内核启动的完整执行路径画出来(虽然不让你画流程图,但我可以用文字描述清楚这条主线):

  1. 芯片上电复位,CPU从SoC内固化Boot ROM的固定地址开始执行。
  2. Boot ROM读取Boot Device上固定偏移出的镜像头,校验并加载TPL(如果有)或SPL到SRAM,然后跳转。
  3. TPL开始执行,它只做最基础的时钟与DDR初始化。DDR训练完成、内存可用了,TPL再从存储介质加载SPL到DDR(也可能是继续保持SRAM)。
  4. SPL继续执行,完善外设初始化(比如打开串口、MMC控制器、显示板级信息),随后加载完整U-Boot(u-boot.bin)到DDR,同时可能一并加载ATF(ARM可信固件)、DTB等。
  5. SPL跳转到完整U-Boot入口,U-Boot proper接管,驱动模型全面激活,然后按用户环境变量启动Linux内核。

对于只有SPL没有TPL的平台,第2、第3步会合并成:Boot ROM直接加载SPL,SPL做DDR初始化后直接加载U-Boot。所以“执行路径”这个词,本质上就是“控制权在哪个Loader手里,以及谁负责把下一级搬进来”。

3.2 board_init_f 与 board_init_r,SPL也有这两阶段

熟悉U-Boot proper启动流程的人,肯定知道board_init_f和board_init_r这两个阶段。SPL同样沿用这两阶段设计,只不过做得更大简化。在arch/arm/cpu/armv8/start.S里,SPL入口会设置栈指针后调用board_init_f,这一步主要是早期硬件初始化(串口、时钟、重定位相关参数)。由于SPL大多直接在SRAM里运行且不重定位(部分平台支持重定位到DDR),所以board_init_f只会非常克制地做点事。

然后控制权交给board_init_r,这个函数在common/spl/spl.c里实现。它负责初始化堆空间、DM驱动模型、读取启动设备顺序,最后进入加载加载阶段。你可以把board_init_r看作是SPL的“主场”,几乎所有核心动作都在这里完成。如果SPL串口有输出,你会看到类似U-Boot SPL 2024.07这样的打印,然后紧接着下一级Loader加载中。

这里要特别注意:TPL的board_init_f和board_init_r与SPL的并不完全相同,虽然同名,但TPL版本会跳过大量初始化,只做最必要的事。在编译时,common/spl/spl.c会根据CONFIG_TPL_BUILD调整内部逻辑,比如TPL通常不会去初始化复杂的存储子系统,甚至不会初始化堆,直接调完DDR初始化就去加载SPL了。

3.3 镜像加载逻辑与跳转细节

镜像加载的核心函数是_spl_load(),它内部根据boot_device走不同的分支。以MMC启动为例,关键逻辑在spl_mmc.c里,会根据CONFIG_SPL_MMC_BOOT_MMC等配置选择是直接从MBR分区里找FIT镜像,还是跳到固定块地址去读裸镜像。

代码如下(示意,非完整摘录):

static int spl_mmc_load_image(struct spl_image_info *spl_image, struct spl_boot_device *bootdev) { u32 boot_mode = spl_boot_mode(bootdev->boot_device); /* Try from partition */ err = mmc_load_image_raw_sector(spl_image, mmc, info, sector); /* If that fails, fallback to raw */ }

加载完镜像后,SPL会根据spl_image->flags、os类型等属性判断该跳到哪。如果是普通裸机U-Boot,直接jump_to_image_no_args();如果带了ATF,可能会用bl2_to_bl31类似的路径,通过scmi接口或者smc指令把控制权转给更高安全等级的BL31。这些跳转的核心就是拿到下一级镜像的入口地址,准备好参数寄存器,然后清掉缓存、关掉MMU、跳转过去。

这套流程里最容易出问题的地方就是“加载地址”和“执行地址”不一致。SPL把U-Boot读到了DDR里的A地址,但U-Boot链接脚本里指定的运行地址是B地址,一旦不同,跳转过去必定死机。我调试时经常先用串口打印确认spl_image.load_addr和entry_point值,再做对比定位。

3.4 链接脚本与Text Base地址

SPL和TPL能落在SRAM正确位置,靠的是各自的链接脚本。在arch/arm/cpu/armv8/u-boot-spl.lds和arch/arm/cpu/armv7/u-boot-spl.lds里,定义了输出段布局。编译时通过CONFIG_SPL_TEXT_BASE(或TPL版本)指定代码段的首地址。这个地址必须和SoC的实际SRAM映射地址对上,否则一上电就PC跑飞。

我以armv8平台举例,链接脚本里最关键的几行是:

. = CONFIG_SPL_TEXT_BASE; .text : { *(.__image_copy_start) *(.vectors) *(.text*) }

CONFIG_SPL_TEXT_BASE这个值通常定义在头文件里,比如include/configs/<板子>.h或者Kconfig配置里。Rockchip的TPL有个特点,它的CONFIG_TPL_TEXT_BASE往往被安排在SRAM低地址,直接映射到DDR初始化代码需要的地址,这部分如果没配对,DDR训练指令就跑飞到Nor Flash地址上去了,后果就是板子完全无输出。

4. 配置、编译与调试实操指南

4.1 Kconfig体系下的SPL/TPL开关解析

在u-boot 2024.07里,SPL和TPL绝大多数功能都通过Kconfig来控制。打开main Kconfig,你会看到CONFIG_SPL、CONFIG_TPL这两个总开关。开发时通常用make menuconfig进行图形化配置,在“Boot options”下面的“SPL/TPL”选项里展开。

几个我常用的关键配置:

  • CONFIG_SPL_FRAMEWORK:框架总开关,定义SPL会走common/spl/spl.c这套流程。
  • CONFIG_SPL_MMC,CONFIG_SPL_SPI_MTD,CONFIG_SPL_NAND_SUPPORT:控制SPL支持哪种启动设备。
  • CONFIG_SPL_FIT_IMAGE:使能FIT镜像解析,复杂平台基本都是靠它同时加载多段镜像。
  • CONFIG_TPL_DRIVERS_MISC:TPL专属的小功能开关,一般不会开太多。
  • CONFIG_XPL_BUILD:这是自动推断的,不需要手动配,但代码里经常拿它来隔离SPL/TPL编译单元。

配置时经常遇到“开了SPL功能但编译报错‘undefined reference to spl_...’”,这种情况根因往往是缺少对应驱动在SPL下的板级支持函数。比如SPL MMC启动时,如果某个板子在SPL阶段还没有注册MCI设备,就会链接失败。遇到这类报错,我一般先查链接脚本有没有把需要的u_boot_list_2_...段包含进去,再查dts里对应控制器节点在u-boot,dm-pre-reloc属性上有没有加标记。

4.2 构建产物与编译流程说明

编译U-Boot 2024.07,我们看一下具体流程。以某个支持SPL/TPL的板子为例,执行:

make <board>_defconfig make -j8

编译完成后,你会看到根目录下出现u-boot.bin、u-boot.srec等文件,同时spl/目录下出现u-boot-spl.bin、u-boot-spl(ELF),tpl/目录下出现u-boot-tpl.bin等。如果平台本来就不支持TPL,tpl目录就不会生成。

需要注意的是,很多厂商会把SPL打包到自己的镜像工具里。比如Rockchip的loader.bin通常就包含DDR初始化代码+TPL+SPL+U-Boot。你要弄清楚自己板子最终烧写的是哪一段:是spl/u-boot-spl.bin直接烧到偏移0,还是生成的idbloader.img这种复合文件?这个我从实际经历看,非常容易搞混,烧错偏移基本就白焊了。

4.3 调试SPL/TPL的日常操作与排查方法

调试SPL/TPL最有效的工具还是串口。保证串口输出正常的情况下,我一般会分三步定位问题:

  1. 确认控制权有没有交到SPL:如果串口完全没有输出,可能Boot ROM都没读出SPL,重点检查启动介质偏移、编译产物放的位置、镜像头校验。很多SoC要求SPL镜像头部带特定magic number,可以用hexdump确认前几个字节。
  2. 看SPL初始化进度:打开CONFIG_SPL_DEBUG选项,SPL内部会打印更多调试信息,比如board_init_r里初始化到哪一步。如果串口有输出但止步于DDR初始化,那重点查DDR参数、PMIC供电、PCB走线。
  3. 看加载的下一级镜像地址对不对:打开CONFIG_SPL_..._RAW打印或者直接调用debug()打印load_addr。很多板子死在“SPL加载完毕但跳转即重启”,多半是地址不对或者镜像需要decompress而SPL没做。

另外还有一个高效调试手段:给SPL加一个新的启动选项,比如从TFTP或者USB启动,把镜像放到远端服务器上,利用已有U-Boot的网口加载镜像。这样可以免去反复烧写启动介质,缩短迭代周期。但前提是SPL阶段驱动支持网络设备,一些轻量级SoC的SPL是不带网卡的,那就只能老老实实烧卡了。

4.4 别踩这些坑:常见问题速查表

最后我把平时容易翻车的点整理成了速查表,希望大家在实际操作中能少走弯路:

问题现象可能原因排查/解决办法
SPL无输出,板子完全没反应Boot ROM未找到SPL镜像,或镜像偏移错误用hexdump检查烧写偏移与镜像magic;确认是否包含头的校验值
SPL打印完“U-Boot SPL”后死机DDR初始化失败检查DDR训练参数、频率设置、供电电压;读芯片手册确认延时参数
SPL加载U-Boot后跳转即复位加载地址与U-Boot链接地址不一致对比CONFIG_SPL_...里的load_addr和CONFIG_SYS_TEXT_BASE
编译报undefined referenceSPL驱动缺少板级支持或链接脚本缺段检查u_boot_list_2_...段是否包含,确认DTS节点有u-boot,dm-pre-reloc
TPL能跑,SPL没反应TPL加载SPL地址可能与SPL运行地址不一致查TPL的跳转目标地址与SPL的CONFIG_SPL_TEXT_BASE
换存储介质后启动失败SPL只固化了一个启动设备顺序修改board_boot_order()或spl_boot_device()复位后的枚举顺序
串口有乱码SPL早期时钟初始化时波特率不稳定在SPL阶段尽早初始化UART时钟,避免在时钟变化期间改写控制台端口
镜像太大放不进SRAM工具链优化级别不够或功能裁剪不够把SPL/TPL的-Os优化打开,或者关闭无用驱动和调试符号

我可能是全公司里把common/spl/spl.c读得最多的人之一。做底层Bring-up这几年,最大的感受是:SPL/TPL的名字看起来复杂,但拆到底就是“分级加载”的思路。只要理解Boot ROM的约束、看过spl.c的核心调度、再把链接地址和加载地址这两个“命门”抓在手里,整个执行路径基本就跑不出你的手掌心了。如果在你的板子上SPL/TPL还有更特殊的玩法,欢迎评论区一起聊聊,毕竟不同SoC的启动链差异,永远是嵌入式里最折磨人也最有意思的一块。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 20:19:09

智能家居硬件开源项目查找渠道与实操学习路径

经常有人问我“去哪里查找智能家居硬件开源项目”&#xff0c;尤其是刚准备入门嵌入式开发的初学者。大家一上来就搜“智能家居系统”关键词&#xff0c;结果搜出来的东西要么太庞杂&#xff0c;要么太抽象&#xff0c;真正能移植到手里的单片机、能画成 PCB、能接入自己家的设…

作者头像 李华
网站建设 2026/10/1 20:18:54

使用 Azure OpenAI 配置 Codex 完整指南:TaoToken 统一 Key 接入实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/1 20:16:35

PDD回环测试:工业实时通信链路验证实战指南

1. 项目概述&#xff1a;这不是“测网速”&#xff0c;而是验证PDD链路真实可用性的关键手术刀 “pdd参数验证&#xff0c;回环测试”——这八个字在工业自动化、电力监控、轨道交通信号系统和智能楼宇集成现场&#xff0c;几乎就是工程师打开调试笔记本时的第一道门槛。它不是…

作者头像 李华
网站建设 2026/10/1 20:16:01

工业互联网数字化中台:从系统重复建设迈向数据通联落地指南

简介&#xff1a;这份PPT方案聚焦工业互联网数字化中台建设&#xff0c;面向企业管理者、IT架构师及数字化转型规划人员&#xff0c;系统阐述中台如何解决传统IT系统应用与资源绑定、数据孤岛、系统维护成本高等痛点。方案内容涵盖工业数字化中台的价值、格创数字化中台的特点、…

作者头像 李华