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里根本跑不开。
重定位的流程大致是这样:
board_init_f计算出DDR里要用的地址:gd->relocaddr- 调用
relocate_code(addr_moni),把代码段、数据段全部拷贝过去 - 修正所有绝对地址引用(因为代码挪了位置)
- 更新栈指针SP,让它指向DDR里的新栈
- 跳转到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 的完整流程串起来了:
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启动流程的核心脉络。