1. U-Boot启动流程全景:从ROM Code到Main U-Boot的完整链路
做嵌入式开发的朋友,十有八九都跟U-Boot打过交道。但说实话,很多人对它的启动过程是一知半解的——只知道上电后U-Boot会跑起来,至于中间经历了什么,为什么有时候快有时候慢,往往说不清楚。
我个人习惯,在优化任何系统之前,先把它的完整生命周期画出来。今天我们就从芯片上电那一刻开始,把U-Boot的启动链路从头到尾捋一遍。
1.1 启动的第一棒:ROM Code
芯片上电后,CPU会从一个固定的地址取指令。这个地址通常固化在芯片内部ROM里,所以叫ROM Code。它是芯片厂商写死的,你改不了。
ROM Code干的事情其实很简单:
- 初始化最基本的硬件(时钟、SRAM、看门狗)
- 从启动介质(SD卡、eMMC、NAND Flash、SPI NOR等)读取下一级代码
- 验证签名(如果使能了安全启动)
- 跳转到下一级代码的入口
这里有个关键点:ROM Code能用的内存非常有限。大多数芯片内部SRAM只有几十KB到几百KB。所以它只能加载一个很小的程序——这就是SPL(Secondary Program Loader)诞生的原因。
1.2 第二棒:SPL(Secondary Program Loader)
SPL是U-Boot源码编译出来的一个小镜像,大小通常被限制在几十KB以内。它的任务很明确:初始化DDR内存,然后把Main U-Boot从存储介质加载到DDR里。
SPL的执行流程大致如下:
- 设置CPU为SVC模式,关中断
- 初始化串口(方便打印调试信息)
- 初始化时钟和DDR控制器
- 初始化存储介质驱动(MMC、NAND、SPI等)
- 从存储介质读取Main U-Boot到DDR
- 跳转到Main U-Boot入口
你想想看,SPL其实就是一个微缩版的U-Boot。它也有设备树,也有驱动模型,只不过功能被裁剪到极致。
1.3 可选阶段:TPL(Tertiary Program Loader)
有些芯片的SRAM实在太小,连SPL都塞不下。这时候就需要引入TPL。TPL比SPL更小,只做一件事:初始化外部RAM(通常是DDR),然后加载SPL到DDR里运行。
TPL不是必须的。我个人的经验是,除非SRAM小于64KB,否则没必要引入TPL。多一个阶段就多一次加载和跳转,时间上不划算。
| 阶段 | 典型大小 | 运行位置 | 主要职责 |
|---|---|---|---|
| ROM Code | 固化在芯片内 | 内部ROM | 加载SPL/TPL,安全验证 |
| TPL | 8~16 KB | 内部SRAM | 初始化DDR,加载SPL |
| SPL | 32~128 KB | 内部SRAM或DDR | 初始化DDR/存储,加载Main U-Boot |
| Main U-Boot | 200 KB~2 MB | DDR | 完整引导流程,加载内核 |
1.4 最后一棒:Main U-Boot
Main U-Boot就是我们通常说的U-Boot。它被加载到DDR后,会做一系列初始化工作:
- 完整的板级初始化(board_init_f / board_init_r)
- 设备树解析(FDT)
- 驱动模型初始化(DM)
- 环境变量加载(env)
- 网络协议栈初始化(如果使能)
- 命令行界面(CLI)准备
- 自动启动脚本执行(bootcmd)
Main U-Boot的启动流程可以分成两个大阶段:
第一阶段(board_init_f):此时U-Boot还在只读存储区(比如SPL把它加载到的DDR地址),不能随意写数据。这个阶段主要做:初始化串口、时钟、DRAM控制器、堆栈等。
第二阶段(board_init_r):U-Boot把自己重定位到DDR的末尾地址,然后开始完整的初始化。这时候驱动模型、设备树、环境变量、网络协议栈等才会真正跑起来。
1.5 完整链路图
下面这张图把整个启动链路画出来了。我建议你保存下来,后面每一章的优化点都可以在这张图上找到对应位置。
1.6 各阶段耗时分布
上面这张图里我标了典型耗时。实际项目中,这些数字可能差很多。比如我调过的一块全志H3板子,SPL阶段花了200多ms,其中DDR Training就占了150ms。后来换了更快的Training算法,直接砍到40ms。
Main U-Boot的耗时浮动更大。如果使能了网络协议栈、USB、文件系统,光驱动初始化就能吃掉300ms。这也是我们后面要重点优化的地方。
1.7 避坑指南
做启动优化这么多年,有些坑我踩过不止一次。这里列几个最常见的:
- DDR Training参数照搬参考设计:不同PCB布局、不同批次的内存颗粒,Training参数可能有差异。我曾经因为照搬了开发板的配置,导致量产时部分板子启动失败。
- SPL镜像超出SRAM大小:加功能时没注意SPL的大小限制,结果编译出来的镜像放不进SRAM。嗯,这种问题通常只能在链接阶段发现。
- 忽略ROM Code的启动介质读取速度:有些芯片的ROM Code支持配置读取速度,默认值往往很保守。如果你用的是高速SD卡或eMMC,适当提高读取速度能省下不少时间。
- Main U-Boot重定位后的地址冲突:如果设备树或者内核的加载地址跟U-Boot重定位后的地址重叠,就会出现诡异的问题——有时候能启动,有时候不行。
好了,这一章我们把U-Boot的完整启动链路梳理清楚了。从ROM Code到Main U-Boot,每个阶段做什么、花多少时间、哪里是优化重点,心里应该有个底了。