找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2429

积分

0

好友

325

主题
发表于 1 小时前 | 查看: 3| 回复: 0

在前三篇文章中,我们完成了最小Bootloader的基础框架:reset向量启动、LED控制、早期调试串口。这些代码能够运行,但从架构角度审视,它们仍是“散装”的裸机程序——缺乏统一的状态管理,没有规范的初始化流程,各功能模块之间也没有清晰的数据传递机制。

当我们深入研读U-Boot源码时,会发现一个无处不在的数据结构——GD(Global Data,全局数据)。从最早期的串口初始化,到DDR探测、环境变量加载、设备树解析、驱动模型构建,几乎每一个模块都要访问GD。可以毫不夸张地说,GD是U-Boot的“神经中枢”,它承载着整个引导过程所需的全部运行时状态。

与GD紧密配合的是 board_init_f 函数,这是U-Boot在代码重定位之前的核心初始化入口。“f”代表“first”或“before relocation”,意味着这是重定位前的第一阶段初始化。在此阶段,代码运行在初始加载地址,外部DDR可能尚未就绪,可用资源极为有限。board_init_f 的使命是在这些严苛约束下完成必要的早期初始化,并为后续的代码重定位和系统完整启动奠定基础。

本篇将深入剖析GD结构体的存在意义与内部构成,并系统性地介绍 board_init_f 框架。这是从“能运行的代码”走向“正规Bootloader”的关键一步,也是理解U-Boot内存管理与启动流程精髓的必经之路。

理解U-Boot的启动阶段划分

在深入GD之前,我们需要先理解U-Boot的启动流程为何要划分为多个阶段。这种划分不是人为的复杂化,而是嵌入式系统启动过程中的客观需求所决定的。

为什么需要分阶段启动?

当SoC上电复位时,面临以下现实约束:

  1. DDR未初始化:外部DRAM需要复杂的时序配置才能工作,此时只有片内SRAM(通常几十KB到几百KB)可用。
  2. 存储设备未初始化:eMMC、SD卡、SPI Flash等都需要相应控制器的初始化和协议握手。
  3. 时钟未配置:PLL、分频器等可能处于默认的低频状态。
  4. 代码位置受限:Bootloader最初可能被加载到片内SRAM的固定地址,空间有限。

因此,U-Boot采用分阶段的启动策略:

Stage 0: Boot ROM
  └─ SoC固化代码,加载SPL到SRAM

Stage 1: SPL (Secondary Program Loader) - 可选
  ├─ 运行在片内SRAM
  ├─ 初始化DDR控制器
  └─ 加载完整U-Boot到DDR

Stage 2: board_init_f (Before Relocation)
  ├─ 运行在初始加载地址
  ├─ GD结构体初始化
  ├─ 早期硬件初始化(串口、定时器等)
  ├─ 计算重定位参数
  └─ 准备将自身复制到DDR高端地址

Stage 3: Relocation
  ├─ 将代码/数据复制到新地址
  ├─ 修正GOT/重定位表
  └─ 跳转到新地址继续执行

Stage 4: board_init_r (After Relocation)
  ├─ 完整的硬件初始化
  ├─ 驱动模型(DM)初始化
  ├─ 环境变量加载
  ├─ 网络/USB/文件系统初始化
  └─ 进入命令行或自动启动内核

每个阶段只承担与其资源条件相匹配的任务,阶段间通过明确的接口传递控制权和必要信息。GD结构体正是这种信息传递的核心载体。

board_init_f的特殊性

board_init_f 运行在重定位之前,这意味着:

  • 不能使用全局变量:因为 .data 和 .bss 段可能尚未正确初始化,或者即将被重定位覆盖。
  • 栈空间有限:初始栈通常设置在片内SRAM,通常只有几KB。这意味着不能使用深层递归,不能在栈上分配大型数组,每一字节都需要精打细算。
  • 代码必须位置无关或在固定地址运行:不能依赖链接时确定的绝对地址(除非是XIP执行)。动态内存分配不可用

正是这些约束,催生了GD结构体的设计——它是一块预先分配的、位置已知的内存区域,用于在无法使用全局变量的阶段存储全局状态。

GD结构体的设计哲学

GD的本质:全局变量的替代方案

在常规的C程序中,全局状态自然地存储在全局变量中。但如前所述,在U-Boot的早期启动阶段,全局变量是不可靠的。那么,如何在没有全局变量的情况下维护全局状态?

U-Boot的解决方案是:将所有需要跨函数共享的状态集中到一个结构体中,并通过一个专用的CPU寄存器保存指向该结构体的指针

在ARM架构上,这个专用寄存器是 r9。GD结构体的首地址被存入r9,此后任何函数只需读取r9就能获得GD指针,进而访问任意全局状态。这种设计巧妙地绕过了对.data/.bss段的依赖。

