4、U-Boot启动流程分析(下):C语言入口与板级初始化
上一讲我们聊完了汇编阶段的“热身运动”,这一讲终于要进入C语言的世界了。说实话,U-Boot的C语言入口才是真正考验移植功底的地方。我当年第一次看这段代码时,也被那密密麻麻的函数调用搞得晕头转向。别急,我们一步步拆开来看。
4.1 从汇编到C:入口函数board_init_f
汇编阶段最后调用了 board_init_f,这个函数定义在 common/board_f.c 中。它干了什么?说白了就是“初始化那些在重定位之前必须搞定的事情”。
核心要点:board_init_f 运行在Flash或ROM中,此时RAM可能还没完全可用,所以不能做复杂的内存操作。
我习惯把 board_init_f 的工作分成三块:
- 基础硬件初始化:串口、时钟、定时器——这些是调试的“眼睛”和“心跳”
- DRAM初始化:板级内存控制器配置,这一步出问题后面全白搭
- 内存布局规划:计算U-Boot自身要重定位到DRAM的哪个位置
来看一个典型的初始化序列(伪代码):
// common/board_f.c 中的 init_sequence_f 数组
static init_fnc_t init_sequence_f[] = {
setup_mon_len, // 计算U-Boot镜像长度
initf_malloc, // 初始化malloc池
initf_console_record, // 控制台记录
arch_cpu_init, // CPU架构相关初始化
initf_dm, // 驱动模型初始化
board_early_init_f, // 板级早期初始化
timer_init, // 定时器初始化
env_init, // 环境变量初始化
init_baud_rate, // 波特率设置
serial_init, // 串口初始化
console_init_f, // 控制台初始化
dram_init, // DRAM大小检测
setup_dest_addr, // 计算重定位目标地址
reserve_xxx, // 预留各种内存区域
...
reloc_fdt_addr, // 设备树重定位地址
setup_reloc, // 设置重定位参数
NULL
};
嗯,这里要注意:dram_init 只是获取DRAM大小,真正的内存控制器初始化通常在 board_early_init_f 或更早的汇编阶段就完成了。我在项目中遇到过一块板子,DRAM大小检测总是返回0,查了两天才发现是 dram_init 里读的寄存器地址写错了——这种低级错误最坑人。
4.2 内存布局的艺术:board_init_f 的“预留”逻辑
为什么要在 board_init_f 里做内存预留?因为U-Boot重定位后,需要知道自己该往哪儿搬。搬之前得先把位置算好,就像搬家前得先量好新家的尺寸。
内存布局从高地址向低地址生长,预留顺序如下:
| 预留项 | 说明 | 对齐要求 |
|---|---|---|
| U-Boot镜像 | 重定位后的代码段+数据段 | 4KB对齐 |
| malloc池 | 动态内存分配区域 | 16字节对齐 |
| 设备树( FDT ) | 扁平设备树二进制块 | 4KB对齐 |
| 栈空间 | 重定位后的栈 | 16字节对齐 |
| Global Data | 全局数据结构gd_t | 8字节对齐 |
我的经验:malloc池大小别设太小,我见过有人设成1KB,结果加载设备树时直接崩了。建议至少留4KB以上,如果用到文件系统,16KB起步。
4.3 重定位:从Flash到RAM的“乾坤大挪移”
board_init_f 执行完毕后,U-Boot会调用 relocate_code 把自己从Flash搬到DRAM中。这一步为什么必要?因为Flash访问速度慢,而且有些功能(比如USB、网络)需要可写的数据段。
重定位流程大致如下:
- 从
gd->relocaddr获取目标地址 - 拷贝U-Boot镜像到目标地址
- 修复代码中的绝对地址引用(relocation table)
- 更新栈指针和全局数据指针
- 跳转到RAM中的
board_init_r
这里有个关键点:重定位后的地址修复。U-Boot在编译时生成了一张“重定位表”,记录了所有需要修正的地址。搬完代码后,遍历这张表,把每个地址加上偏移量。我曾经手写过一个板子的重定位代码,忘了处理重定位表,结果跳转后直接跑飞——那感觉,就像你搬了新家但门牌号没改,快递全送错地方了。
4.4 board_init_r:真正的“主战场”
重定位完成后,控制权交给 board_init_r(定义在 common/board_r.c)。此时U-Boot已经在RAM中运行,可以放开手脚做各种初始化了。
board_init_r 的初始化序列比 board_init_f 长得多,我挑几个重点说:
- 驱动模型初始化:枚举所有设备,绑定驱动
- 存储设备初始化:MMC、NAND、SPI Flash等
- 文件系统支持:FAT、ext4、UBIFS等
- 网络协议栈:如果使能了网络功能
- 命令行解析器:准备接收用户输入
避坑指南:我曾经在 board_init_r 里加了一个调试打印,结果打印内容被缓冲了没立即输出,导致我误以为系统卡住了。后来发现是控制台缓冲没刷新——加个 serial_flush() 就好了。
4.5 设备树传递:从“硬编码”到“软配置”
设备树(Device Tree)是现代U-Boot的核心配置机制。它让板级描述从代码中分离出来,变成一份独立的数据文件。说白了,以前改个GPIO引脚得改C代码重新编译,现在改dts文件就行。
U-Boot中设备树的传递路径:
- SPL(如果使用)加载U-Boot时,同时加载设备树到内存
board_init_f中通过reloc_fdt_addr预留设备树空间- 重定位时,设备树也被搬到RAM中
board_init_r通过gd->fdt_blob访问设备树- 最终启动内核时,把设备树地址传给内核
设备树在内存中的布局:
// 设备树头部结构(简化)
struct fdt_header {
uint32_t magic; // 魔数 0xD00DFEED
uint32_t totalsize; // 总大小
uint32_t off_dt_struct; // 结构块偏移
uint32_t off_dt_strings; // 字符串块偏移
uint32_t off_mem_rsvmap; // 保留内存映射
uint32_t version; // 版本号
uint32_t size_dt_struct; // 结构块大小
uint32_t size_dt_strings; // 字符串块大小
};
实用技巧:在U-Boot命令行下可以用 fdt addr 查看设备树地址,用 fdt list 遍历节点。调试设备树时,这两个命令比看打印日志快多了。
4.6 启动流程全景图
下面这张图把整个第二阶段的流程串起来了,建议你对照代码一起看:
4.7 几个容易踩的坑
最后分享几个我在实际移植中遇到的典型问题:
- 栈溢出:
board_init_f阶段栈空间有限,别在里面定义大数组。我见过有人放了个4KB的局部变量,直接盖掉了全局数据区。 - 设备树地址冲突:设备树加载地址和U-Boot重定位地址重叠,导致设备树被覆盖。检查
CONFIG_SYS_FDT_LOAD_ADDR和CONFIG_SYS_TEXT_BASE是否冲突。 - 重定位表缺失:某些编译器优化会去掉重定位表,导致搬完代码后地址全错。在链接脚本里检查
.rel.dyn段是否存在。 - 串口打印延迟:重定位后串口寄存器地址变了,如果没更新基地址,打印会乱码或没输出。
血的教训:我曾经在 board_init_f 里调用了 printf,但此时控制台还没完全初始化,结果打印内容被缓冲了。重定位后缓冲清空,打印才出来——但那时系统已经跑到 board_init_r 了,调试信息完全对不上时间线。所以早期调试建议直接用 serial_putc 或 putc,别依赖 printf。
好了,U-Boot的第二阶段启动流程就分析到这里。从 board_init_f 的“轻装上阵”到 board_init_r 的“全面展开”,再到设备树的灵活传递,每一步都有它的设计考量。理解了这些,你再看任何板子的U-Boot代码,都不会觉得陌生了。