1. 课程导入

做嵌入式开发的朋友,十有八九都跟U-Boot打过交道。但说实话,很多人对它的印象就是「一段跑完就跳进内核的代码」。我以前也这么想,直到有一次产品启动慢了200ms,被客户追着问——这才逼着我一条一条指令去抠启动流程。

这一章,我们就从CPU上电后的第一条指令开始,一路走到main_loop。我会把每个阶段的性能瓶颈点都指出来。嗯,这些坑我基本都踩过。

1.1 整体启动流程一览

先看一张总图,把整个路径串起来。你心里有个谱,后面拆开讲才不会乱。

U-Boot启动流程总览 U-Boot 启动流程总览 Reset向量 汇编初始化 board_init_f 重定位 board_init_r 设备初始化 环境变量加载 main_loop 💡 红色阶段(board_init_f / board_init_r)是C环境初始化核心,也是性能瓶颈高发区。 橙色阶段(设备初始化)往往因为轮询等待和无效探测拖慢整体启动。

整个流程可以分成两大段: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指令从文字池加载地址可能会触发异常。我见过有人把启动代码从别的平台直接搬过来,第一条指令就挂了,查了半天才发现是这个问题。

⚠️ 注意: 某些ARM Cortex-A系列CPU在复位后,MMU和Cache默认是关闭的。这意味着第一条指令的取指直接走AHB总线,速度极慢。如果你的Flash接口有等待周期,这里就已经开始「慢」了。

1.3 汇编初始化:关中断、设栈、清BSS

进入reset标签后,U-Boot会做几件关键的事:

  1. 切换到SVC模式,关掉所有中断
  2. 设置CPU频率(有些平台在这里就做,有些放到后面)
  3. 初始化栈指针——注意,此时栈可能还在SRAM里,空间很有限
  4. 清零BSS段
  5. 调用_main,进入C环境

这段代码全部是汇编,执行速度很快。但有一个性能陷阱:BSS清零。如果BSS段很大(比如你定义了一个全局大数组),逐字节清零会消耗不少时间。我建议你编译完后用objdump看一下BSS大小,超过几百KB就要考虑优化了。

💡 优化技巧: 如果SoC支持DCache ZVA指令(清零虚拟地址),可以用它代替逐字清零。ARMv7及以上架构都支持,能快一个数量级。

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,看看实际镜像大小,别留太多冗余空间。

🔑 关键点: 重定位完成后,U-Boot会跳转到DRAM中的新地址继续执行。此时所有全局变量和函数指针都必须重新定位。如果某个驱动用了绝对地址(比如直接操作寄存器地址的宏),重定位后不会受影响;但如果用了全局变量指针,就要小心了。

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的初始化流程是:

  1. 绑定阶段:根据设备树(DTB)创建所有udevice实例
  2. 探测阶段:按依赖关系依次调用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命令,看看实际耗时分布——没有数据,优化就是瞎蒙。