U-Boot通过以下宏声明GD指针:

#define DECLARE_GLOBAL_DATA_PTR     register volatile gd_t *gd asm("r9")

这个声明告诉编译器:变量 gd 是一个“寄存器变量”,它始终存储在r9寄存器中。编译器会:

  • 在访问 gd 时直接使用r9,而非内存地址
  • 在函数调用时保持r9不变(r9在ARM调用约定中是callee-saved寄存器)
  • 不将r9分配给其他临时变量

GD的存储位置

GD结构体本身需要占用一块内存。在不同的启动阶段,这块内存的位置不同:

重定位前:GD通常被放置在初始栈的下方。栈向下生长,GD就在栈底(最低地址)。以i.MX6ULL为例,片内OCRAM的顶部(如0x00920000)被设为初始栈指针,GD结构体则紧贴其下方分配。

重定位后:GD会被复制到DDR的预规划位置。新的GD地址在 board_init_f 中计算并存储在 gd->new_gd 字段中。重定位代码负责将GD内容复制到新位置,然后更新r9寄存器。

GD的初始化

GD结构体的初始化发生在非常早期——在 _main(C运行时启动)函数中,甚至在 board_init_f 被调用之前。初始化过程包括:

  1. 计算GD地址:通常是 初始SP - sizeof(gd_t),然后对齐到16字节边界
  2. 清零GD内存:确保所有字段从0开始
  3. 设置r9寄存器:将计算出的GD地址存入r9

此阶段两个关键函数:board_init_f_alloc_reserveboard_init_f_init_reserve

GD的生命周期

GD的生命周期贯穿整个U-Boot的执行过程:

  1. 创建:在 _main 中分配和初始化
  2. 填充(board_init_f):逐步填入硬件信息、内存配置、时钟频率等
  3. 复制(relocation):连同U-Boot代码一起复制到DDR新位置
  4. 继续使用(board_init_r):在重定位后的代码中继续访问和更新
  5. 传递给内核:某些信息(如内存大小)会被转换为内核启动参数

GD结构体的内部构成

U-Boot源码中的GD定义

在U-Boot源码中,GD结构体定义在 include/asm-generic/global_data.h,并由各架构通过 arch/<arch>/include/asm/global_data.h 进行扩展。核心字段包括:

/* include/asm-generic/global_data.h */
typedef struct global_data gd_t;
typedef struct global_data {
    bd_t *bd;                       /* 板级信息指针 */
    unsigned long new_gd;           /* 重定位后的GD地址 */
    const void *fdt_blob;           /* 设备树指针 */
    struct udevice *cur_serial_dev; /* 当前使用的串口设备 */
    struct jt_funcs *jt;            /* 跳转表(导出函数表) */
struct board_f *boardf;         /* 仅在重定位前使用的板级信息 */

    unsigned long ram_base;         /* U-Boot 使用的 RAM 起始地址 */
    unsigned long ram_top;          /* U-Boot 可用 RAM 的顶部地址 */
    unsigned long ram_size;         /* RAM总大小 */

    unsigned long relocaddr;        /* 重定位后的U-Boot地址 */
    unsigned long reloc_off;        /* 重定位偏移量 */
    unsigned long irq_sp;           /* IRQ 模式栈指针 */
    unsigned long start_addr_sp;    /* 重定位后的栈指针(SVC 模式主栈) */

    unsigned long flags;            /* 全局标志位 */
    unsigned int baudrate;          /* 串口波特率 */
    unsigned long cpu_clk;          /* CPU时钟频率 */
    unsigned long bus_clk;          /* 总线时钟频率 */
    unsigned long pci_clk;          /* PCI时钟(如适用)*/
    unsigned long mem_clk;          /* 内存时钟频率 */
    unsigned int mon_len;           /* U-Boot 镜像长度 */

    unsigned long fb_base;          /* 帧缓冲基地址 */

    struct udevice *dm_root;        /* 驱动模型根设备 */
    /* ... 更多字段 ... */
} gd_t;

GD的flags字段

gd->flags 是一个位图,每一位代表一个布尔状态,用于记录系统状态:

/* include/asm-generic/global_data.h */
#define GD_FLG_RELOC           0x00001  /* 代码已重定位 */
#define GD_FLG_DEVINIT         0x00002  /* 设备已初始化 */
#define GD_FLG_SILENT          0x00004  /* 静默模式 */
#define GD_FLG_POSTFAIL        0x00008  /* POST测试失败 */
#define GD_FLG_POSTSTOP        0x00010  /* POST停止 */
#define GD_FLG_LOGINIT         0x00020  /* Log已初始化 */
#define GD_FLG_DISABLE_CONSOLE 0x00040  /* 禁用控制台 */
#define GD_FLG_ENV_READY       0x00080  /* 环境变量已导入 */
#define GD_FLG_SERIAL_READY    0x00100  /* 串口已就绪 */
#define GD_FLG_FULL_MALLOC_INIT 0x00200 /* malloc完全初始化 */
#define GD_FLG_SPL_INIT        0x00400  /* SPL已初始化 */
#define GD_FLG_SKIP_RELOC      0x00800  /* 跳过重定位 */
#define GD_FLG_RECORD          0x01000  /* 记录控制台 */
#define GD_FLG_ENV_DEFAULT     0x02000  /* 使用默认环境变量 */

