2、嵌入式系统启动流程:从复位向量到Bootloader再到内核的完整启动链路,理解U-Boot的职责边界

嵌入式系统的上电启动过程是一个从硬件复位到操作系统运行的完整链路,涉及CPU架构定义、存储介质映射、外设初始化、内存管理等多个层面。理解这条链路中每个阶段的职责划分,是进行U-Boot移植和调试的前提。本章将从CPU复位后的第一条指令开始,逐步拆解启动的各个阶段,明确U-Boot在整个链路中的位置与边界——它既不是起点,也不是终点,而是连接硬件初始化与操作系统运行的关键桥梁。

嵌入式系统完整启动链路与U-Boot职责边界 阶段1: 复位向量 CPU上电/复位 PC=0x00000000 (ARM) / 0xFFFFFFF0 (x86) 阶段2: BootROM SoC内部固化代码 初始化时钟/存储控制器 从启动介质加载SPL 阶段3: SPL U-Boot第一阶段 DDR初始化 加载U-Boot主程序 阶段4: U-Boot 完整Bootloader 外设驱动/环境变量 加载内核/设备树 阶段5: Kernel 解压/初始化子系统 挂载根文件系统 启动init进程 阶段6: 用户空间 init → 服务/应用 Shell/业务程序 U-Boot职责边界:硬件初始化 → 加载内核 → 移交控制权 启动介质 NOR/NAND Flash eMMC/SD/SPI Flash 网络/USB/UART 内存布局 DDR初始化时序 U-Boot代码段/数据段 内核加载地址/设备树地址 外设初始化 UART/GPIO I2C/SPI/PCIe 以太网/USB BootROM负责最底层硬件引导,SPL负责DDR初始化,U-Boot主程序负责完整外设驱动与内核加载 内核接管后,U-Boot的使命结束,控制权完全移交 T0: 上电 T1: BootROM完成 T2: SPL完成 T3: U-Boot完成

2.1 启动链路全景:从CPU复位到用户空间的六个阶段

核心原理:CPU复位后的世界

当嵌入式系统上电或复位时,CPU的PC(程序计数器)会被硬件强制设置为一个固定地址。对于ARM Cortex-A系列处理器,这个地址通常是0x00000000(或由BOOT引脚配置的映射地址);对于x86架构则是0xFFFFFFF0。这个地址指向的并不是DDR内存——因为此时DDR尚未初始化——而是指向SoC内部固化的BootROM(只读存储器)或外部NOR Flash的起始地址。BootROM是芯片厂商在流片时写入的不可修改代码,其核心任务极其有限:初始化最基本的时钟、配置启动介质控制器、然后根据启动引脚(Boot Mode)的配置,从指定的存储介质中读取一小段代码到SRAM中执行。

这段被加载到SRAM的小代码,在传统U-Boot体系中被称为SPL(Secondary Program Loader,二级程序加载器)。SPL是U-Boot的精简版本,它解决了BootROM无法初始化DDR的困境——因为DDR的初始化需要复杂的时序训练和控制器配置,BootROM为了控制芯片面积和成本,通常只支持从介质中加载不超过几十KB的代码。SPL被设计为足够小(通常16KB~64KB),能够在SRAM中运行,完成DDR控制器的初始化,然后将完整的U-Boot主程序从存储介质加载到DDR中运行。

U-Boot主程序运行后,才进入我们熟悉的Bootloader世界:它拥有完整的命令行、环境变量、文件系统驱动、网络协议栈等。U-Boot的最终使命是加载Linux内核镜像(zImage/Image)和设备树文件(.dtb)到DDR的指定位置,设置好启动参数(bootargs),然后将控制权通过bootm或bootz命令移交给内核。内核接管后,U-Boot的代码就完成了历史使命,不再参与系统运行。

启动介质与加载流程

不同SoC支持的启动介质差异很大,常见的有:

启动介质 BootROM加载方式 典型场景 SPL大小限制
NOR Flash XIP(原地执行),CPU直接映射Flash地址 工业控制、高可靠性场景 无严格限制(但受SRAM约束)
NAND Flash BootROM读取前几KB到SRAM,需处理坏块 消费电子、大容量存储 通常≤4KB(受BootROM限制)
eMMC/SD BootROM通过SD/MMC控制器读取 手机、平板、开发板 通常≤64KB
SPI NOR Flash BootROM通过SPI控制器读取 路由器、IoT设备 通常≤32KB
网络(TFTP) BootROM内置网卡驱动(极少数SoC支持) 开发调试阶段 不适用(需U-Boot完整网络栈)

