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页表等所有东西都安排好位置。

我画了一张图,帮你理清这个分配顺序:

board_init_f 内存布局分配流程 DRAM起始地址 (gd->ram_base) ① 预留 Global Data 结构体 (GD) ② 预留 malloc 堆空间 ③ 预留 Board Info 结构体 ④ 预留 U-Boot 代码段 + 数据段 ⑤ 预留 MMU 页表 (TLB) ⑥ 栈空间 (从高地址向低地址生长)

这个分配顺序是有讲究的。你看,GD在最前面,因为它是整个U-Boot的“大脑”,所有全局信息都靠它传递。然后是堆空间,因为后续的malloc操作需要它。U-Boot自身放在中间偏后的位置,这样重定位后代码段和数据段能连续。栈放在最后,从高地址往低地址长——这是ARM处理器的栈生长方向。

我的经验: 调试内存布局问题时,最有效的办法就是在board_init_f里加打印,把每个预留区域的起始地址和大小都打出来。我曾经在一个Cortex-A7平台上遇到过栈溢出覆盖了U-Boot代码段的问题,就是靠这种“土办法”定位到的。

4.3 relocate_code:U-Boot的“搬家”过程

内存布局算好了,接下来就是真正的搬家——relocate_code。这个函数是汇编写的,因为它要处理代码拷贝和链接地址修正,这些操作在C语言里很难安全地完成。

搬家的核心逻辑其实不复杂:

  1. 拷贝:把U-Boot从Flash/ROM拷贝到DRAM中预留的位置
  2. 修正:更新所有绝对地址引用(因为代码在DRAM里的地址和Flash里不一样)
  3. 跳转:跳转到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的完整流程

最后,我把整个第二阶段的流程串起来,给你一个全景图:

第二阶段完整流程:board_init_f → board_init_r board_init_f 执行 初始化串口/定时器/GPIO,计算内存布局 relocate_code 执行 拷贝U-Boot到DRAM,修正链接地址,跳转到新地址 BSS段清零 + 栈切换 清空BSS段,切换到DRAM中的新栈 board_init_r 执行 初始化中断/环境变量/驱动/控制台/命令解析器 进入主循环 (main_loop) 等待用户输入命令,或自动启动内核

嗯,到这里,U-Boot的第二阶段就讲完了。从board_init_f的“裸奔”到board_init_r的“全副武装”,中间经历了内存布局计算、代码重定位、BSS清零等一系列关键步骤。理解了这个过程,你就掌握了U-Boot启动的核心脉络。