4、U-Boot启动流程(下):从SPL到U-Boot Proper、board_init_f与board_init_r详解

上一章我们聊了SPL是怎么被唤醒的。今天接着往下走——SPL把接力棒交给U-Boot Proper,然后Proper自己还要经历两段截然不同的初始化旅程:board_init_f 和 board_init_r。

说白了,这俩函数的名字就差一个字母,但干的活天差地别。一个在Flash里跑,一个在DDR里跑。一个连malloc都用不了,一个可以随便分配内存。我当年刚接触U-Boot时,就被这俩函数搞晕过——明明都叫board_init,为什么一个在前面一个在后面?

核心要点:SPL → U-Boot Proper 的跳转,本质上是“从受限环境到完整环境”的过渡。board_init_f 负责在Flash里完成早期硬件初始化并规划DDR布局,board_init_r 则在DDR里完成所有后续初始化并进入命令行。

4.1 SPL到U-Boot Proper的跳转机制

SPL的任务很简单:初始化最基本的硬件(时钟、串口、DDR),然后把U-Boot Proper从存储介质加载到DDR里,最后跳过去。

跳转的入口在哪里?看代码:

// common/spl/spl.c
void spl_load_image(struct spl_image_info *spl_image)
{
    // ... 加载U-Boot Proper到DDR ...
    spl_image->entry_point = CONFIG_SYS_TEXT_BASE;
}

void spl_jump_to_image(struct spl_image_info *spl_image)
{
    typedef void __noreturn (*image_entry_t)(void);
    image_entry_t image_entry = (image_entry_t)spl_image->entry_point;

    // 清理SPL用过的资源
    spl_cleanup();

    // 跳转!
    image_entry();
}

嗯,这里要注意:spl_cleanup() 会关掉SPL自己使能的中断、重置一些外设状态。我见过一个坑——某款芯片的SPL里开了看门狗,跳转前没关,结果U-Boot Proper刚跑起来就被狗咬了。后来我在 spl_cleanup() 里加了一行关狗代码才解决。

跳转之后,CPU开始执行U-Boot Proper的第一条指令。这时候它还在Flash里(或者SPL已经把它拷贝到DDR里了,取决于配置)。不管哪种情况,board_init_f 都会先登场。

4.2 board_init_f:在Flash里打地基

board_init_f 的全称是 "board initialization, before relocation"。翻译成人话就是:在重定位之前,把板子初始化到能用的程度。

这个阶段有几个关键特征:

  • 运行位置:还在Flash或ROM里,代码是位置无关的(PIC)
  • 内存状态:DDR可能已经初始化了(SPL干的),但U-Boot自己还没用上
  • 堆栈:只能用有限的栈空间,不能malloc
  • 全局数据:用 gd_t 结构体,但只能放在寄存器或SRAM里

board_init_f 的核心工作可以归纳为一张表:

初始化项 说明 我踩过的坑
串口初始化 让控制台能输出信息 波特率算错,输出全是乱码
时钟系统 配置CPU、总线、外设时钟 PLL锁定超时没处理
DDR控制器 如果SPL没做,这里要补上 时序参数配错,系统不定期死机
GD布局规划 计算重定位后gd和malloc区域的位置 对齐没做好,访问异常
Board specific GPIO、电源管理等板级初始化 某个GPIO没拉高,外设不工作

我个人习惯在 board_init_f 里加一个调试打印,输出当前运行地址和gd指针。这样一旦后面出问题,能快速定位是重定位前还是重定位后挂的。

小技巧:在 board_init_f 里用 debug() 宏打印信息。这个宏在DEBUG开启时才输出,不会影响发布版本的启动速度。

4.3 重定位:从Flash搬到DDR

board_init_f 的最后一步,是调用 relocate_code() 把U-Boot自己从Flash搬到DDR里。为什么要搬?因为Flash太慢了,而且有些功能(比如USB、网络)需要大量内存缓冲区,在Flash里根本跑不开。

重定位的流程大致是这样:

  1. board_init_f 计算出DDR里要用的地址:gd->relocaddr
  2. 调用 relocate_code(addr_moni),把代码段、数据段全部拷贝过去
  3. 修正所有绝对地址引用(因为代码挪了位置)
  4. 更新栈指针SP,让它指向DDR里的新栈
  5. 跳转到DDR里的 board_init_r

这里有个关键点:重定位之后,原来的Flash代码就没用了。如果这时候发生异常,你看到的PC指针应该指向DDR地址范围。我曾经调试过一个bug,重定位后PC莫名其妙跳回Flash,查了两天才发现是中断向量表没重定位。

