2. 启动流程概览

大家好,我是你们的嵌入式系统讲师。今天咱们聊聊U-Boot的启动流程。说实话,我刚开始接触U-Boot时,也被那一堆汇编代码和复杂的初始化顺序搞得头大。但后来我发现,只要把启动流程拆成几个阶段来看,其实没那么玄乎。

U-Boot从复位到最终把内核加载起来,说白了就是一条「从硬到软」的路径。你想想看,CPU刚上电时,内存都还没初始化,你拿什么跑C代码?所以必须分阶段来。

启动阶段的划分:SPL、TPL、U-Boot proper

现代U-Boot把启动过程拆成了三个主要阶段:

  • TPL(Tertiary Program Loader):最早期加载器,负责最基础的硬件初始化
  • SPL(Secondary Program Loader):二级加载器,负责初始化DRAM并加载U-Boot proper
  • U-Boot proper:完整的U-Boot,负责加载操作系统内核

嗯,这里要注意:不是所有平台都需要TPL。像一些简单的ARM Cortex-M系列,直接从SPL开始就够了。但像Xilinx ZynqMP、TI AM65x这类复杂SoC,TPL几乎是必须的。

核心观点:启动阶段的划分,本质上是「用最小的代码量,逐步把硬件环境搭起来」。每一步只做必要的事,绝不贪多。

从复位到内核启动的完整路径

我习惯把整个路径画成一条流水线。咱们一步步来看:

  1. 复位向量:CPU上电后,从复位向量地址开始执行。通常是0x00000000或0xFFFF0000。
  2. 汇编阶段:设置异常向量表、初始化栈指针、关闭中断。这段代码在start.S里。
  3. 板级初始化:调用board_init_f(),初始化时钟、串口、GPIO等基础外设。
  4. DRAM初始化:这是SPL的核心任务。没有DRAM,你连U-Boot proper都放不下。
  5. 加载U-Boot proper:从Flash/SD卡/eMMC中把U-Boot proper读到DRAM里。
  6. 跳转到U-Boot proper:SPL执行完,直接跳转到DRAM中的U-Boot proper入口。
  7. U-Boot proper初始化:完整的设备驱动、文件系统、网络协议栈初始化。
  8. 加载内核:从存储介质或网络加载内核镜像到DRAM。
  9. 启动内核:设置启动参数,跳转到内核入口。

我曾经在一个项目中,SPL阶段死活加载不了U-Boot proper。查了两天才发现,原来是DRAM的时序参数配错了。你想想看,DRAM都没正常工作,数据读出来全是乱的,怎么可能加载成功?

个人经验:调试启动问题时,我建议先确认SPL阶段的串口输出是否正常。如果串口连「U-Boot SPL」这几个字都打印不出来,那问题大概率出在时钟或复位上。

各阶段的内存布局

不同阶段使用的内存区域是完全隔离的。我画了一张图,方便大家理解:

U-Boot启动阶段内存布局 高地址 低地址 内核加载区域 Kernel Image (由U-Boot proper加载) U-Boot proper 完整U-Boot (由SPL加载到DRAM) SPL 二级加载器 (运行在SRAM或片上RAM) TPL 最早期加载器 (仅初始化最基本硬件) 启动内核 加载内核 加载U-Boot 初始化DRAM

避坑指南:我曾经遇到过SPL和U-Boot proper使用同一块内存区域的情况。结果SPL刚把U-Boot proper加载完,自己还没跳转呢,就把自己覆盖了。嗯,这种bug查起来特别痛苦。所以一定要确认各阶段的内存地址不重叠。

启动阶段的典型配置

不同平台对启动阶段的支持不一样。我整理了一个表格,方便大家对照:

平台 TPL SPL U-Boot proper 典型存储介质
ARM Cortex-A (如i.MX6) 可选 必需 必需 SD卡、eMMC、NAND
ARM Cortex-M (如STM32MP1) 不需要 必需 可选 SPI Flash、SD卡
RISC-V (如SiFive FU540) 可选 必需 必需 QSPI Flash、SD卡
Xilinx ZynqMP 必需 必需 必需 QSPI、SD卡、eMMC

看到这个表格,你可能会问:为什么有些平台需要TPL,有些不需要?说白了,就是片上SRAM的大小决定的。如果片上SRAM只有几十KB,连SPL都放不下,那就得用TPL先初始化DRAM,再把SPL加载到DRAM里跑。

启动流程中的关键函数

我挑几个最关键的函数,给大家拆解一下:

  • _start:复位向量入口,在arch/arm/lib/vectors.S中。设置CPU为SVC模式,关闭中断。
  • board_init_f:板级早期初始化。初始化时钟、串口、定时器。这个函数在SPL和U-Boot proper中都会调用。
  • board_init_r:板级后期初始化。初始化完整的设备驱动、文件系统、网络协议栈。
  • main_loop:U-Boot proper的主循环。等待用户输入命令,或者自动执行bootcmd启动内核。

调试技巧:我习惯在board_init_f里加一个串口打印,输出当前阶段的标识。比如打印"SPL: board_init_f done"。这样一旦启动卡住,马上就能知道是哪个阶段出了问题。

启动流程的时序分析

从复位到内核启动,整个过程的时序大致如下:

  1. 0-10ms:CPU复位,执行汇编代码,初始化栈指针。
  2. 10-50ms:SPL执行,初始化DRAM,加载U-Boot proper。
  3. 50-200ms:U-Boot proper执行,初始化所有外设,加载内核。
  4. 200ms+:内核启动,挂载根文件系统,启动用户空间。

你想想看,从按下电源键到看到登录提示符,整个过程也就几百毫秒。但背后却是成千上万行代码在协同工作。这就是嵌入式系统的魅力所在。

总结一下:U-Boot的启动流程,本质上是一个「由简到繁、由小到大」的过程。TPL做最基础的初始化,SPL把DRAM搭起来,U-Boot proper负责加载内核。每一层都只做自己该做的事,绝不越界。