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 执行流程图 board_init_f 入口 1. 初始化串口 (serial_init) —— 让开发板能“说话”,输出调试信息 2. 初始化时钟 (timer_init / clock_init) —— 配置PLL,让CPU跑在目标频率 3. 初始化DRAM (dram_init) —— 检测并初始化板载DDR内存 4. 初始化栈与GD (board_init_f_init_reserve) —— 分配全局数据结构gd_t的空间 5. 返回SPL / 跳转到board_init_r —— 完成使命,移交控制权 虚线表示可选路径

嗯,这张图基本把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命令执行的时候。

传递设备树的流程大致如下:

  1. 加载设备树:从存储设备(MMC、NAND、TFTP等)把.dtb文件读到内存里。
  2. 修改设备树:U-Boot可能会根据实际硬件情况,修改设备树里的某些节点(比如内存大小、MAC地址)。
  3. 传递地址:把设备树在内存中的物理地址,通过寄存器(比如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的实现,对照我上面讲的流程图,看看你的板子在这两个函数里都做了哪些额外的事情。

可可精品资料库