U-Boot 启动流程:入口函数、board_init_f、board_init_r、main_loop 的执行顺序
U-Boot 的启动流程,说白了就是一条从汇编到 C、从初始化到交互的清晰路径。我刚开始啃 U-Boot 源码时,最头疼的就是搞不清这些函数谁先谁后、各自干了什么。今天咱们就把这条线捋清楚。
核心脉络:U-Boot 启动分为两个阶段——第一阶段(汇编 + board_init_f)负责把硬件拉到能跑 C 代码的程度;第二阶段(board_init_r + main_loop)完成完整外设初始化,最后进入命令行循环。
4.1 入口函数:从汇编到 C 的第一跳
U-Boot 的入口在 arch/arm/lib/vectors.S(ARM 架构为例),复位向量直接跳转到 _start。这里全是汇编,干三件事:
- 设置 CPU 为 SVC 模式,关中断
- 初始化栈指针(SP),为调用 C 函数做准备
- 清零 BSS 段
然后调用 board_init_f。嗯,这里有个细节——board_init_f 虽然是 C 函数,但此时 DDR 还没完全初始化,代码还在 SRAM 或 ROM 里跑。我当年调试一块新板子时,发现 board_init_f 里一访问全局变量就死机,后来才意识到是 BSS 段没清干净,栈指针也指歪了。
个人经验:如果你在移植 U-Boot 时卡在 board_init_f 之前,先检查链接脚本中 BSS 段的起始地址和长度是否与汇编代码中的符号一致。我曾经因为链接脚本里 .bss 段对齐方式写错,导致清零范围少了 4 字节,结果某个全局变量一直是个随机值,折腾了两天。
4.2 board_init_f:早期初始化,在 SRAM 里跳舞
board_init_f 在 common/board_f.c 中实现。它运行在一个临时环境中——栈在 SRAM 或 Cache 作为 RAM(CORTEX-A 系列常用),DDR 可能还没完全可用。
这个函数的核心工作可以列成一张表:
| 初始化项 | 说明 | 我踩过的坑 |
|---|---|---|
| 串口初始化 | 输出第一个打印信息 | 波特率分频算错,输出全是乱码 |
| 时钟系统 | 设置 CPU、总线、外设时钟 | PLL 锁定时间不够,系统跑飞 |
| DRAM 初始化 | 检测并初始化 DDR 控制器 | 时序参数配错,DDR 校验失败 |
| 板级识别 | 读取硬件版本、配置引脚 | GPIO 上拉没配,检测结果错误 |
| relocate 地址计算 | 计算 U-Boot 自身要拷贝到 DDR 的目标地址 | 地址重叠,拷贝后代码跑飞 |
执行完这些后,board_init_f 会调用 relocate_code,把 U-Boot 自身从 SRAM 拷贝到 DDR 中。然后跳转到 DDR 里的 board_init_r。
注意:board_init_f 结束时,栈指针会切换到 DDR 空间。如果 DDR 初始化失败,这一步直接死机。我建议你在 board_init_f 开头加一个串口打印,确认至少串口是活的,否则出了问题连调试信息都看不到。
4.3 board_init_r:完整初始化,在 DDR 里安家
board_init_r 在 common/board_r.c 中。此时 U-Boot 已经运行在 DDR 里,栈空间充足,可以初始化所有外设了。
这个函数做的事情更多,我挑几个关键的:
- 中断控制器初始化——使能系统级中断
- 定时器初始化——提供时间基准,用于延时和超时
- 环境变量加载——从存储设备(SPI Flash、MMC、NAND)读取环境变量
- 设备树支持——如果有 FDT,会解析并应用
- 标准设备初始化——网卡、存储控制器、USB 等
你想想看,board_init_r 里有一个初始化函数列表(init_sequence_r),按顺序逐个调用。我遇到过一个问题:某个驱动在 board_init_r 里注册时依赖另一个驱动先完成,但顺序没排对,结果注册失败。后来我手动调整了 init_sequence_r 的顺序才解决。
关键点:board_init_r 的最后一步是调用 main_loop。如果 main_loop 返回了,U-Boot 会重启系统。所以 main_loop 本质上是一个无限循环。
4.4 main_loop:命令行交互与自动启动
main_loop 在 common/main.c 中。它的逻辑其实很简单:
- 检查环境变量
bootdelay,等待用户按键中断自动启动 - 如果用户按下任意键,进入命令行(Shell)
- 如果超时,执行
bootcmd环境变量中的命令(通常是启动内核)
命令行部分是一个解析循环:读取输入、解析命令、查找命令表、执行对应函数。U-Boot 的命令表是一个链表,每个命令通过 U_BOOT_CMD 宏注册。
我习惯在 main_loop 里加一个自定义的启动计数打印,方便看板子重启了多少次。调试时特别有用。
避坑指南:我曾经在 main_loop 里遇到一个诡异问题——输入命令后没反应。查了半天发现是串口接收中断和轮询模式冲突了。U-Boot 默认用轮询读串口,但某个驱动把串口中断打开了,导致轮询读不到数据。解决办法是在 main_loop 入口强制关闭串口中断。
4.5 整体流程可视化
下面这张图把整个启动流程串起来了,我建议你对照源码看:
4.6 执行顺序总结
我把整个流程浓缩成一句话:
_start → board_init_f → relocate_code → board_init_r → main_loop
其中 board_init_f 和 board_init_r 是 C 语言层面的两个核心阶段,前者在 SRAM 里做最小初始化,后者在 DDR 里做完整初始化。你想想看,这种两阶段设计其实很巧妙——先用少量代码把 DDR 跑起来,再借助 DDR 的大空间做更多事情。
我的建议:如果你在移植 U-Boot 到新板子,先确保 board_init_f 里的串口和 DDR 初始化正确。只要这两个通了,后面的问题都可以通过串口打印定位。我每次移植新板子,都会在 board_init_f 和 board_init_r 的入口各加一个打印,确认两个阶段都跑到了。
至于 main_loop,它其实是一个无限循环。如果它退出了,U-Boot 会调用 hang() 或重启。所以正常情况下,你看到的 U-Boot 命令行就是 main_loop 在干活。