4、U-Boot 启动第二阶段:C 环境初始化与 Board 相关代码

好,咱们接着往下走。上一阶段我们聊了汇编级的“开荒”,把 CPU 模式、时钟、DDR 这些硬骨头啃下来了。现在,U-Boot 终于可以跳进 C 语言的世界了。

这一阶段,说白了就是给 C 代码搭好“戏台子”。戏台子搭不好,后面的戏就没法唱。我个人习惯把这一阶段叫做“从裸金属到半操作系统的过渡”。

核心目标: 从汇编跳转到 C 语言环境,完成板级硬件初始化,为加载内核做好准备。

4.1 从汇编到 C 的“惊险一跳”

还记得上一章结尾那个 bl board_init_f 吗?这就是关键一跳。但跳之前,CPU 得先准备好 C 语言的运行环境。

最重要的就是栈指针(SP)。在汇编阶段,我们通常会给 SP 一个临时的、安全的地址,比如 SRAM 的顶部。为什么是临时的?因为这时候 DDR 可能还没完全初始化好,或者我们还没决定最终的栈放哪里。

/* 汇编阶段的典型操作,在 start.S 中 */
ldr sp, =CONFIG_SYS_INIT_SP_ADDR
bic sp, sp, #7  /* 8 字节对齐,ARM 规范 */
b   board_init_f

嗯,这里要注意对齐。ARM 架构要求 SP 必须 8 字节对齐,否则调用 printf 这类带可变参数的函数时,会莫名其妙地崩溃。我曾经在一个项目里,就因为少写了这一行 bic,调试了整整两天,最后发现是栈没对齐,打印浮点数时直接跳到了异常向量表。

4.2 board_init_f:板级初始化的“前奏曲”

board_init_f 这个函数,名字里的 _f 代表 pre-relocation(重定位前)。它运行在只读的 Flash 或 SRAM 里,主要任务是“侦察”和“规划”。

它不会把所有外设都初始化,而是做最必要的事:

  • 初始化串口:让你能看到打印信息。这是调试的生命线。
  • 初始化计时器:提供延时和超时功能。
  • 初始化 DDR 控制器:虽然汇编阶段可能已经初始化了,但这里会做更详细的配置和校验。
  • 规划内存布局:决定 U-Boot 自身要重定位到 DDR 的哪个地址,以及各种数据区(堆、环境变量、内核加载地址)的位置。

这个函数会调用一个非常重要的结构体——init_sequence_f。这是一个函数指针数组,里面按顺序排列了所有需要调用的初始化函数。

阶段 典型函数 作用
1 setup_mon_len 计算 U-Boot 自身长度
2 fdtdec_setup 解析设备树(如果使用)
3 initf_malloc 初始化早期的 malloc 池
4 initf_bootstage 启动阶段计时
5 board_early_init_f 板级早期初始化(GPIO、时钟等)
6 dram_init 获取 DDR 大小和配置
7 setup_dest_addr 计算重定位目标地址
我的经验: 如果你在 board_early_init_f 里初始化了某个 GPIO 控制电源,一定要确保这个 GPIO 的默认状态不会导致系统掉电。我见过一个案例,工程师在这里拉低了一个使能引脚,结果 DDR 初始化到一半,电源被关了,整个系统直接“死机”。

4.3 内存布局规划:一场精密的“搬家”

U-Boot 最终要从 Flash 或 ROM 里把自己“搬”到 DDR 中运行。为什么?因为 DDR 速度快,而且可读写。这个“搬家”过程就是重定位(Relocation)。

在 board_init_f 的最后,它会计算出一张“搬家地图”:

  1. U-Boot 自身代码:要拷贝到哪个地址?通常是 DDR 的高端地址。
  2. malloc 堆:给动态内存分配用。
  3. 环境变量区:存放你设置的 bootargs 等参数。
  4. 内核加载地址:将来 bootm 命令会把内核解压到这里。
  5. 设备树(FDT)加载地址:给内核传递硬件信息。

这个规划结果会保存在一个全局数据结构 gd_t(global data)里。这个结构体是 U-Boot 的“中枢神经”,所有关键信息都存里面。

/* 典型的 gd_t 结构体成员(简化版) */
typedef struct global_data {
    ulong relocaddr;      /* 重定位后的起始地址 */
    ulong reloc_off;      /* 重定位偏移量 */
    ulong start_addr_sp;  /* 重定位后的栈指针 */
    ulong ram_top;        /* DDR 的顶端地址 */
    ulong malloc_limit;   /* malloc 池大小 */
    bd_t *bd;             /* 板级信息结构体 */
} gd_t;

4.4 重定位与 board_init_r:真正的“主场”

规划完成后,U-Boot 会调用一个汇编函数 relocate_code。这个函数会:

  • 把 U-Boot 自身从 Flash 拷贝到 DDR 的目标地址。
  • 修正代码中的绝对地址引用(因为代码位置变了)。
  • 重新设置栈指针到 DDR 中。
  • 跳转到 DDR 中的 board_init_r 函数。

从这一刻起,U-Boot 就在 DDR 里“安家”了。board_init_r 里的 _r 代表 post-relocation(重定位后)。这才是真正的板级初始化主场。

它会初始化所有剩下的外设:

  • NAND/NOR Flash 控制器
  • 网络控制器(MAC/PHY)
  • USB 控制器
  • MMC/SD 卡控制器
  • I2C/SPI 总线
  • 控制台(Console):让你能敲命令
避坑指南: 我曾经在移植 U-Boot 到一块新板子时,board_init_r 里初始化了网络,但死活 ping 不通。最后发现是 PHY 芯片的复位时序问题——我在初始化 MAC 之前就把 PHY 复位了,导致 PHY 没有准备好。正确的做法是先确保 PHY 上电稳定,再复位,再初始化。

4.5 核心流程图:第二阶段全景

下面这张图,我把整个第二阶段的关键路径画出来了。你对照着看,思路会更清晰。

U-Boot 启动第二阶段流程图 U-Boot 启动第二阶段:C 环境初始化与 Board 代码 汇编阶段结束 设置 SP 栈指针 board_init_f() init_sequence_f[] 执行流程: 1. setup_mon_len → 2. fdtdec_setup → 3. initf_malloc 4. board_early_init_f → 5. dram_init → 6. setup_dest_addr 7. reserve_xxx (预留内存) → 8. reloc_fdt_addr relocate_code() board_init_r() 初始化所有外设 进入主循环

4.6 板级代码的“三驾马车”

在 U-Boot 的源码里,跟具体板子相关的代码通常分布在三个地方。搞清楚它们,你移植的时候就知道该改哪里了。

目录/文件 内容 典型修改场景
board/<vendor>/<board>/ 板级初始化函数、board_init_f/r 换 DDR 型号、改 GPIO 配置
include/configs/<board>.h 宏定义、配置选项、内存地址 改环境变量偏移、调内核加载地址
arch/arm/mach-<soc>/ SoC 级代码(时钟、定时器、DDR 控制器) 改 CPU 频率、调 DDR 时序参数
调试小技巧: 如果你在 board_init_f 阶段就遇到了问题,比如串口没输出,别急着怀疑硬件。先检查 include/configs/ 里的 CONFIG_SYS_INIT_SP_ADDR 是否指向了一个有效的、可读写的内存区域。我遇到过好几次,这个地址指向了未初始化的 DDR 区域,导致压栈时直接死机。

好了,第二阶段的核心内容就是这些。从汇编到 C 的跳转、内存布局规划、重定位、再到板级初始化,每一步都有它的道理。你把这些搞清楚了,U-Boot 在你眼里就不再是一个黑盒子了。