4、U-Boot 启动第二阶段:C 环境初始化与 Board 相关代码
好,咱们接着往下走。上一阶段我们聊了汇编级的“开荒”,把 CPU 模式、时钟、DDR 这些硬骨头啃下来了。现在,U-Boot 终于可以跳进 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 的最后,它会计算出一张“搬家地图”:
- U-Boot 自身代码:要拷贝到哪个地址?通常是 DDR 的高端地址。
- malloc 堆:给动态内存分配用。
- 环境变量区:存放你设置的
bootargs等参数。 - 内核加载地址:将来
bootm命令会把内核解压到这里。 - 设备树(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):让你能敲命令
board_init_r 里初始化了网络,但死活 ping 不通。最后发现是 PHY 芯片的复位时序问题——我在初始化 MAC 之前就把 PHY 复位了,导致 PHY 没有准备好。正确的做法是先确保 PHY 上电稳定,再复位,再初始化。
4.5 核心流程图:第二阶段全景
下面这张图,我把整个第二阶段的关键路径画出来了。你对照着看,思路会更清晰。
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 在你眼里就不再是一个黑盒子了。