U-Boot 的启动流程看似复杂,但其骨架其实非常清晰------两个函数指针数组
init_sequence_f[]和init_sequence_r[],配合一个统一的执行引擎initcall_run_list(),就完成了从硬件上电到命令行提示符=>的全部调度。本文从源码层面拆解这套"双阶段初始化架构"的设计哲学与执行细节。
一、执行引擎:initcall_run_list()
这两个数组并非直接遍历调用,而是由一个统一的调度器编排执行。代码位于 include/initcall.h:
c
typedef int (*init_fnc_t)(void);
static inline int initcall_run_list(const init_fnc_t init_sequence[])
{
for (init_fnc_ptr = init_sequence; *init_fnc_ptr; ++init_fnc_ptr) {
unsigned long reloc_ofs = 0;
...
if (IS_ENABLED(CONFIG_SANDBOX) || (gd->flags & GD_FLG_RELOC))
reloc_ofs = gd->reloc_off;
ret = (*init_fnc_ptr)(); // 调用初始化函数
if (ret) {
printf("initcall sequence %p failed at call %p (err=%d)\n",
init_sequence, (char *)*init_fnc_ptr - reloc_ofs, ret);
return -1;
}
}
return 0;
}
设计关键点
| 特性 | 说明 |
|---|---|
| 顺序执行 | 数组中的函数按索引顺序逐个调用 |
| 失败即停 | 任一函数返回非 0,整个序列失败,board_init_f() / board_init_r() 会调用 hang() 死锁 |
| 重定位感知 | 对于 sandbox 或已完成重定位的系统,自动加上 gd->reloc_off,调试信息打印原始地址 |
| NULL 终止 | 数组末尾以 NULL 结束,作为遍历终止条件 |
这种设计的核心思想是:将复杂的启动流程拆成一组高内聚、低耦合的初始化步骤,每个步骤只返回 0(成功)或非 0(失败),由统一的调度器编排顺序。
二、init_sequence_f[]:前置重定位阶段
board_init_f() 在 common/board_f.c 中调用它:
c
void board_init_f(ulong boot_flags)
{
gd->flags = boot_flags;
gd->have_console = 0;
if (initcall_run_list(init_sequence_f))
hang();
}
这个阶段 U-Boot 通常还运行在 Flash/ROM 中(真实嵌入式平台),或者运行在宿主机的原始加载地址(sandbox)。核心任务是:为"重定位到 DRAM"做好一切准备工作。
2.1 自身度量与基础初始化
c
setup_mon_len, // 计算 U-Boot 自身镜像长度 → gd->mon_len
initf_malloc, // 初始化 pre-relocation 的简单 malloc
log_init, // 初始化日志子系统
initf_bootstage, // 启动阶段计时标记
setup_spl_handoff, // SPL 交接数据处理
setup_mon_len 的代码体现了跨平台差异:
c
static int setup_mon_len(void)
{
#if defined(__ARM__) || defined(__MICROBLAZE__)
gd->mon_len = (ulong)&__bss_end - (ulong)_start;
#elif defined(CONFIG_SANDBOX) || defined(CONFIG_EFI_APP)
gd->mon_len = (ulong)&_end - (ulong)_init;
...
}
为什么重要 :后续所有内存预留(reserve)都基于 gd->mon_len 来计算。
2.2 架构与驱动模型预初始化
c
arch_cpu_init, // 架构级 CPU 初始化(通常是 __weak 空函数)
mach_cpu_init, // SoC 级初始化
initf_dm, // ★ Driver Model 预初始化
arch_cpu_init_dm, // 架构级 DM 初始化
initf_dm 的代码:
c
static int initf_dm(void)
{
#if defined(CONFIG_DM) && CONFIG_VAL(SYS_MALLOC_F_LEN)
ret = dm_init_and_scan(true); // true = pre_reloc_only
...
}
这里调用 dm_init_and_scan(true),即只绑定标记为 pre-reloc 的设备。这是为了在重定位前就能使用最基础的驱动(如串口)。
2.3 控制台与信息输出
c
env_init, // 初始化环境变量(此时通常是内存默认值或硬编码值)
init_baud_rate, // 从环境变量读取波特率
serial_init, // 初始化串口硬件
console_init_f, // stage 1 控制台初始化
display_options, // 打印 "U-Boot 2020.04" 等版本信息
display_text_info, // 调试信息
checkcpu, // CPU 检查
注意 console_init_f 中的 _f 后缀表示这是 stage F 的控制台,此时控制台可能还没有作为 DM 设备完全 probe,只是设置了最基本的输出通道。
2.4 DRAM 探测与内存布局规划
c
announce_dram_init, // 打印 "DRAM: "
dram_init, // ★ 探测/设置 RAM 大小 → gd->ram_size
setup_dest_addr, // 计算 RAM 顶部地址 → gd->ram_top, gd->relocaddr
dram_init 是板级必须实现的函数。在 sandbox 中:
c
// board/sandbox/sandbox.c
int dram_init(void)
{
gd->ram_size = CONFIG_SYS_SDRAM_SIZE; // 直接读取配置
return 0;
}
setup_dest_addr 之后,进入一连串的 reserve_* 函数,它们从 RAM 顶部向下"雕刻"出一块块预留区域:
c
reserve_round_4k, // 4KB 对齐
reserve_video, // 视频帧缓冲
reserve_trace, // trace 缓冲区
reserve_uboot, // ★ U-Boot 自身镜像要复制到的位置
reserve_malloc, // ★ malloc 堆区域
reserve_board, // ★ bd_t 结构体
setup_machine,
reserve_global_data, // ★ gd 的新副本
reserve_fdt, // 设备树拷贝区
reserve_bootstage,
reserve_bloblist,
reserve_arch,
reserve_stacks, // ★ 栈
dram_init_banksize,
show_dram_config, // 打印 DRAM 配置
这是一张内存布局蓝图 。最终计算出的 gd->relocaddr(U-Boot 要复制到的 DRAM 地址)、gd->start_addr_sp(栈顶)、gd->new_gd(新的全局数据指针)都会在后续使用。
最后:
c
reloc_fdt, // 将设备树重定位到 DRAM
reloc_bootstage,
reloc_bloblist,
setup_reloc, // 计算重定位偏移量 gd->reloc_off
clear_bss, // 清空 BSS
在大多数真实架构中,到这里会调用 jump_to_copy() 或 relocate_code() 把自身复制到 DRAM,然后跳转到 board_init_r()。sandbox 平台上没有真实的重定位拷贝,所以直接返回后由 main() 调用 board_init_r(gd->new_gd, 0)。
三、init_sequence_r[]:后置重定位阶段
board_init_r() 在 common/board_r.c 中:
c
void board_init_r(gd_t *new_gd, ulong dest_addr)
{
gd = new_gd; // 切换到新的全局数据指针
...
if (initcall_run_list(init_sequence_r))
hang();
/* NOTREACHED - run_main_loop() does not return */
hang();
}
此时 U-Boot 已经运行在 DRAM 中(或 sandbox 的模拟 RAM 中),可以使用完整的 malloc、可以安全地 probe 所有设备。核心任务是:完成所有子系统初始化,最终进入命令行主循环。
3.1 重定位后基础设施
c
initr_reloc, // 设置标志:GD_FLG_RELOC | GD_FLG_FULL_MALLOC_INIT
initr_reloc_global_data,// 修正全局数据中的指针
initr_barrier, // 内存屏障(PPC 专用)
initr_malloc, // ★ 初始化正式 malloc 堆
initr_malloc 的代码:
c
static int initr_malloc(void)
{
malloc_start = gd->relocaddr - TOTAL_MALLOC_LEN;
mem_malloc_init((ulong)map_sysmem(malloc_start, TOTAL_MALLOC_LEN),
TOTAL_MALLOC_LEN);
return 0;
}
这里初始化的 malloc 是真正的、完整的堆,不再受 SYS_MALLOC_F_LEN 限制。
3.2 Driver Model 正式初始化
c
initr_dm, // ★ 重新初始化 DM(这次 pre_reloc_only=false)
board_init, // 板级初始化(sandbox 中调用)
initr_dm_devices, // 初始化早期需要的 DM 设备
initr_dm 的代码很关键:
c
static int initr_dm(void)
{
/* Save the pre-reloc driver model and start a new one */
gd->dm_root_f = gd->dm_root; // 保留 F 阶段的 DM 根
gd->dm_root = NULL; // 清空,准备重建
gd->timer = NULL;
ret = dm_init_and_scan(false); // false = 扫描所有设备
...
}
为什么需要重建 DM? 因为 F 阶段使用的设备和内存布局在重定位后可能已经失效,R 阶段需要基于新的内存地址重新扫描设备树并绑定所有驱动。
3.3 控制台与标准 IO
c
stdio_init_tables, // 初始化 stdio 设备表
initr_serial, // 再次初始化串口(使用新的 gd)
initr_announce, // 调试打印当前运行地址
power_init_board, // 板级电源初始化(__weak 空函数)
...
stdio_add_devices, // 注册所有 stdio 设备
console_init_r, // ★ stage R 控制台完全初始化
console_init_r 与 console_init_f 的区别:R 阶段控制台已经是完整的 DM 设备,支持所有输入输出重定向。
3.4 环境变量与网络
c
initr_env, // ★ 加载/初始化环境变量
...
#ifdef CONFIG_CMD_NET
initr_ethaddr, // 从环境变量或 EEPROM 读取 MAC 地址
initr_net, // 初始化网络栈
#endif
3.5 最终入口
c
run_main_loop, // ★ 永不返回
run_main_loop 的实现:
c
static int run_main_loop(void)
{
#ifdef CONFIG_SANDBOX
sandbox_main_loop_init();
#endif
for (;;)
main_loop(); // 进入命令行循环
return 0;
}
这就是你在屏幕上看到 => 提示符之前的最后一个初始化步骤。
四、F 阶段 vs R 阶段的本质区别
| 维度 | init_sequence_f[] | init_sequence_r[] |
|---|---|---|
| 运行位置 | Flash/ROM(或原始加载地址) | DRAM(或重定位后的地址) |
| malloc | 受限制的 pre-reloc malloc(如果有) | 完整的 malloc 堆 |
| Driver Model | 仅 pre_reloc 设备 | 所有设备 |
| 全局数据 | 使用原始的 gd(可能在 SRAM/栈上) | 使用新的 gd(在 DRAM 中预留的区域) |
| 控制台 | stage F,基础输出 | stage R,完整的 DM 设备 |
| 核心目标 | 探测 DRAM,计算重定位地址 | 初始化所有子系统,进入命令行 |
| 失败行为 | hang() 死锁 |
hang() 死锁 |
五、在 Sandbox 平台上的特殊表现
由于 sandbox 没有真实的 Flash→DRAM 重定位过程,很多函数被简化或为空:
setup_mon_len():计算_end - _init,而不是真实的镜像拷贝长度initf_dm()/initr_dm():都调用dm_init_and_scan(),但因为 sandbox 在 F 和 R 阶段都使用相同的内存地址,gd->dm_root_f的保存/恢复更多是为了兼容其他架构initr_reloc():只是设置两个标志位,没有实际的代码搬运jump_to_copy():在 sandbox 中被条件编译排除__weak函数(如arch_cpu_init、power_init_board、checkcpu):sandbox 没有覆盖它们,所以执行的是空实现
六、设计哲学与调试技巧
6.1 为什么采用数组调度?
- 新增初始化步骤只需在数组中插入一个函数指针,无需改动框架代码
- 平台差异通过
#ifdef和__weak屏蔽,公共代码保持不变 - 调试方便 :如果启动卡在某个步骤,可以通过在
initcall_run_list()中加打印精确定位
6.2 实战调试建议
如果 U-Boot 启动挂死,在 common/board_f.c 和 common/board_r.c 中给 initcall_run_list() 添加如下打印:
c
printf(">>> initcall: %p\n", (char *)*init_fnc_ptr - reloc_ofs);
通过比对地址与 System.map,即可秒定位到具体卡死的初始化函数。
七、总结
init_sequence_f[] 和 init_sequence_r[] 是 U-Boot 启动流程的骨架:
- F 阶段像是一个"自检与规划阶段":测量自己多大、初始化最基本的串口、探测 DRAM、然后在 DRAM 顶部画出一张"内存分配图"
- R 阶段像是一个"全面建设阶段" :基于新的内存地址重新建立 DM、初始化完整的堆、加载环境变量、注册所有设备,最后把控制权交给
main_loop()
理解这两个数组,你就掌握了阅读任何平台 U-Boot 启动代码的"导航图"------无论是 Sandbox、ARM64 还是 RISC-V,Stage F/R 的调度逻辑都是通用的。