4、U-Boot启动流程第二阶段:board_init_f到board_init_r的C语言初始化
上一章我们聊完了汇编阶段的“热身运动”,说白了就是给CPU把基本模式设好、把异常向量表挂上。但真正的重头戏,是从board_init_f这个C函数开始的。
我个人习惯把这一阶段称为“从裸机到半成品”的蜕变。为什么这么说?因为此时DRAM还没完全初始化好,全局变量还不能随便用,连BSS段都没清零。你想想看,一个C函数在这种环境下跑,是不是有点“裸奔”的感觉?
4.1 board_init_f:在“废墟”上搭脚手架
board_init_f这个函数,名字里的“f”代表“first”。它干的事情很纯粹:在DRAM还没完全就绪之前,把必要的硬件初始化做完,然后给自己找一个能用的临时栈空间。
我刚开始看这段代码时有个疑惑:为什么不在汇编阶段就把DRAM初始化好?后来在项目里踩过一次坑才明白——有些板子的DRAM初始化需要读取环境变量,而环境变量又存在存储设备上,存储设备的驱动又依赖中断和定时器……这就成了死循环。所以board_init_f只做最基础的事情。
board_init_f的核心任务清单:
- 初始化串口(让你能看到打印信息)
- 初始化定时器(为延时和超时打基础)
- 初始化GPIO(板级控制的基本单元)
- 初始化DRAM控制器(但此时DRAM可能还没完全可用)
- 计算并分配后续使用的内存布局
这里有个关键点:board_init_f执行时,全局变量(GD)是放在寄存器或者某个固定地址上的,而不是放在BSS段。因为BSS段还没清零,你敢用全局变量,它就敢给你随机值。
// board_init_f 的典型调用链(简化版)
void board_init_f(ulong boot_flags)
{
gd = &gdata; // GD指针指向一个临时结构体
// 阶段1:板级基础初始化
board_early_init_f();
timer_init();
serial_init();
// 阶段2:DRAM初始化
dram_init(); // 获取DRAM大小信息
// 阶段3:计算内存布局
setup_dest_addr();
reserve_global_data();
reserve_malloc();
reserve_board();
reserve_video();
reserve_trace();
reserve_uboot();
reserve_mmu();
// 阶段4:跳转到board_init_r
relocate_code(gd->relocaddr, &gdata, gd->start_addr_sp);
}
注意看最后一行relocate_code,这是个汇编函数。它负责把U-Boot自身从Flash或ROM拷贝到DRAM中。为什么要做这一步?因为Flash的读取速度太慢了,U-Boot在DRAM里跑才能达到正常速度。我在一个NAND Flash启动的项目里,没做重定位前,U-Boot启动要花8秒,做完重定位后直接降到1.2秒——差距就是这么明显。
4.2 内存布局的“精打细算”
board_init_f里最烧脑的部分,就是内存布局的计算。说白了,就是在一片连续的DRAM空间里,把U-Boot自身、堆、栈、设备树、MMU页表等所有东西都安排好位置。
我画了一张图,帮你理清这个分配顺序:
这个分配顺序是有讲究的。你看,GD在最前面,因为它是整个U-Boot的“大脑”,所有全局信息都靠它传递。然后是堆空间,因为后续的malloc操作需要它。U-Boot自身放在中间偏后的位置,这样重定位后代码段和数据段能连续。栈放在最后,从高地址往低地址长——这是ARM处理器的栈生长方向。
我的经验: 调试内存布局问题时,最有效的办法就是在board_init_f里加打印,把每个预留区域的起始地址和大小都打出来。我曾经在一个Cortex-A7平台上遇到过栈溢出覆盖了U-Boot代码段的问题,就是靠这种“土办法”定位到的。
4.3 relocate_code:U-Boot的“搬家”过程
内存布局算好了,接下来就是真正的搬家——relocate_code。这个函数是汇编写的,因为它要处理代码拷贝和链接地址修正,这些操作在C语言里很难安全地完成。
搬家的核心逻辑其实不复杂:
- 拷贝:把U-Boot从Flash/ROM拷贝到DRAM中预留的位置
- 修正:更新所有绝对地址引用(因为代码在DRAM里的地址和Flash里不一样)
- 跳转:跳转到DRAM中的新地址继续执行
这里有个概念叫“重定位偏移量”(relocation offset),计算公式很简单:
relocation_offset = gd->relocaddr - CONFIG_SYS_TEXT_BASE
其中CONFIG_SYS_TEXT_BASE是U-Boot在链接时指定的基地址,gd->relocaddr是运行时实际拷贝到的地址。两者的差值,就是所有绝对地址需要调整的量。
注意: 重定位完成后,原来的代码段还在Flash里。如果你在重定位后还去读Flash里的代码,拿到的就是旧数据。我见过一个坑:有人在重定位后还用Flash地址调用函数,结果程序直接跑飞。正确的做法是,重定位后所有函数调用都要通过DRAM中的新地址。
4.4 board_init_r:进入“正常世界”
重定位完成后,U-Boot跳转到board_init_r。这个“r”代表“relocated”或“real”——总之,从现在开始,U-Boot在DRAM里运行,BSS段已经清零,全局变量可以正常使用了。
board_init_r做的事情比board_init_f多得多:
| 初始化项 | 说明 | 我踩过的坑 |
|---|---|---|
| 中断控制器 | 使能中断,注册中断服务 | 中断优先级没配好,导致低优先级中断一直响应不了 |
| 环境变量 | 从存储设备加载环境变量 | 环境变量校验和错误,U-Boot直接卡死在加载阶段 |
| 设备驱动 | 初始化网卡、存储、USB等 | 网卡PHY初始化时序不对,要加延时才能稳定 |
| 控制台 | 设置标准输入输出 | 串口波特率配错,打印全是乱码 |
| 命令解析器 | 初始化shell环境 | 命令表没注册,自定义命令敲了没反应 |
我个人觉得,board_init_r最值得关注的是它的初始化顺序。U-Boot用了一个init_sequence_r数组来管理所有初始化函数,每个函数执行完后检查返回值,失败就打印错误并停止启动。
// init_sequence_r 的典型片段
static init_fnc_t init_sequence_r[] = {
initr_trace,
initr_reloc, // 确认重定位完成
initr_caches, // 使能缓存
initr_malloc, // 初始化堆管理
initr_dm, // 驱动模型初始化
initr_env, // 加载环境变量
initr_serial, // 串口重初始化
initr_console_record,
initr_net, // 网络子系统初始化
initr_post, // 上电自检
NULL,
};
这个数组的顺序不是随便排的。比如initr_caches必须在initr_malloc之前,因为缓存使能后,malloc的性能才能上来。再比如initr_env必须在initr_net之前,因为网络配置可能要从环境变量里读取。
关键理解: board_init_f和board_init_r的分界线,就是重定位。重定位之前,U-Boot在Flash里“苟且偷生”;重定位之后,U-Boot在DRAM里“大展拳脚”。这个分界线的存在,让U-Boot既能从Flash启动,又能享受DRAM的高速。
4.5 从board_init_f到board_init_r的完整流程
最后,我把整个第二阶段的流程串起来,给你一个全景图:
嗯,到这里,U-Boot的第二阶段就讲完了。从board_init_f的“裸奔”到board_init_r的“全副武装”,中间经历了内存布局计算、代码重定位、BSS清零等一系列关键步骤。理解了这个过程,你就掌握了U-Boot启动的核心脉络。