通过检查这些标志,代码可以知道当前处于启动的哪个阶段,哪些子系统已经就绪。

bd_t结构体

bd_t结构体定义在 include/asm-generic/u-boot.h。

gd->bd 指向的 bd_t(Board Data)结构体包含传递给内核的板级信息:

typedef struct bd_info {
    unsigned long bi_memstart;      /* 内存起始地址 */
    unsigned long bi_memsize;       /* 内存大小 */
    unsigned long bi_flashstart;    /* Flash起始地址 */
    unsigned long bi_flashsize;     /* Flash大小 */
    unsigned long bi_flashoffset;   /* Flash保留区偏移 */
    unsigned long bi_sramstart;     /* SRAM起始地址 */
    unsigned long bi_sramsize;      /* SRAM大小 */
    unsigned long bi_bootflags;     /* 启动标志 */
    unsigned long bi_ip_addr;       /* IP地址 */
    unsigned char bi_enetaddr[6];   /* MAC地址 */
    unsigned long bi_arch_number;   /* ARM机器类型号 */
    unsigned long bi_boot_params;   /* 内核启动参数地址 */
    ...
} bd_t;

bd_t在 board_init_f 的内存预留阶段分配空间,在 board_init_r 中填充具体值。

早期Malloc机制

board_init_f阶段,完整的malloc/free尚不可用(那需要DRAM),但某些初始化函数(特别是设备模型DM的初始化)需要动态分配内存。U-Boot为此实现了一个极简的“早期malloc”。位置在 common/malloc_simple.c。

这个早期malloc的特点是:

  • 只分配不释放:没有free功能,分配指针malloc_ptr只会单调递增。
  • 极简高效:没有空闲链表管理、没有内存碎片处理。
  • 容量有限:总大小由CONFIG_SYS_MALLOC_F_LEN决定,通常只有几KB。
  • 过渡性质:在DRAM初始化完成、board_init_r阶段调用mem_malloc_init后,会切换到功能完整的dlmalloc。

board_init_f框架解析

board_init_f 承担着U-Boot启动第一阶段的所有C语言初始化工作,其核心使命可以归纳为三点:

  1. 初始化DRAM控制器,使外部DDR内存可用。
  2. 规划DRAM内存布局,确定U-Boot重定位目标地址、栈地址、malloc区域、设备树存放位置等。
  3. 计算重定位参数,为后续的代码搬移做好准备。

board_init_f运行结束后,系统将具备以下条件:

  • DRAM已初始化并可访问
  • GD中记录了完整的内存布局信息
  • 重定位地址、栈地址等关键参数已计算就绪

但此时代码尚未被搬移到DRAM,U-Boot仍在原始加载地址运行。真正的搬移工作由后续的relocate_code汇编函数完成。

init_sequence_f初始化序列

U-Boot采用了一种优雅的设计模式来组织 board_init_f 中的初始化步骤——初始化序列(init sequence)。将复杂的初始化过程分解为一系列独立的小函数,然后按顺序依次调用。这是一个函数指针数组,每个元素指向一个初始化函数:

/* common/board_f.c */
static const init_fnc_t init_sequence_f[] = {
    setup_mon_len,
#ifdef CONFIG_OF_CONTROL
    fdtdec_setup,
#endif
    initf_malloc,
    initf_bootstage,
    arch_cpu_init,          /* 架构级CPU初始化 */
    mach_cpu_init,          /* 机器级CPU初始化 */
    initf_dm,               /* 早期驱动模型初始化 */
    board_early_init_f,     /* 板级早期初始化 */
    timer_init,             /* 定时器初始化 */
    env_init,               /* 环境变量初始化 */
    init_baud_rate,         /* 波特率初始化 */
    serial_init,            /* 串口初始化 */
    console_init_f,         /* 控制台初始化 */
    display_options,        /* 显示启动信息 */
    display_text_info,
    announce_dram_init,
    dram_init,              /* DDR初始化 */
    setup_dest_addr,        /* 计算目标地址 */
    reserve_round_4k,       /* 预留空间并对齐 */
    reserve_mmu,            /* 预留MMU表空间 */
    reserve_video,          /* 预留显存 */
    reserve_malloc,         /* 预留malloc堆 */
    reserve_board_buf,      /* 预留板级缓冲 */
    reserve_global_data,    /* 预留新GD空间 */
    reserve_fdt,            /* 预留设备树空间 */
    reserve_stacks,         /* 预留栈空间 */
    setup_dram_config,
    reloc_fdt,
    setup_reloc,            /* 设置重定位参数 */
    NULL,
};