4.4 board_init_r:在DDR里盖大楼

进入 board_init_r 之后,U-Boot才算真正活过来了。这时候它运行在DDR里,有充足的内存空间,可以初始化各种复杂外设。

board_init_r 的初始化清单比 board_init_f 长得多:

  • 堆管理:初始化malloc区域,从此可以动态分配内存
  • 设备模型:枚举所有设备,绑定驱动
  • 存储设备:MMC、NAND、SPI Flash等
  • 网络协议栈:初始化网卡、配置IP
  • 文件系统:挂载FAT、ext4等
  • 环境变量:从存储介质加载env
  • 命令行:启动shell,等待用户输入

看一段典型的 board_init_r 调用链:

// common/board_r.c
void board_init_r(gd_t *new_gd, ulong dest_addr)
{
    // 初始化堆
    initr_malloc();

    // 初始化驱动模型
    initr_dm();

    // 初始化存储设备
    initr_mmc();
    initr_nand();

    // 初始化网络
    initr_net();

    // 加载环境变量
    initr_env();

    // 进入主循环
    main_loop();
}

每个 initr_* 函数都对应一个初始化任务。它们按顺序执行,任何一个失败都会导致对应的功能不可用。我曾经遇到过 initr_env() 加载环境变量时CRC校验失败,结果U-Boot用了默认环境,导致启动参数不对,内核起不来。

警告:board_init_r 里的初始化顺序不能随意调整。比如网络初始化依赖DMA和中断控制器,如果顺序搞反了,网卡驱动会直接挂掉。

4.5 两个阶段的对比总结

我把 board_init_f 和 board_init_r 的差异整理成了一张对比表,方便你快速查阅:

对比维度 board_init_f board_init_r
运行位置 Flash / ROM DDR
代码类型 位置无关(PIC) 位置相关
内存分配 只能用栈,不能malloc 完整的堆管理
初始化范围 最小硬件集 所有外设和协议栈
调试难度 高(没有完整控制台) 低(有命令行)
典型耗时 几十毫秒 几百毫秒到几秒

说白了,board_init_f 是“生存模式”——只求能跑起来、能搬代码。board_init_r 是“正常模式”——所有资源都到位了,可以大展拳脚。

4.6 整体启动流程SVG图

下面这张图把从SPL到U-Boot Proper、再到 board_init_f 和 board_init_r 的完整流程串起来了:

U-Boot 启动流程:从SPL到board_init_r SPL 启动 初始化时钟/串口/DDR 加载U-Boot Proper到DDR U-Boot Proper 入口 board_init_f() 重定位 relocate_code() board_init_r() main_loop() 命令行 SPL:受限环境 Flash运行,最小初始化 board_init_f:过渡阶段 Flash运行,规划DDR布局 重定位:关键转折点 代码从Flash搬到DDR board_init_r:完整环境 DDR运行,所有外设就绪 main_loop:用户交互 等待命令或自动启动内核 关键节点说明 ① SPL完成最小硬件初始化,加载U-Boot Proper到DDR ② board_init_f在Flash中完成早期初始化,规划重定位地址 ③ relocate_code将U-Boot自身从Flash拷贝到DDR并修正地址 ④ board_init_r在DDR中初始化所有外设,进入命令行

4.7 实际项目中的注意事项

最后聊几个实战中容易踩的坑:

  • gd指针丢失:重定位后gd指针指向Flash里的旧结构体,必须更新。我见过有人忘了在汇编里更新gd,结果board_init_r里访问gd全是乱值。
  • 栈溢出:board_init_f阶段栈空间很小(通常几KB),递归调用或大局部变量会直接崩掉。我曾经在board_init_f里定义了一个1KB的数组,结果栈溢出了,查了两天才发现。
  • 外设状态残留:SPL初始化过的外设,跳转到Proper后最好重新初始化一遍。有些芯片的外设寄存器在跳转后状态不确定,不重新初始化会出诡异问题。
  • 环境变量校验:board_init_r加载环境变量时,如果存储介质有坏块或CRC错误,U-Boot会使用默认环境。这时候启动参数可能不对,内核起不来。建议在量产前做一次环境变量读写测试。

嗯,这一章的内容就到这里。记住一句话:board_init_f 是“活下去”,board_init_r 是“活得好”。理解了这两个阶段的区别,你就掌握了U-Boot启动流程的核心脉络。