第4章 U-Boot启动流程详解:第一阶段(汇编)与第二阶段(C语言)
U-Boot的启动流程,说白了就是一段从汇编到C语言的“接力跑”。很多初学者一上来就被start.S吓住了,觉得汇编代码又长又乱。其实你把它拆成两个阶段来看,思路就清晰了。
我个人习惯把第一阶段叫做“硬件初始化接力棒”,第二阶段叫做“板级配置大本营”。今天我们就顺着这条线,把U-Boot怎么从CPU上电一路跑到命令行交互的完整路径走一遍。
4.1 第一阶段(汇编):从复位向量到DDR就绪
CPU上电后,PC指针直接跳到复位向量地址。对于ARM芯片来说,这个地址通常是0x00000000或者0xFFFF0000。U-Boot的start.S就在这里等着。
我记得第一次移植U-Boot到一块新板子时,板子死活不跑。折腾了两天,最后发现是异常向量表没对齐——ARM要求向量表必须4KB对齐,我偏偏少写了一个.align指令。嗯,从那以后我每次看start.S都会先检查对齐。
4.1.1 第一阶段核心任务清单
| 步骤 | 代码位置 | 做什么 | 为什么重要 |
|---|---|---|---|
| 1 | arch/arm/cpu/armv7/start.S | 设置异常向量表 | CPU复位后第一条指令入口 |
| 2 | arch/arm/cpu/armv7/start.S | 切换到SVC模式,关中断 | 确保CPU处于特权模式,不受干扰 |
| 3 | arch/arm/cpu/armv7/lowlevel_init.S | 初始化PLL、时钟树 | CPU和总线必须跑在正确频率 |
| 4 | board/vendor/board/lowlevel_init.S | 初始化DDR控制器 | 没有DDR,代码无处安放 |
| 5 | arch/arm/lib/crt0.S | 初始化串口(调试用) | 没有串口,你连板子在干啥都不知道 |
| 6 | arch/arm/lib/crt0.S | 重定位U-Boot自身到DDR | 从Flash/ROM拷贝到DDR,提升执行速度 |
| 7 | arch/arm/lib/crt0.S | 设置堆栈指针SP | 为跳转到C语言做准备 |
4.1.2 关键代码片段解析
我们来看start.S里最核心的一段——设置异常向量表并跳转到lowlevel_init:
/*
* 异常向量表 —— 必须放在起始地址,4KB对齐
*/
_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 @ IRQ中断
ldr pc, _fiq @ FIQ快速中断
reset:
/* 设置CPU为SVC模式,关掉所有中断 */
mrs r0, cpsr
bic r0, r0, #0x1f
orr r0, r0, #0xd3
msr cpsr, r0
/* 关闭MMU和Cache —— 刚上电时地址空间还没映射好 */
mrc p15, 0, r0, c1, c0, 0
bic r0, r0, #0x0005 @ 关MMU和DCache
bic r0, r0, #0x1000 @ 关ICache
mcr p15, 0, r0, c1, c0, 0
/* 跳转到lowlevel_init,初始化时钟和DDR */
bl lowlevel_init
/* 重定位U-Boot到DDR */
bl _main
你注意看,这里有个细节:bl lowlevel_init 用的是相对跳转,所以不管U-Boot当前在Flash还是SRAM里,这条指令都能正确执行。我见过有人把这里改成绝对跳转,结果板子直接变砖——因为Flash地址和DDR地址根本不在一个段上。
4.2 第二阶段(C语言):board_init_f 到 board_init_r
汇编阶段把DDR、时钟、串口这些“基础设施”搭好后,就该C语言登场了。入口函数是 board_init_f(),它在DDR里运行,负责初始化板级外设。
你想想看,为什么要有 board_init_f 和 board_init_r 两个函数?其实这是U-Boot的一个设计智慧:board_init_f 在“临时环境”下运行(可能还在SRAM里),只做最必要的初始化;board_init_r 在“完整环境”下运行(DDR已经就绪,堆栈也够大),可以做所有事情。
4.2.1 board_init_f 做了什么
- 初始化串口 —— 让你能看到打印信息
- 初始化定时器 —— 为延时和超时提供基础
- 初始化GPIO —— 控制板上的LED、复位等
- 初始化I2C/SPI —— 访问板上的EEPROM、传感器
- 初始化MMC/SD卡 —— 为加载内核做准备
- 设置环境变量存储位置 —— 决定env存在Flash还是EEPROM
4.2.2 board_init_r 的完整初始化流程
board_init_r 是U-Boot第二阶段的核心,它调用 init_sequence_r[] 数组里的所有初始化函数。这个数组定义在 common/board_r.c 中,我列几个关键项:
| 初始化函数 | 作用 | 依赖 |
|---|---|---|
| initr_trace | 初始化调试跟踪 | 无 |
| initr_reloc | 完成重定位后的收尾工作 | DDR就绪 |
| initr_malloc | 初始化动态内存分配池 | DDR就绪 |
| initr_dm | 初始化驱动模型(Driver Model) | malloc就绪 |
| initr_env | 加载环境变量(从Flash/EEPROM) | I2C/SPI就绪 |
| initr_net | 初始化网络协议栈 | DM就绪 |
| initr_cmd | 注册所有命令 | DM就绪 |
| run_main_loop | 进入命令行循环 | 以上全部完成 |
这里有个关键点:initr_dm 必须在 initr_env 之前执行。为什么?因为驱动模型初始化后,才能正确识别板上的设备树(Device Tree),而环境变量可能依赖设备树来定位存储位置。顺序错了,环境变量就加载失败。
4.3 main_loop:命令行交互的“心脏”
所有初始化完成后,U-Boot进入 main_loop()。这个函数在 common/main.c 里,说白了就是一个死循环:等待用户输入、解析命令、执行命令、再等待。
main_loop的核心逻辑其实很简单:
void main_loop(void)
{
const char *s;
int bootdelay;
/* 检查是否有自动启动命令(bootcmd) */
s = env_get("bootcmd");
bootdelay = env_get_ulong("bootdelay", 10, CONFIG_BOOTDELAY);
if (s != NULL && bootdelay >= 0) {
/* 倒计时,等待用户按键中断自动启动 */
if (autoboot_command(s)) {
/* 用户没按键,执行bootcmd启动内核 */
run_command_list(s, -1, 0);
}
}
/* 进入命令行循环 */
for (;;) {
/* 读取用户输入 */
len = cli_readline(CONFIG_SYS_PROMPT);
/* 解析并执行命令 */
if (len > 0)
run_command(lastcommand, 0);
}
}
你注意看那个 autoboot_command() 函数。它会在倒计时期间不断检查串口是否有输入。如果用户按下任意键,倒计时中断,进入命令行模式;如果倒计时结束,自动执行 bootcmd 环境变量里定义的命令(通常是 bootm 启动内核)。
4.4 三个阶段的数据流关系
为了让你更直观地理解三个阶段怎么衔接,我画了一张数据流图:
4.5 实战经验:如何快速定位启动失败
在实际项目中,U-Boot启动失败是最常见的“劝退”场景。我总结了一套排查思路:
- 看串口有没有输出 —— 如果完全没有打印,问题大概率在汇编阶段:时钟没配好、DDR没初始化、或者串口引脚配置错了。
- 看输出到哪一行卡住 —— 如果打印了“U-Boot 2023.04”但卡在“DRAM:”后面,说明DDR初始化有问题。如果卡在“Net:”后面,说明网络驱动有问题。
- 看能不能进命令行 —— 如果能进命令行但执行命令报错,检查环境变量和设备树是否加载正确。
- 看bootm能不能启动内核 —— 如果bootm执行后卡死,检查内核镜像格式、设备树地址、以及内核入口地址是否匹配。