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几乎是必须的。
核心观点:启动阶段的划分,本质上是「用最小的代码量,逐步把硬件环境搭起来」。每一步只做必要的事,绝不贪多。
从复位到内核启动的完整路径
我习惯把整个路径画成一条流水线。咱们一步步来看:
- 复位向量:CPU上电后,从复位向量地址开始执行。通常是0x00000000或0xFFFF0000。
- 汇编阶段:设置异常向量表、初始化栈指针、关闭中断。这段代码在start.S里。
- 板级初始化:调用board_init_f(),初始化时钟、串口、GPIO等基础外设。
- DRAM初始化:这是SPL的核心任务。没有DRAM,你连U-Boot proper都放不下。
- 加载U-Boot proper:从Flash/SD卡/eMMC中把U-Boot proper读到DRAM里。
- 跳转到U-Boot proper:SPL执行完,直接跳转到DRAM中的U-Boot proper入口。
- U-Boot proper初始化:完整的设备驱动、文件系统、网络协议栈初始化。
- 加载内核:从存储介质或网络加载内核镜像到DRAM。
- 启动内核:设置启动参数,跳转到内核入口。
我曾经在一个项目中,SPL阶段死活加载不了U-Boot proper。查了两天才发现,原来是DRAM的时序参数配错了。你想想看,DRAM都没正常工作,数据读出来全是乱的,怎么可能加载成功?
个人经验:调试启动问题时,我建议先确认SPL阶段的串口输出是否正常。如果串口连「U-Boot SPL」这几个字都打印不出来,那问题大概率出在时钟或复位上。
各阶段的内存布局
不同阶段使用的内存区域是完全隔离的。我画了一张图,方便大家理解:
避坑指南:我曾经遇到过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"。这样一旦启动卡住,马上就能知道是哪个阶段出了问题。
启动流程的时序分析
从复位到内核启动,整个过程的时序大致如下:
- 0-10ms:CPU复位,执行汇编代码,初始化栈指针。
- 10-50ms:SPL执行,初始化DRAM,加载U-Boot proper。
- 50-200ms:U-Boot proper执行,初始化所有外设,加载内核。
- 200ms+:内核启动,挂载根文件系统,启动用户空间。
你想想看,从按下电源键到看到登录提示符,整个过程也就几百毫秒。但背后却是成千上万行代码在协同工作。这就是嵌入式系统的魅力所在。
总结一下:U-Boot的启动流程,本质上是一个「由简到繁、由小到大」的过程。TPL做最基础的初始化,SPL把DRAM搭起来,U-Boot proper负责加载内核。每一层都只做自己该做的事,绝不越界。