4. 启动流程第二阶段(C语言部分):board_init_f、board_init_r深度解析
好,咱们接着聊。上一章我们把汇编阶段的“硬核”启动讲完了,那时候CPU刚上电,连栈都还没准备好。现在,程序终于跳到了C语言的世界。这一章,我带你看看U-Boot里两个最重要的C函数——board_init_f和board_init_r。说白了,它们就是U-Boot的“前半生”和“后半生”。
核心要点:board_init_f负责“从零到一”,在受限的临时环境下完成最基础的内存初始化、时钟配置、串口输出等。board_init_r则是“二次创业”,在完整的内存环境中重新初始化所有外设,加载设备树,准备引导内核。
4.1 board_init_f:在“临时工棚”里干活
你想想看,这时候的U-Boot还住在哪里?它可能还在ROM里,或者刚被搬到了SRAM里。空间小,速度慢,连个像样的堆都没有。所以board_init_f的任务就是:用最少的资源,把最核心的硬件搞起来。
我个人习惯把board_init_f的执行流程画成一张图,这样看代码时心里有底。来,我手绘了一张SVG流程图,你看看是不是这个理儿:
嗯,这张图基本把board_init_f的骨架画出来了。但实际代码里,每个步骤都有很多坑。我挑几个重点说说。
4.1.1 dram_init:内存初始化,这是“生死线”
为什么说它是生死线?因为DRAM没配好,后面所有代码都跑不起来。U-Boot里dram_init函数通常要完成两件事:
- 检测内存大小:通过读取硬件寄存器或执行内存测试,确定板子上焊了多大容量的DDR。
- 配置内存控制器:设置时序参数(CAS、tRCD、tRP等),这些参数跟你的PCB走线长度、颗粒型号强相关。
我的经验:有一次我移植一块新板子,内存死活初始化不过。折腾了两天,最后发现是PCB上DDR的VREF参考电压走线没接好,导致内存颗粒的参考电压偏差了0.1V。从那以后,我每次调内存都会先拿万用表量一下VREF电压。
代码层面,dram_init通常长这样:
int dram_init(void)
{
/* 假设我们用的是DDR3,容量512MB */
gd->ram_size = 512 * 1024 * 1024;
/* 实际项目中,这里会调用厂商提供的DDR初始化库 */
/* 比如:ddr3_init(CONFIG_DDR_CLOCK, CONFIG_DDR_TIMING); */
return 0;
}
注意,这里只是告诉U-Boot“我有512MB内存”,但真正的内存控制器配置,通常是在汇编阶段或者更早的SPL里完成的。U-Boot的dram_init更多是做一个“确认”和“上报”的动作。
4.1.2 board_init_f_init_reserve:给全局数据找个家
U-Boot里有个全局数据结构叫gd_t(global data),它记录了板子信息、启动参数、环境变量指针等。在board_init_f阶段,我们需要在内存里给它分配一块空间。
这个函数做的事情很简单:在栈顶附近预留一块区域,然后把gd指针指向那里。代码大概是这样:
void board_init_f_init_reserve(ulong base)
{
/* base是当前可用的内存起始地址 */
gd = (gd_t *)base;
memset(gd, 0, sizeof(gd_t));
/* 预留early malloc的空间 */
gd->malloc_base = base + sizeof(gd_t);
gd->malloc_limit = CONFIG_SYS_MALLOC_F_LEN;
}
注意:这个阶段的malloc是“early malloc”,空间非常有限(通常只有几KB)。千万别在这里申请大块内存,否则会踩到栈空间,导致程序崩溃。我曾经见过一个同事在board_init_f里申请了1MB的缓冲区,结果直接覆盖了栈,板子死得悄无声息。
4.2 board_init_r:搬进“新家”,大展拳脚
当board_init_f执行完毕,U-Boot已经完成了DRAM的初始化。这时候,程序会把自己从SRAM(或ROM)搬到DRAM里运行。这个动作叫“重定位”(relocation)。重定位之后,board_init_r就登场了。
说白了,board_init_r是在一个“完整”的环境里重新初始化所有东西。它要做的事情比board_init_f多得多:
| 初始化项 | 说明 | 我的建议 |
|---|---|---|
| 中断控制器 | 配置GIC,使能中断 | 先关掉所有中断源,再逐个使能 |
| 定时器 | 初始化系统定时器,提供时间基准 | 用硬件定时器,别用软件循环 |
| 环境变量 | 从存储设备加载env | 注意env的校验和,防止损坏 |
| 设备树 | 解析并传递FDT | 后面会详细讲 |
| 网络栈 | 初始化MAC、PHY | 先读PHY ID,确认芯片型号 |
| 存储设备 | 初始化MMC、NAND、SPI Flash | 注意时序配置,不同颗粒差异大 |
你看,这个表里每一项拿出来都能写一篇文章。但这一章我们重点讲设备树传递,因为这是U-Boot和内核之间的“桥梁”。
4.2.1 设备树传递:U-Boot给内核的“见面礼”
设备树(Device Tree,简称DT)说白了就是一个描述硬件信息的文件。U-Boot在启动内核之前,需要把设备树的地址告诉内核。这个动作通常发生在board_init_r的最后阶段,也就是bootm命令执行的时候。
传递设备树的流程大致如下:
- 加载设备树:从存储设备(MMC、NAND、TFTP等)把
.dtb文件读到内存里。 - 修改设备树:U-Boot可能会根据实际硬件情况,修改设备树里的某些节点(比如内存大小、MAC地址)。
- 传递地址:把设备树在内存中的物理地址,通过寄存器(比如ARM的
r2寄存器)传递给内核。
代码层面,U-Boot里有一个关键函数叫image_setup_libfdt,它负责完成设备树的修改和最终地址的确定。我摘一段核心逻辑:
int image_setup_libfdt(bootm_headers_t *images, void *blob)
{
int ret;
/* 1. 确保设备树在内存中 */
if (fdt_check_header(blob) != 0) {
printf("ERROR: Device Tree blob is invalid\n");
return -1;
}
/* 2. 更新设备树中的内存节点 */
ret = fdt_fixup_memory(blob, (u64)gd->ram_base, (u64)gd->ram_size);
if (ret) {
printf("ERROR: Cannot fix up memory node\n");
return ret;
}
/* 3. 更新其他硬件信息(比如以太网MAC) */
ret = fdt_fixup_ethernet(blob);
if (ret) {
printf("WARNING: Could not fix up ethernet\n");
}
/* 4. 最终,blob指向的地址就是内核要用的设备树地址 */
images->ft_addr = blob;
return 0;
}
避坑指南:我曾经遇到一个情况,设备树加载到内存后,内核启动时死活不认。查了半天,发现是设备树地址没有做cache一致性处理。U-Boot在修改设备树时,CPU的cache里是新的数据,但DDR里还是旧的。解决办法是在传递之前调用flush_dcache_range,把cache刷到内存里。
4.3 从board_init_f到board_init_r的“交接仪式”
这两个函数之间是怎么衔接的?其实U-Boot内部有一个全局指针gd,它在board_init_f阶段被初始化,然后通过重定位过程,被复制到DRAM里的新位置。board_init_r启动时,直接读取这个gd指针,就能拿到之前所有的初始化成果。
这个交接过程,我建议你重点关注两个宏:
CONFIG_SPL_BUILD:如果定义了,表示当前是SPL阶段,board_init_f执行完后会返回SPL,由SPL加载U-Boot。CONFIG_SYS_INIT_SP_OFFSET:定义了栈指针在SRAM中的偏移量,确保重定位前后栈不冲突。
嗯,说到这,我想起一个细节。很多新手在移植U-Boot时,发现board_init_f执行完就卡死了。排查到最后,往往是gd指针在重定位后被破坏了。我的建议是:在board_init_r的第一行代码里,打印一下gd指针的值,看看它是不是指向了DRAM里的正确位置。
好了,这一章的内容就到这里。你只要记住:board_init_f是“打地基”,board_init_r是“盖房子”。地基没打好,房子盖得再漂亮也白搭。下一章我们会深入设备树的解析和修改,那是U-Boot和内核通信的“语言”。
小练习:打开你的U-Boot源码,找到board_init_f和board_init_r的实现,对照我上面讲的流程图,看看你的板子在这两个函数里都做了哪些额外的事情。