这种设计的优点:

  1. 清晰的执行顺序:初始化步骤以数组形式线性排列,一目了然。
  2. 易于维护:添加或删除初始化步骤只需修改数组。
  3. 统一的错误处理:每个函数返回0表示成功,非0表示失败,由框架统一检查。
  4. 可配置性:通过条件编译,不同配置可以包含不同的初始化步骤。

board_init_f执行入口

/* common/board_f.c */
void board_init_f(ulong boot_flags)
{
    gd->flags = boot_flags;
    gd->have_console = 0;

    if (initcall_run_list(init_sequence_f))
        hang();
}

这个函数的结构极为简洁:设置启动标志,然后通过initcall_run_list机制依次执行init_sequence_f数组中注册的所有初始化函数。如果任何一个初始化函数返回非零值(表示错误),系统将调用hang()` 永久挂起。

initcall机制详解

U-Boot借鉴了Linux内核的initcall设计思想,将启动过程分解为一系列独立的初始化函数,按顺序组织在一个函数指针数组中依次调用,任何一个失败则立即终止并报错。

int initcall_run_list(const init_fnc_t init_sequence[])
{
    ulong reloc_ofs;
    const init_fnc_t *ptr;
    init_fnc_t func;
    int ret = 0;

    for (ptr = init_sequence; func = *ptr, func; ptr++) {
        reloc_ofs = calc_reloc_ofs();

        if (reloc_ofs) {
            debug("initcall: %p (relocated to %p)\n",
                  (char *)func - reloc_ofs, (char *)func);
        } else {
            debug("initcall: %p\n", (char *)func - reloc_ofs);
        }

        ret = func();
        if (ret)
            break;
    }

    if (ret) {
        printf("initcall failed at call %p (err=%d)\n",
               (char *)func - reloc_ofs, ret);

        return ret;
    }

    return 0;
}

calc_reloc_ofs()函数用于获取当前的重定位偏移量,其返回值决定了 debug 日志中如何打印函数地址——是打印重定位后的地址,还是打印编译时的原始链接地址。开发者通过 func - reloc_ofs 运算,可以反推出编译时的原始地址,从而在 map 文件中精确定位是哪个函数。

U-Boot 2022年引入了 Event(事件)机制,这是对传统 initcall 的一种补充。传统方式下,所有初始化函数都以函数指针的形式硬编码在 init_sequence_f[]init_sequence_r[] 数组中。Event 机制允许在初始化序列中插入“事件触发点”,当执行到该点时,会触发所有注册了对应事件类型的回调函数。

内存布局规划

board_init_f 的一个核心任务是规划重定位后的内存布局。这个规划过程由一系列 reserve_* 函数完成,它们从RAM顶部开始,依次向下分配各功能区域。

这个布局由一系列 reserve_* 函数计算:

static int setup_dest_addr(void)
{
    gd->ram_top = gd->ram_base + gd->ram_size;
    gd->relocaddr = gd->ram_top;  /* 从顶部开始分配 */
    return 0;
}

static int reserve_malloc(void)
{
    gd->relocaddr -= CONFIG_SYS_MALLOC_LEN;
    gd->start_addr_sp = gd->relocaddr;
    return 0;
}

static int reserve_global_data(void)
{
    gd->relocaddr -= sizeof(gd_t);
    gd->relocaddr &= ~15;  /* 16字节对齐 */
    gd->new_gd = gd->relocaddr;
    return 0;
}

这种布局设计的精妙之处在于:

  1. U-Boot放在最高地址:为内核和ramdisk留出连续的低地址空间
  2. 栈在堆的下方:即使栈溢出,也不会覆盖U-Boot代码
  3. 16字节对齐:满足ARM ABI对栈指针的要求
  4. 4KB对齐:满足MMU页表的要求

board_init_f执行完init_sequence_f中所有函数后返回。此时控制流回到_main汇编代码(arch/arm/lib/crt0.S)的下半部分,系统已具备执行代码重定位的所有条件。

下一篇文章将详细分析 init_sequence_f 初始化序列中的各个函数,并在云栈社区继续我们的U-Boot探索之旅。




上一篇:注意力机制革新残差连接:MoRA架构如何让大模型训练更稳定高效?
下一篇:苏剑林第一视角:用层间注意力替换残差连接的探索历程与数学推导
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-3-21 06:29 , Processed in 0.831945 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表