深入解析 U-Boot:从上电到引导内核的全流程技术剖析

前言:U-Boot 的核心定位

U-Boot(全称为 Das U-Boot,即 “The Universal Boot Loader”)是嵌入式世界中一款举足轻重、应用极为广泛的开源引导加载程序(Bootloader)。在嵌入式 Linux 生态系统中,它扮演着无可替代的角色,是连接底层硬件固件与上层操作系统内核的关键桥梁,已成为事实上的行业标准。

其核心使命可以高度概括为两大任务:

  1. 承上启下,初始化硬件:在操作系统内核获得控制权之前,U-Boot 负责完成系统硬件环境的全面初始化。这其中最关键的一步是建立并初始化 DRAM 内存,因为现代操作系统内核体积庞大,必须在足够容量的 DRAM 中才能运行。除此之外,它还包括时钟、电源管理单元(PMU)、总线等基础硬件的配置。

  2. 加载并启动操作系统:U-Boot 负责定位并读取存储介质(如 eMMC、SD 卡、NAND/SPI Flash、甚至网络)中的操作系统内核镜像及相关数据(如设备树 DTBinitramfs),将它们加载到 DRAM 的指定地址,最后以精确、约定的方式将 CPU 的控制权安全地交接给内核,从而启动整个操作系统。

第一章:U-Boot 的多阶段启动流程

现代 U-Boot 的启动流程并非一步到位,而是遵循一个清晰的多阶段模型。这种设计的根本原因源于芯片硬件的物理限制:SoC(系统级芯片)内部集成的高速静态内存(SRAM)容量非常有限(通常为几十到几百 KB),而外部动态内存(DRAM)容量则大得多(可达数 GB)。因此,必须有一个“先遣队”在有限的 SRAM 中运行,以完成 DRAM 的初始化,为完整功能的 U-Boot 创造运行空间。

结合芯片固化的 BootROM,完整的启动链条可以划分为以下几个核心阶段:

阶段别名/术语运行位置主要作用
阶段 0BootROM / BL1SoC 内部固化的 ROM硬件启动的根源。上电后执行的第一段代码,负责基础时钟和 SRAM 初始化,并从预设的启动介质中加载并验证下一阶段引导程序(SPL)。
阶段 1SPL / BL2芯片内部 SRAMDRAM 的初始化者。作为 U-Boot 的“先遣队”,核心使命是初始化外部 DRAM 控制器和 DRAM 芯片,从而极大扩展可用内存空间,并加载完整的 U-Boot 主体到 DRAM 中。
阶段 2U-Boot 主体 / BL3外部 DRAM功能完备的引导平台。负责全面的外设初始化(网络、存储、显示等),建立环境变量,提供交互式命令行,并最终加载、配置并启动操作系统内核。

第二章:启动时间线:从上电到内核的详细执行路径

现在,我们将上述阶段模型与具体的代码执行流程相结合,深入探究从 SoC 上电到 Linux 内核启动的每一步细节。

2.1 阶段 0:BootROM (BL1) - 硬件的苏醒

此阶段由芯片制造商在 SoC 内部的掩模 ROM (Mask ROM) 中硬编码,用户无法修改。当系统通电或复位时,处理器会从一个固定的地址(复位向量)开始执行 BootROM 中的代码。

  • 核心任务:

    1. 最小系统初始化:初始化 CPU 核心本身、基础时钟、看门狗以及内部 SRAM。

    2. 启动介质扫描:根据熔断丝(fuses)或 GPIO 引脚的配置,按照预设的优先级顺序(例如:先检查 SD 卡,再检查 eMMC,然后是 SPI Flash 等)扫描可引导设备。

    3. 加载与校验:在启动介质的特定偏移地址(如 0 地址或特定扇区)查找引导程序的头部信息。BootROM 会对头部进行合法性校验(如计算校验和)。

    4. 交接控制权:校验通过后,将下一阶段的引导程序(通常是 U-Boot 的 SPL 部分)从存储介质加载到芯片内部的 SRAM 中,然后将程序计数器(PC)跳转到 SPL 的入口地址,完成控制权交接。

跳转目标: U-Boot SPL 的入口点 (_start)。

2.2 阶段 1:SPL (BL2) - 在 SRAM 中搭建舞台