这里需要特别注意的是NAND Flash的启动场景。由于NAND存在坏块问题且不支持XIP,BootROM只能读取NAND的前几页(通常4KB~8KB)到SRAM。因此,SPL必须被设计为足够小,且需要包含NAND控制器的基本驱动。如果SPL超过BootROM的读取限制,系统将无法启动——这是移植U-Boot时最常见的"死机"原因之一。

关键认知:BootROM是SoC厂商固化的,用户无法修改。U-Boot移植的起点是SPL,而不是BootROM。SPL的代码量必须严格控制在目标SoC的SRAM大小和BootROM读取限制之内。

案例拆解:i.MX6ULL的启动链路分析

以NXP i.MX6ULL为例,这是一颗基于ARM Cortex-A7内核的工业级SoC,广泛用于工控和物联网设备。其启动链路如下:

  1. 上电复位:CPU从0x00000000取指,该地址映射到内部128KB ROM(BootROM)。
  2. BootROM执行:读取BOOT_MODE[1:0]引脚和eFUSE配置,确定启动设备。i.MX6ULL支持NOR、NAND、SD/eMMC、SPI NOR等启动方式。
  3. 加载SPL:BootROM从启动介质读取前4KB到SRAM(OCRAM,128KB),校验Header后跳转执行。i.MX6ULL的SPL包含DDR初始化代码(使用DCD表配置DDR控制器)。
  4. SPL初始化DDR:SPL根据DCD(Device Configuration Data)表中的参数配置DDR3/DDR3L控制器的时序、驱动强度、刷新率等。DCD表是NXP特有的机制,由imx8m_ddr_init或ddr3_init等工具生成。
  5. 加载U-Boot主程序:SPL将U-Boot镜像从SD卡(偏移1KB处)或NAND(偏移1MB处)读取到DDR的0x87800000地址,然后跳转执行。
  6. U-Boot主程序:完成串口、网络、Flash驱动初始化,进入命令行或自动执行bootcmd中的启动命令。

这个案例中值得关注的是DCD表的作用。i.MX6ULL的SPL本身不包含DDR初始化的完整代码,而是通过解析DCD表来配置DDR控制器。DCD表是一系列地址-数据对,直接写入DDR控制器的寄存器。这种设计使得DDR参数的调整不需要重新编译SPL,只需修改DCD表即可——但这也意味着DCD表错误会导致SPL在DDR初始化阶段崩溃,且难以调试。

2.2 U-Boot的职责边界:它做什么,不做什么

核心原理:U-Boot的"三不原则"

理解U-Boot的职责边界,可以用"三不原则"来概括:不负责CPU复位、不负责内核运行、不负责应用管理。U-Boot的起点是SPL被BootROM加载后,终点是内核开始运行(具体来说是内核解压完成并接管中断向量表)。在这个区间内,U-Boot负责:

  • 硬件初始化:DDR、UART、GPIO、I2C、SPI、以太网、USB、Flash等外设的驱动初始化。这些初始化是"最小可用"级别的——只要能满足加载内核的需求即可,不需要像Linux驱动那样完整。
  • 启动介质管理:从Flash、SD卡、网络等介质中读取内核镜像和设备树。U-Boot需要支持文件系统(FAT、ext4、UBIFS等)或裸分区读取。
  • 启动参数传递:通过bootargs环境变量向内核传递命令行参数,如console=ttyS0,115200、root=/dev/mmcblk0p2等。
  • 内核加载与跳转:将内核镜像复制到指定内存地址,设置CPU寄存器(如r0=0, r1=机器ID, r2=设备树地址),然后跳转到内核入口点。

U-Boot明确不负责的工作包括:

  • CPU复位向量和BootROM:这些由芯片厂商固化,U-Boot无法干预。
  • 内核的完整驱动初始化:U-Boot初始化外设只是为了自身使用(如串口打印日志、网卡下载镜像),内核启动后会重新初始化所有硬件。
  • 根文件系统的挂载和用户进程管理:这些是内核和init进程的职责。
  • 电源管理和深度休眠唤醒:这些通常由内核的电源管理框架处理,U-Boot只提供最基本的电源域初始化。

判断方法:如何界定U-Boot的移植范围

