U-Boot双阶段启动机制深度解析:init_sequence_f[] 与 init_sequence_r[]

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_rconsole_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_initpower_init_boardcheckcpu):sandbox 没有覆盖它们,所以执行的是空实现

六、设计哲学与调试技巧

6.1 为什么采用数组调度?

  1. 新增初始化步骤只需在数组中插入一个函数指针,无需改动框架代码
  2. 平台差异通过 #ifdef__weak 屏蔽,公共代码保持不变
  3. 调试方便 :如果启动卡在某个步骤,可以通过在 initcall_run_list() 中加打印精确定位

6.2 实战调试建议

如果 U-Boot 启动挂死,在 common/board_f.ccommon/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 的调度逻辑都是通用的。


相关推荐
叮叮当当05431 小时前
解决linux终端使用vim方向键失效问题
linux·运维·vim
原来是猿1 小时前
网络计算器:理解序列化与反序列化(上)
linux·运维·服务器·网络·tcp/ip
执笔仗剑天涯1 小时前
WSL安装cc-switch
linux·windows·wsl·cc-switch
Cx330❀1 小时前
从零实现一个 C++ 轻量级日志系统:原理与实践
大数据·linux·运维·服务器·开发语言·c++·搜索引擎
程序leo源1 小时前
Linux深度理解
linux·运维·服务器·c语言·c++·青少年编程·c#
Quinn271 小时前
正点原子 RK3562 Android14 Ubuntu 编译 SDK 环境准备:依赖、repo 与 Swap 配置一次搞定
linux·运维·ubuntu·mpu·正点原子·rk3562·arm linux
怀旧,1 小时前
【Linux系统编程】22. 线程同步与互斥(上)
linux·运维·服务器
光电笑映1 小时前
进程控制:从创建到替换的完整指南
linux·运维·服务器
Dovis(誓平步青云)1 小时前
《SQL语义等价性检查:Pivot的CASE WHEN改写策略与限制》
linux·windows·sql·microsoft·oracle·stable diffusion