SPL (Secondary Program Loader) 是 U-Boot 的精简版,其设计目标是在资源极其有限的 SRAM 中完成关键任务。

  • 代码入口: arch/$(ARCH)/cpu/$(CPU)/start.S 文件中的 _start 标号。这是由链接脚本(如 u-boot-spl.lds)指定的整个 SPL 程序的入口。

  • 执行流程:

    1. 汇编阶段 (start.S):

      • 关闭中断:屏蔽所有中断,防止意外干扰。

      • 设置 CPU 模式:将处理器设置为管理模式(SVC Mode),以获得足够的权限。

      • 设置堆栈:在 SRAM 的高端地址设置一个临时的堆栈指针(SP),为即将到来的 C 函数调用做准备。

      • 清除 BSS 段_bss_start_bss_end 之间的内存区域(用于存放未初始化的全局变量和静态变量)被清零。

      • 跳转到 C 代码: 通过 bl board_init_f 指令,跳转到 SPL 的 C 语言入口函数。

    2. C 语言阶段 (spl.c, board_c.c 等):

      • board_init_f() 函数会执行一系列基本的初始化,其中最重要的任务是初始化 DRAM 控制器。这是 SPL 的核心使命。

      • 一旦 DRAM 可用,SPL 会继续初始化用于加载下一阶段代码的存储设备(如 eMMC 控制器)。

      • 最后,SPL 将完整的 U-Boot 主体镜像(u-boot.bin)从存储介质加载到刚刚初始化完成的 DRAM 的某个临时地址。

      • 完成加载后,SPL 会跳转到 DRAM 中 U-Boot 主体的入口点,正式结束自己的历史使命。

跳转目标: U-Boot 主体在 DRAM 中的入口点 (_start)。

2.3 阶段 2:U-Boot 主体 (BL3) - 在 DRAM 中全面展开

完整的 U-Boot 在宽敞的 DRAM 中运行,此时它才能执行所有复杂的功能。其执行流程巧妙地分为两个子阶段:重定位前(Pre-relocation)和重定位后(Post-relocation)。

2.3.1 子阶段 A:重定位前 (board_init_f) - 准备最终运行环境

U-Boot 主体被 SPL 加载到 DRAM 的地址可能不是其最终运行的地址(链接地址)。“重定位”就是将 U-Boot 自身代码从加载地址拷贝到链接地址的过程。board_init_f 阶段的代码就是为此做准备。

  • 代码入口: 同样是 arch/$(ARCH)/cpu/$(CPU)/start.S 中的 _start,但这是 U-Boot 主体镜像的 _start

  • 文件: 主要逻辑在 common/board_f.c

  • 主要任务:

    1. 初始化早期外设:通常会初始化一个串口,以便能够尽早输出调试信息。

    2. 初始化全局数据结构 gd:分配并初始化一个名为 global_data 的关键数据结构,它像一个“全局变量总线”,用于在 U-Boot 的不同子系统和阶段之间传递信息。

    3. 执行 init_sequence_f[]:这是一个函数指针数组,包含了重定位前必须执行的一系列初始化函数,如内存控制器(此时是确认并记录DRAM大小)、定时器等。

    4. 计算并准备重定位:确定代码的当前加载地址和最终链接地址,为搬移做准备。

    5. 执行重定位:调用 relocate_code() 将 U-Boot 自身的 .text, .data 等段从加载地址拷贝到链接地址。

    6. 清空 BSS 段:再次对重定位后的 BSS 段进行清零。

    7. 跳转到下一阶段:最后,跳转到重定位后的 C 代码入口 board_init_r

跳转目标: board_init_r(),位于重定位后地址的 U-Boot C 代码入口。

2.3.2 子阶段 B:重定位后 (board_init_r) - U-Boot 主逻辑阶段

board_init_r (r for relocation) 是 U-Boot 的“main”函数,标志着 U-Boot 所有功能的完全启动。

  • 文件: 主要逻辑在 common/board_r.c

  • 主要任务:

    1. 全面硬件初始化:执行 init_sequence_r[] 函数指针数组,对系统中几乎所有的外设进行初始化,包括但不限于:网络接口(Ethernet)、存储控制器(NAND/eMMC/SPI)、USB、I2C/SPI 总线、显示控制器等。

    2. 环境变量加载:从 Flash 或其他非易失性存储中读取并解析环境变量。这些变量(如 bootcmd, bootargs, serverip)是 U-Boot 行为配置的核心。

    3. 控制台与命令行建立:初始化完整的控制台,使 U-Boot 的交互式命令行接口完全可用。用户此时可以通过串口与 U-Boot 交互。

    4. 执行自动引导 (autoboot):启动一个倒计时。如果在倒计时结束前用户没有按下任何键中断,U-Boot 将自动执行 bootcmd 环境变量中定义的命令序列,开始加载并引导操作系统内核。

  • 关键逻辑: common/autoboot.c 中的 autoboot_command() 负责解析并执行 bootcmd

第三章:U-Boot 加载内核与设备树的终极使命

执行 bootcmd 是 U-Boot 引导流程的最高潮。一个典型的 bootcmd 如下所示:

bootcmd=mmc dev 0; fatload mmc 0:1 ${kernel_addr_r} Image; fatload mmc 0:1 ${fdt_addr_r} xxx.dtb; booti ${kernel_addr_r} - ${fdt_addr_r}