在实际项目中,判断某项功能是否属于U-Boot的移植范围,可以遵循以下流程:

  1. 问:这个功能是否在CPU复位后、内核启动前需要?如果是,则属于U-Boot范畴。例如,DDR初始化必须在U-Boot中完成,因为内核运行需要DDR。
  2. 问:这个功能是否影响U-Boot自身运行?例如,串口驱动是必须的,因为U-Boot需要打印日志;但GPU驱动则不需要,因为U-Boot不涉及图形显示(除非需要显示开机Logo)。
  3. 问:这个功能是否可以被内核接管?如果内核启动后会自动重新初始化该硬件,则U-Boot只需做最小初始化。例如,以太网控制器在U-Boot中只需支持TFTP下载即可,不需要完整的网络协议栈。
  4. 问:这个功能是否涉及安全启动?如果产品需要安全启动(Secure Boot),则U-Boot需要集成验签逻辑,验证内核镜像的签名后才允许加载。
经验提醒:在移植U-Boot时,不要试图在U-Boot中实现"完整"的驱动。U-Boot的驱动模型(Driver Model)虽然支持类似Linux的设备树匹配,但其目的是为了代码复用和配置灵活,而不是为了功能完备。U-Boot驱动只要能满足"加载内核"这个核心目标即可。

案例拆解:一个典型的启动日志分析

以下是一个基于Zynq UltraScale+ MPSoC平台的U-Boot启动日志(精简版),我们通过日志来理解各阶段的职责:

U-Boot SPL 2023.04-00001-g1234567 (Apr 01 2024 - 10:30:00 +0800)
PMUFW:  v1.1
EL3:    EL3 State: Non-Secure
EL2:    EL2 State: Non-Secure
Trying to boot from MMC1
spl: mmc boot: trying to boot from mmc1
spl_load_image_fat: reading u-boot.bin from mmc 1:1
spl: payload image: u-boot.bin, load addr: 0x8000000
spl: jumping to 0x8000000

U-Boot 2023.04-00001-g1234567 (Apr 01 2024 - 10:30:00 +0800)
CPU:   ZynqMP ZU7EV
Silicon: v3.1
DRAM:  4 GiB
MMC:   mmc@ff160000: 0, mmc@ff170000: 1
Loading Environment from FAT... OK
In:    serial@ff000000
Out:   serial@ff000000
Err:   serial@ff000000
Net:   eth0: ethernet@ff0e0000
Hit any key to stop autoboot:  0
switch to partitions #0, OK
mmc1 is current device
Scanning mmc 1:1...
Found U-Boot script /boot.scr
reading /boot.scr
## Executing script at 20000000
reading Image
reading system.dtb
## Loading kernel from FIT Image at 10000000 ...
   Using 'conf-zu7ev.dtb' configuration
   Trying 'kernel-1' kernel subimage
   Verifying Hash Integrity ... OK
   Trying 'fdt-1' fdt subimage
   Verifying Hash Integrity ... OK
## Loading ramdisk from FIT Image at 10000000 ...
   Trying 'ramdisk-1' ramdisk subimage
   Verifying Hash Integrity ... OK
   Loading ramdisk from 0x11000000 to 0x20000000
## Flattened Device Tree blob at 0x10000000
   Booting using the fdt blob at 0x10000000
Working FDT set to 0x10000000
   Loading Device Tree to 0x20000000, end 0x2000f000 ... OK
   Loading Kernel Image to 0x20080000, end 0x20ffffff ... OK
   Loading Ramdisk to 0x21000000, end 0x21ffffff ... OK
   Starting kernel ...

[    0.000000] Booting Linux on physical CPU 0x0000000000 [0x410fd034]
[    0.000000] Linux version 6.1.30 (build@server) (gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0) #1 SMP PREEMPT_DYNAMIC Mon Apr 1 10:00:00 UTC 2024
[    0.000000] Machine model: ZynqMP ZU7EV
[    0.000000] earlycon: uart0 at MMIO32 0x00000000ff000000 (options '115200n8')
[    0.000000] printk: bootconsole [uart0] enabled

从日志中可以清晰看到职责划分:

  • SPL阶段(前5行):初始化PMU固件、设置EL3/EL2状态、从MMC1的FAT分区读取u-boot.bin到地址0x8000000,然后跳转。
  • U-Boot主程序阶段(第7~25行):打印CPU信息、初始化DRAM(4 GiB)、初始化MMC、加载环境变量、初始化串口和网络。然后执行boot.scr脚本,从FIT镜像中验证并加载内核、设备树和ramdisk。
  • 内核启动阶段(第27行起):U-Boot打印Starting kernel ...后,控制权移交。内核打印Booting Linux on physical CPU,开始自己的初始化流程。

