1. U-Boot启动流程深度剖析:从reset向量到main_loop
做嵌入式开发的朋友,十有八九都跟U-Boot打过交道。但说实话,很多人对它的印象就是「一段跑完就跳进内核的代码」。我以前也这么想,直到有一次产品启动慢了200ms,被客户追着问——这才逼着我一条一条指令去抠启动流程。
这一章,我们就从CPU上电后的第一条指令开始,一路走到main_loop。我会把每个阶段的性能瓶颈点都指出来。嗯,这些坑我基本都踩过。
1.1 整体启动流程一览
先看一张总图,把整个路径串起来。你心里有个谱,后面拆开讲才不会乱。
整个流程可以分成两大段:SPL阶段(如果使能了)和U-Boot proper阶段。我们重点讲proper阶段,因为SPL的逻辑类似,只是更精简。
1.2 Reset向量:CPU的第一口气
CPU上电后,PC指针直接跳到复位向量地址。对于ARM来说,通常是0x00000000或0xFFFF0000。这里放的就是_start标签。
/* arch/arm/cpu/armv7/start.S */
.globl _start
_start:
b reset
/* 异常向量表 */
ldr pc, _undefined_instruction
ldr pc, _software_interrupt
ldr pc, _prefetch_abort
ldr pc, _data_abort
ldr pc, _not_used
ldr pc, _irq
ldr pc, _fiq
这里有个细节——第一条指令必须是b reset,而不是ldr pc, =reset。为什么?因为此时CPU可能还没初始化好内存控制器,ldr指令从文字池加载地址可能会触发异常。我见过有人把启动代码从别的平台直接搬过来,第一条指令就挂了,查了半天才发现是这个问题。
1.3 汇编初始化:关中断、设栈、清BSS
进入reset标签后,U-Boot会做几件关键的事:
- 切换到SVC模式,关掉所有中断
- 设置CPU频率(有些平台在这里就做,有些放到后面)
- 初始化栈指针——注意,此时栈可能还在SRAM里,空间很有限
- 清零BSS段
- 调用
_main,进入C环境
这段代码全部是汇编,执行速度很快。但有一个性能陷阱:BSS清零。如果BSS段很大(比如你定义了一个全局大数组),逐字节清零会消耗不少时间。我建议你编译完后用objdump看一下BSS大小,超过几百KB就要考虑优化了。
1.4 board_init_f:C环境初建
进入board_init_f()后,U-Boot开始用C语言做事。这个函数在common/board_f.c里,核心逻辑是遍历一个初始化序列数组。
/* common/board_f.c 中的 init_sequence_f */
static init_fnc_t init_sequence_f[] = {
setup_machine,
init_baud_rate, /* 初始化串口波特率 */
serial_init, /* 串口初始化 */
console_init_f, /* 控制台早期初始化 */
display_options, /* 打印U-Boot版本信息 */
display_text_info, /* 打印文本段信息 */
print_cpuinfo, /* 打印CPU信息 */
dram_init, /* 初始化DRAM */
setup_dest_addr, /* 设置重定位目标地址 */
reserve_global_data, /* 预留GD结构体空间 */
reserve_malloc, /* 预留malloc堆空间 */
reserve_board, /* 预留板级信息空间 */
reserve_video, /* 预留显存空间 */
reserve_trace, /* 预留trace缓冲区 */
reserve_uboot, /* 预留U-Boot自身空间 */
reserve_mmu, /* 预留MMU页表空间 */
setup_reloc, /* 设置重定位偏移 */
clear_bss, /* 再次清零BSS */
...
};
这个序列里,dram_init是第一个大瓶颈。它要初始化DDR控制器、训练时序、校准阻抗。我遇到过一块板子,DDR训练花了300ms,占了整个启动时间的40%。
另一个容易被忽略的点是串口初始化。如果串口驱动里做了轮询等待(比如等待TX FIFO空),每次打印都会卡一下。你想想看,如果调试信息开了全量打印,光串口输出就能吃掉上百毫秒。
1.5 重定位:从SRAM搬到DRAM
board_init_f跑完后,U-Boot把自己从SRAM(或Flash)复制到DRAM的高地址处。这一步叫重定位。
重定位的代码在arch/arm/lib/relocate.S里,核心逻辑就是:
/* 伪代码示意 */
relocate_code:
/* 计算源地址和目标地址 */
/* 逐字复制 */
copy_loop:
ldmia r0!, {r3-r10}
stmia r1!, {r3-r10}
cmp r0, r2
blo copy_loop
/* 修复符号表 */
/* 跳转到新地址 */
这里有个性能问题:复制粒度。默认是8个字(32字节)一次循环。如果你的U-Boot镜像有500KB,在低速Flash上复制就要花不少时间。我建议你检查一下CONFIG_SYS_MONITOR_LEN,看看实际镜像大小,别留太多冗余空间。
1.6 board_init_r:真正的初始化开始
重定位完成后,调用board_init_r()。这个函数在common/board_r.c里,同样有一个初始化序列:
| 初始化项 | 典型耗时 | 性能影响说明 |
|---|---|---|
| initr_timer | 1-5ms | 初始化系统定时器,通常很快 |
| initr_env | 10-100ms | 从存储设备加载环境变量,如果设备慢(如SPI Flash),这里会卡 |
| initr_dm | 5-50ms | 驱动模型初始化,绑定所有设备 |
| initr_net | 50-500ms | 网卡初始化,包括PHY自协商,这是重灾区 |
| initr_mmc | 100-1000ms | MMC/SD卡初始化,卡识别和模式切换很慢 |
| stdio_add_devices | 10-100ms | 控制台设备注册,如果包含LCD初始化,时间会更长 |
看到没?网卡和MMC是两大耗时大户。我曾经在一个项目里,PHY自协商等了800ms,后来改成固定模式配置,直接降到50ms。
1.7 设备初始化:驱动模型与探测
U-Boot从2016年开始全面推行驱动模型(Driver Model, DM)。所有设备都通过udevice结构体管理,初始化时递归探测。
DM的初始化流程是:
- 绑定阶段:根据设备树(DTB)创建所有
udevice实例 - 探测阶段:按依赖关系依次调用
probe回调
性能问题出在探测阶段。很多驱动在probe里做了大量轮询等待。比如I2C控制器,如果总线上挂了一个不存在的设备,驱动可能会超时重试3次,每次100ms——300ms就这么没了。
dm tree命令一看,有个GPIO扩展芯片的设备树节点配错了地址,驱动反复探测失败。去掉那个节点后,启动时间直接砍掉200ms。
1.8 环境变量加载:被低估的瓶颈
环境变量默认存储在存储设备上(SPI Flash、MMC、NAND等)。加载过程包括:
- 读取存储设备(可能涉及块设备驱动初始化)
- 校验CRC
- 解析键值对
- 设置默认值(如果校验失败)
这里有个隐藏问题:环境变量的存储位置。如果放在SPI Flash上,读取速度通常只有几MB/s。一个4KB的环境变量块,读出来就要几毫秒。但如果你的环境变量很大(比如包含了大量bootargs),或者存储设备驱动没优化,这个时间会翻倍。
CONFIG_ENV_IS_IN_FAT或CONFIG_ENV_IS_IN_MMC里,利用文件系统缓存。或者干脆把环境变量编译进U-Boot镜像(CONFIG_ENV_IS_NOWHERE),省掉读取时间。
1.9 main_loop:等待命令
所有初始化完成后,U-Boot进入main_loop()。这个函数在common/main.c里,核心逻辑是:
/* common/main.c */
void main_loop(void)
{
/* 处理bootdelay */
if (bootdelay >= 0) {
/* 倒计时,等待用户按键 */
s = readline("Hit any key to stop autoboot: ");
}
/* 执行bootcmd */
if (autoboot) {
run_command_list(bootcmd, -1, 0);
}
/* 进入交互模式 */
for (;;) {
readline("=> ");
run_command(lastcommand, 0);
}
}
这里有一个性能点:bootdelay。默认是3秒,很多产品根本不需要用户干预。我建议你根据产品需求,把CONFIG_BOOTDELAY设为0或-1。如果必须保留倒计时,至少把提示信息精简一下,别在倒计时里打印大段文字。
1.10 各阶段性能瓶颈总结
我把整个流程的瓶颈点整理成一张表,方便你对照排查:
| 阶段 | 瓶颈点 | 典型耗时 | 优化方向 |
|---|---|---|---|
| Reset向量 | Flash读取速度、CPU启动模式 | <1ms | 检查Flash等待周期配置 |
| 汇编初始化 | BSS清零、栈初始化 | 1-10ms | 使用DCache ZVA指令 |
| board_init_f | DDR训练、串口打印 | 50-500ms | 裁剪调试信息、优化DDR参数 |
| 重定位 | 镜像复制速度 | 10-100ms | 缩小镜像体积、使用DMA复制 |
| board_init_r | 环境变量加载、设备探测 | 100-1000ms | 优化存储驱动、裁剪设备树 |
| 设备初始化 | PHY自协商、MMC识别、I2C超时 | 100-1500ms | 固定配置、减少重试、异步探测 |
| 环境变量加载 | 存储读取速度、CRC校验 | 10-100ms | 内嵌环境变量、使用快速存储 |
| main_loop | bootdelay等待 | 0-3000ms | 关闭或缩短bootdelay |
你看,整个启动流程里,设备初始化和DDR训练占了绝大部分时间。后面的章节,我会针对每个瓶颈点给出具体的优化方案。但在此之前,我建议你先在自己的板子上跑一遍bootstage命令,看看实际耗时分布——没有数据,优化就是瞎蒙。