这条命令清晰地展示了加载内核与设备树的步骤:

  1. mmc dev 0: 切换到 eMMC/SD 卡设备 0。

  2. fatload mmc 0:1 ${kernel_addr_r} Image: 从设备 0 的分区 1(FAT格式)中,加载 Image 文件(内核镜像)到环境变量 kernel_addr_r 指定的 DRAM 地址。

  3. fatload mmc 0:1 ${fdt_addr_r} xxx.dtb: 同样,加载 xxx.dtb 文件(设备树)到 fdt_addr_r 指定的 DRAM 地址。

  4. booti ${kernel_addr_r} - ${fdt_addr_r}: 执行引导命令。

3.1 内核与设备树的加载与引导

当用户输入或 bootcmd 自动执行 booti / bootm / bootz 等命令时,U-Boot 内部会执行一系列复杂的函数调用。

  • 对应文件: cmd/booti.c (用于 ARM64 Image 格式), cmd/bootz.c (用于 ARM zImage 格式), common/bootm.c (一个通用的、支持多种格式的旧命令)。

  • 函数逻辑:

    1. 镜像检查与解析:检查指定地址处的镜像头部,识别其类型(如 uImage、zImage、FIT Image、PE/COFF 等)和属性(架构、压缩方式等)。

    2. 解压缩(如果需要):如果内核是压缩的(如 zImagegzip 压缩的 Image),U-Boot 会在内存中对其进行解压。

    3. 设备树处理

      • U-Boot 会读取用户加载的 DTB 文件。

      • 动态修改 (FDT Manipulation):这是非常关键的一步。U-Boot 会向设备树中添加或修改信息。例如,它会在 /chosen 节点下创建 bootargs 属性,将 U-Boot 的 bootargs 环境变量的值写入其中,供内核解析。它还会添加内存保留区信息,告知内核哪些内存已被 U-Boot 使用。

    4. 设置引导参数:根据目标架构,准备好传递给内核的参数。对于现代 ARM 系统,这主要是准备好存放 DTB 地址的寄存器。

    5. 跳转至内核:最后,关闭缓存和 MMU,将 DTB 的地址放入指定的寄存器(如 ARM32 的 r2,ARM64 的 x0),然后执行一条跳转指令,将 CPU 控制权交给内核的入口地址。

boot_jump_linux(kernel_entry, machid, r2); // 示例跳转调用

至此,U-Boot 的使命宣告完成,波澜壮阔的 Linux 内核启动过程正式开始。

第四章:总结与关键索引

4.1 提炼后的 U-Boot 启动流程图

[SoC 上电]
    └──> [BootROM (BL1) in ROM] - 最小化初始化,加载 SPL
           ↓
           [SPL (BL2) in SRAM]
               ├──> [_start in start.S] - 设置汇编环境 (栈, CPU模式)
               │      ↓
               └──> [board_init_f in C] - 初始化 DRAM,加载 U-Boot 主体到 DRAM
                      ↓
                      [U-Boot Main Body in DRAM]
                          ├──> [Pre-Relocation: _start -> board_init_f] - 准备环境,自我搬移
                          │      ↓ (Relocate & Jump)
                          ├──> [Post-Relocation: board_init_r] - 全面初始化外设,加载环境变量,启动命令行
                          │      ↓
                          └──> [autoboot executes 'bootcmd'] - 自动执行引导命令
                                 ↓
                                 [Commands: load, booti/bootm/bootz]
                                     ├──> 从存储加载 Kernel + DTB 到 DRAM
                                     ├──> 解析镜像,处理设备树 (FDT)
                                     └──> 设置引导参数,准备跳转
                                           ↓
                                           [Handover to Kernel] -> 内核开始执行

4.2 关键代码文件索引

功能主要相关文件备注
启动入口 (汇编)arch/$(ARCH)/cpu/$(CPU)/start.SSPL 和 U-Boot 主体都有各自的 _start 入口。
SPL C 语言逻辑common/spl/spl.cSPL 的 C 语言主流程。
重定位前初始化common/board_f.cboard_init_f 的实现。
重定位后主逻辑common/board_r.cboard_init_r 的实现。
内核启动命令cmd/booti.c, cmd/bootz.c, common/bootm.cbooti, bootz, bootm 命令的实现。
设备树处理 (FDT)lib/fdtdec.c, common/fdt_support.c设备树的解析、读取和修改功能。
自动引导逻辑common/autoboot.cbootcmd 的倒计时与执行。

4.3 U-Boot 的核心职责精炼

总而言之,U-Boot 的职责远非简单的“加载文件”。它是一个承上启下、高度复杂的系统软件。其核心职责可精炼为三点:

  1. 承接硬件:从一个仅有 CPU 和片上 ROM 的“裸机”环境开始,逐步唤醒并初始化DRAM、时钟、电源等核心硬件资源,为上层软件的运行搭建起坚实的物理舞台。

  2. 建立环境:通过初始化各类外设,建立起一套灵活的环境变量机制和功能强大的交互式命令行,为开发者提供一个可调试、可配置、可扩展的引导平台。

  3. 启动系统:作为其最终目标,精确地加载操作系统内核及其所需的数据(如设备树),并严格遵循体系架构的规范,将系统控制权完美、安全地交接给操作系统。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值