注意第25行Verifying Hash Integrity ... OK——这是FIT镜像的哈希校验,属于安全启动的一部分。U-Boot在加载内核前验证镜像的完整性,防止被篡改的镜像被执行。这是U-Boot职责边界内的重要安全功能。

2.3 启动链路中的关键交接点:U-Boot与内核的握手协议

核心原理:ARM架构下的启动约定

U-Boot跳转到内核时,并不是简单地"跳过去"就完事,而是需要遵循一套严格的ABI(应用二进制接口)约定。在ARM 32位架构下,U-Boot跳转内核时需要设置:

  • r0 = 0:必须为0,表示无参数。
  • r1 = 机器类型ID:在设备树(DT)时代,这个值通常被忽略,但传统ATAGS方式下需要传递。对于支持设备树的平台,r1可以设置为~0(即0xFFFFFFFF)。
  • r2 = 设备树物理地址:指向内存中有效的设备树二进制(DTB)的起始地址。
  • MMU关闭、D-Cache关闭:内核启动时要求MMU和D-Cache处于关闭状态,I-Cache可以开启也可以关闭。
  • 中断关闭:所有中断(IRQ和FIQ)必须被屏蔽。

在ARM 64位架构下,约定略有不同:

  • x0 = 设备树物理地址:直接传递DTB地址。
  • x1 = 0,x2 = 0,x3 = 0:保留,必须为0。
  • 异常级别:U-Boot通常运行在EL2(Hypervisor)或EL3(Secure Monitor),跳转内核时需切换到EL2或EL1。如果U-Boot在EL3运行,需要先切换到EL2再跳转。

这些约定定义在Linux内核的Documentation/arm/booting.rst和Documentation/arm64/booting.rst中。U-Boot的bootm命令会自动处理这些细节,但如果你手动编写跳转代码(例如在SPL中直接跳转内核),就必须严格遵守。

操作流程:bootm命令的执行过程

U-Boot的bootm命令是加载和启动内核的核心命令,其执行流程如下:

  1. 解析镜像头:检查镜像格式(Legacy uImage或FIT Image),读取镜像的加载地址、入口地址、类型等信息。
  2. 验证镜像:如果启用了安全启动,验证镜像的哈希或签名。
  3. 复制镜像:将内核镜像从当前地址复制到加载地址(如果两者不同)。
  4. 处理设备树:如果使用FIT镜像,提取DTB并复制到指定地址;如果使用独立DTB,则使用fdt addr命令设置的地址。
  5. 处理ramdisk:如果存在initrd/ramdisk,将其复制到指定地址。
  6. 设置启动参数:根据bootargs环境变量构造ATAGS或设备树中的chosen节点。
  7. 执行跳转:调用boot_jump_linux函数,设置寄存器并跳转到内核入口。

在U-Boot源码中,bootm命令的实现位于cmd/bootm.c,而具体的架构相关跳转代码位于arch/arm/lib/bootm.c(32位)和arch/arm/lib/arm64/board.c(64位)。理解这些代码的流程,对于调试"内核启动失败"问题至关重要。

风险提示:最常见的启动失败原因之一是设备树地址错误。如果U-Boot传递的DTB地址与内核期望的地址不一致,内核会在早期初始化阶段崩溃,且日志可能只显示Uncompressing Linux... done, booting the kernel后无任何输出。此时应检查fdt_addr环境变量和bootm命令的参数是否正确。

案例拆解:一次内核启动失败的排查过程

假设我们在一款基于Rockchip RK3568的板卡上遇到以下现象:U-Boot正常启动,执行bootm命令后,串口输出Starting kernel ...后无任何反应,系统挂死。

初步排查步骤:

  1. 确认U-Boot是否成功跳转:在U-Boot中执行go 0x200000(直接跳转到内核加载地址),观察是否有不同反应。如果同样挂死,说明问题在内核侧。
  2. 检查设备树地址:执行fdt addr查看当前DTB地址,确认与bootm命令中使用的地址一致。RK3568的典型配置是内核加载到0x200000,DTB加载到0x1f00000。
  3. 检查内核串口配置:确认bootargs中的console=参数与内核实际使用的串口一致。RK3568的调试串口通常是ttyS2,如果U-Boot使用的是ttyS0,内核日志会输出到错误的串口。
  4. 使用earlycon:在bootargs中添加earlycon=uart8250,mmio32,0xfe660000(RK3568 UART2的地址),让内核在早期就输出日志。

← 上一章 📖 返回目录 下一章 →