4、U-Boot启动流程分析:从reset向量到main_loop的完整链路
U-Boot的启动流程,说白了就是一段从汇编到C、从板级初始化到命令行交互的完整旅程。很多初学者一上来就看start.S,结果被各种宏和条件编译绕晕了。我刚开始做移植那会儿也是这样,后来花了整整两天,一行一行地跟代码,才把这条链路理清楚。
今天我们就沿着这条主线走一遍。你不需要记住每一行代码,但要把关键节点和跳转逻辑刻在脑子里。
4.1 第一站:reset向量——CPU的第一口气
CPU上电或复位后,PC指针会跳到一个固定的地址。对于ARM芯片,通常是0x00000000或0xFFFF0000。U-Boot的链接脚本会把start.S中的_start标签放在这个位置。
/* arch/arm/lib/vectors.S 或 start.S */
_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可能还没初始化DDR,只能在SRAM或ROM里跑。相对跳转指令不依赖绝对地址,更安全。我在一个项目中遇到过有人把这里改成绝对跳转,结果板子直接变砖——嗯,从那以后我再也不敢乱改向量表了。
4.2 第二站:reset函数——从异常模式到SVC模式
跳转到reset标签后,U-Boot做的第一件事是切换处理器模式。ARM在复位后默认处于SVC模式,但有些调试器或BootROM可能会把它留在其他模式。所以代码里会强制设置CPSR寄存器:
reset:
/* 设置SVC模式,关闭IRQ/FIQ */
mrs r0, cpsr
bic r0, r0, #0x1f
orr r0, r0, #0xd3
msr cpsr, r0
紧接着是关闭MMU和Cache。为什么?因为此时页表还没建立,打开MMU会直接触发异常。我见过有人为了加速启动,在SPL里提前开了Cache,结果数据不一致导致校验失败——折腾了两天才找到原因。
| 寄存器/功能 | 初始值 | 说明 |
|---|---|---|
| CPSR | 0xD3 | SVC模式,IRQ/FIQ关闭 |
| SCTLR | MMU=0, D-Cache=0, I-Cache=0 | 关闭内存管理单元和缓存 |
| SP | 指向SRAM顶部 | 临时栈,DDR未初始化 |
4.3 第三站:lowlevel_init——板级最底层的初始化
设置好基本运行环境后,U-Boot会调用lowlevel_init。这个函数通常由板级代码实现,负责初始化PLL、时钟、DDR控制器等。说白了,就是让CPU能跑起来、让内存能用。
ENTRY(lowlevel_init)
/* 初始化系统时钟 */
bl system_clock_init
/* 初始化DDR控制器 */
bl ddr_init
/* 返回 */
mov pc, lr
ENDPROC(lowlevel_init)
我个人习惯在这里加一个LED闪烁函数,用来确认代码是否跑到了这个位置。调试裸机程序时,串口可能还没初始化,LED就是唯一的“printf”。
4.4 第四站:_main——从汇编到C的桥梁
完成底层初始化后,U-Boot会跳转到_main。这个函数是汇编到C的过渡,它做了三件关键的事:
- 设置C运行环境:初始化栈指针、BSS段清零
- 调用board_init_f:在SRAM中完成早期板级初始化
- 重定位:将U-Boot自身从SRAM拷贝到DDR
ENTRY(_main)
/* 设置初始栈 */
ldr sp, =(CONFIG_SYS_INIT_SP_ADDR)
bic sp, sp, #7 /* 8字节对齐 */
/* 清零BSS段 */
ldr r0, =__bss_start
ldr r1, =__bss_end
mov r2, #0
1: cmp r0, r1
strlo r2, [r0], #4
blo 1b
/* 调用board_init_f */
bl board_init_f
/* ... 后续重定位逻辑 ... */
ENDPROC(_main)
你想想看,为什么BSS清零这么重要?因为C语言中未初始化的全局变量默认是0,如果BSS段没清零,这些变量的值就是随机的。我遇到过一块板子,每次启动网络都不正常,最后发现是BSS段里的MAC地址缓存没清零,导致随机值被当成有效地址。
4.5 第五站:board_init_f——板级早期初始化
board_init_f是一个函数指针数组的遍历过程。U-Boot定义了一个init_sequence_f数组,里面按顺序放了几十个初始化函数:
static init_fnc_t init_sequence_f[] = {
setup_machine, /* 设置机器类型 */
env_init, /* 环境变量初始化 */
init_baud_rate, /* 设置波特率 */
serial_init, /* 串口初始化 */
console_init_f, /* 控制台早期初始化 */
dram_init, /* 检测DDR大小 */
setup_dest_addr, /* 计算重定位地址 */
/* ... 更多初始化项 ... */
NULL,
};
这个数组的执行顺序非常讲究。比如serial_init必须在console_init_f之前,否则printf打不出字。我建议你在调试时,可以在每个函数入口加一个打印标记,这样能精确定位是哪个初始化步骤卡住了。
4.6 第六站:重定位——从SRAM搬到DDR
当board_init_f执行完毕后,U-Boot已经知道了DDR的大小和地址。接下来就是重定位——把U-Boot自身从SRAM拷贝到DDR的高地址区域。
/* relocate_code 是汇编实现的 */
ENTRY(relocate_code)
ldr r1, =__image_copy_start /* 源地址:SRAM */
ldr r2, =__image_copy_end /* 结束地址 */
ldr r3, =CONFIG_SYS_TEXT_BASE /* 目标地址:DDR */
1: ldmia r1!, {r10-r11} /* 从SRAM加载 */
stmia r3!, {r10-r11} /* 写入DDR */
cmp r1, r2
blo 1b
/* 修复GOT表 */
bl relocate_vectors
/* 跳转到DDR中的board_init_r */
ldr pc, =board_init_r
ENDPROC(relocate_code)
这里有个关键点:重定位后必须修复GOT表(全局偏移表),否则函数调用会跳到错误的地址。我记得第一次移植U-Boot时,重定位后板子直接死机,查了两天才发现是GOT表没更新——从那以后,我每次改链接脚本都会仔细检查GOT段。
4.7 第七站:board_init_r——完整的板级初始化
跳转到DDR中的board_init_r后,U-Boot才算真正“活”过来了。这个函数同样遍历一个初始化数组,但这次做的事情更全面:
- 完整的外设初始化:网卡、Flash、I2C、SPI等
- 环境变量加载:从存储设备读取env
- 设备驱动绑定:DM框架的设备树解析
- 命令行初始化:注册所有命令
static init_fnc_t init_sequence_r[] = {
initr_reloc, /* 重定位完成标记 */
initr_caches, /* 开启Cache */
initr_env, /* 环境变量初始化 */
initr_dm, /* 驱动模型初始化 */
initr_net, /* 网络初始化 */
/* ... */
NULL,
};
4.8 终点站:main_loop——命令行的世界
所有初始化完成后,U-Boot进入main_loop。这是启动流程的终点,也是用户交互的起点。
void main_loop(void)
{
const char *s;
/* 检查启动延迟 */
bootdelay_process();
/* 处理预置命令 */
s = getenv("bootcmd");
if (s != NULL)
run_command_list(s, -1, 0);
/* 进入命令行循环 */
for (;;) {
len = readline(CONFIG_SYS_PROMPT);
if (len > 0)
run_command(line, 0);
}
}
这里有个常见的坑:bootdelay_process会等待用户按键中断自动启动。如果串口线接触不良,或者终端软件没有正确发送回车,U-Boot会一直卡在等待状态。我建议在产品发布时,把CONFIG_BOOTDELAY设为0,或者直接让bootcmd自动执行,避免生产测试时卡住。
4.9 完整链路总览
为了让你更直观地理解整个流程,我画了一张流程图。你可以把它当作地图,以后看代码时随时对照。
4.10 避坑总结
最后,我把这些年踩过的坑整理成一张表,你移植时可以直接对照排查:
| 阶段 | 常见问题 | 排查方法 |
|---|---|---|
| reset向量 | 链接地址与实际加载地址不一致 | 检查链接脚本和CONFIG_SYS_TEXT_BASE |
| lowlevel_init | DDR参数配置错误导致死机 | 用示波器量DDR时钟和片选信号 |
| board_init_f | 栈溢出导致函数返回异常 | 在回调函数入口打印SP值 |
| 重定位 | GOT表未修复,函数调用跑飞 | 反汇编检查跳转地址是否正确 |
| board_init_r | Cache一致性导致外设数据错误 | 临时关闭Cache确认问题 |
| main_loop | 串口无输出或无法输入 | 检查波特率、引脚复用、终端软件 |
这条链路,我前前后后走了不下五十遍。每次移植新板子,我都会对照这个流程过一遍。你只要把每个阶段的核心任务记住,遇到问题就知道该去哪个文件里找答案。