第2章:U-Boot启动流程深度解析:从ROM Code到SPL再到U-Boot proper的三阶段启动

做瑞芯微平台开发,第一个要啃的硬骨头就是启动流程。很多刚入行的朋友拿着数据手册看半天,总觉得启动过程像个黑盒子——上电之后到底发生了什么?为什么有时候串口没输出,连个错误提示都没有?

我个人习惯,拿到一个新板子,第一件事就是把这套启动流程在脑子里过一遍。说白了,瑞芯微的启动就是一场接力赛:ROM Code跑第一棒,SPL跑第二棒,U-Boot proper跑最后一棒。每一棒交接的时候,都有可能出问题。

2.1 三阶段启动全景图

先看整体框架。我画了一张流程图,把这三个阶段的关系理清楚。你跟着箭头走一遍,心里就有底了。

瑞芯微三阶段启动流程图 瑞芯微平台三阶段启动流程 阶段一:ROM Code BootROM (芯片内部固化) 初始化时钟、DDR、存储控制器 加载SPL 阶段二:SPL Secondary Program Loader 初始化DDR、加载U-Boot proper 加载U-Boot 阶段三:U-Boot U-Boot proper 引导内核 存储介质:eMMC / NAND Flash / SD卡 / SPI Flash / USB(烧录模式) 启动设备选择逻辑(BootROM内部) 读取BootROM配置 扫描启动介质列表 校验IDB/签名 优先级:eMMC Boot1 → eMMC Boot2 → SD卡 → SPI Flash → USB下载 每个介质尝试读取IDB(Initial Data Block),校验通过则加载SPL 全部失败 → 进入MaskROM模式(USB下载等待)

这张图把整个流程串起来了。从上电到引导内核,中间经过三次跳转。每次跳转都伴随着硬件初始化的交接——ROM Code只做最基础的初始化,SPL把DDR跑起来,U-Boot proper才做完整的板级初始化。

2.2 ROM Code阶段:芯片的第一口气

芯片上电后,CPU从复位向量开始执行。这个复位向量指向哪里?指向芯片内部固化的BootROM,也就是ROM Code。这部分代码是芯片出厂就写死的,你改不了,也不需要改。

ROM Code干了三件事:

  • 基础硬件初始化:关闭看门狗、初始化系统时钟(通常是24MHz晶振起振)、配置PLL到安全频率
  • 启动设备检测:按照预设优先级扫描各个存储介质
  • 加载SPL:从找到的第一个合法介质中读取IDB(Initial Data Block),校验通过后加载到SRAM中执行

关键点:ROM Code阶段DDR还没有初始化!SPL必须放在SRAM中运行。瑞芯微平台的SRAM大小通常是8KB~32KB,这就限制了SPL的体积不能太大。我见过有人把SPL编译到100多KB,结果死活跑不起来——因为SRAM根本装不下。

2.3 SPL阶段:承上启下的关键角色

SPL(Secondary Program Loader)是U-Boot的一个精简版本。它的任务很明确:初始化DDR,然后把U-Boot proper从存储介质加载到DDR中。

为什么需要SPL?直接让ROM Code加载U-Boot不行吗?

原因很简单:U-Boot proper太大了。一个完整的U-Boot编译出来少说几百KB,而ROM Code能用的SRAM只有几十KB。所以必须有个中间人——SPL先把自己塞进SRAM,初始化DDR,再把大块头的U-Boot proper请进DDR。

SPL的执行流程大致如下:

  1. 从ROM Code跳转过来,此时CPU还在SRAM里运行
  2. 再次初始化时钟和PLL(ROM Code的配置可能比较保守)
  3. 初始化DDR控制器,配置时序参数
  4. 从存储介质读取U-Boot proper到DDR的指定地址
  5. 跳转到U-Boot proper的入口,把控制权交出去

个人经验:调试SPL阶段最痛苦的是DDR初始化。DDR时序参数配错了,SPL直接卡死,串口连个P都没有。我曾经在一块RK3399板子上折腾了三天,最后发现是DDR的ODT配置和硬件设计不匹配。建议你在移植时,先用瑞芯微官方的DDR工具生成参数,再手动核对一遍。

2.4 U-Boot proper阶段:真正的引导加载器

U-Boot proper就是我们通常说的U-Boot。它运行在DDR中,有完整的驱动框架、命令行界面、网络协议栈。它的主要工作是加载并启动Linux内核。

U-Boot proper的启动流程可以概括为:

步骤 说明 关键函数
1. 架构初始化 设置异常向量表、初始化MMU和Cache _start → reset
2. 板级初始化 调用board_init_f,初始化串口、定时器等基础外设 board_init_f
3. 重定位 将U-Boot自身从加载地址复制到DDR末尾的RAM区域 relocate_code
4. BSS清零 清零BSS段,准备C运行环境 board_init_r
5. 设备初始化 初始化所有注册的设备驱动(网卡、存储、显示等) initr_*系列函数
6. 进入主循环 启动命令行,等待用户输入或自动引导 main_loop

这里有个细节值得注意:重定位。U-Boot proper被SPL加载到DDR的某个地址(比如0x02000000),但这个地址不一定适合长期运行。U-Boot会把自己复制到DDR的高端地址区域,然后跳转过去继续执行。这样做的好处是给内核腾出连续的加载空间。

2.5 关键链接脚本分析

链接脚本决定了代码段的布局。U-Boot的链接脚本分布在多个目录下,但核心逻辑是相通的。我挑两个最重要的说。

2.5.1 SPL的链接脚本

SPL的链接脚本通常位于 arch/arm/mach-rockchip/u-boot-spl.lds。它的核心约束是:所有代码必须能在SRAM中运行。

MEMORY
{
    .sram : ORIGIN = CONFIG_SPL_TEXT_BASE, LENGTH = CONFIG_SPL_MAX_SIZE
}

SECTIONS
{
    .text : {
        *(.vectors)
        *(.text*)
    } > .sram

    .data : {
        *(.data*)
    } > .sram

    .bss : {
        *(.bss*)
    } > .sram
}

你看,所有段都放在.sram区域。CONFIG_SPL_TEXT_BASE是SPL的起始地址,CONFIG_SPL_MAX_SIZE是SPL的最大体积。这两个值必须和芯片的SRAM大小匹配。

避坑指南:我曾经在移植RK3568时,把CONFIG_SPL_MAX_SIZE设成了128KB,但芯片SRAM只有32KB。编译没报错,运行直接死。后来用objdump反汇编才发现,代码地址已经超出SRAM范围了。检查这个参数时,一定要看芯片手册里的SRAM大小。

2.5.2 U-Boot proper的链接脚本

U-Boot proper的链接脚本在 arch/arm/cpu/armv8/u-boot.lds(ARMv8架构)。它比SPL复杂得多,因为要处理重定位。

SECTIONS
{
    . = 0x00000000;

    .text : {
        *(.text*)
    }

    . = ALIGN(4);
    .rodata : {
        *(.rodata*)
    }

    . = ALIGN(4);
    .data : {
        *(.data*)
    }

    . = ALIGN(4);
    .got : {
        *(.got*)
    }

    . = ALIGN(4);
    .bss : {
        *(.bss*)
    }
}

注意这里的起始地址是0x00000000。这不是真正的物理地址,而是一个相对地址。U-Boot在重定位时会根据实际加载地址修正所有符号。这个机制叫PIC(位置无关代码),是U-Boot能灵活运行在不同地址的关键。

2.6 启动设备选择逻辑

瑞芯微的启动设备选择逻辑在ROM Code里实现。它的工作方式有点像BIOS的启动顺序检测。

具体流程是这样的:

  1. ROM Code读取芯片的启动引脚状态(BOOT_MODE寄存器),确定启动模式
  2. 按照预设的介质优先级依次尝试:eMMC Boot1 → eMMC Boot2 → SD卡 → SPI Flash → USB下载
  3. 对每个介质,读取IDB(Initial Data Block)的前512字节
  4. 校验IDB的签名和校验和,通过则加载SPL到SRAM
  5. 全部失败 → 进入MaskROM模式,等待USB下载

重要概念:IDB是瑞芯微特有的启动数据块。它包含SPL代码、DDR初始化参数、以及一个32字节的签名头。签名头里有魔数(0x0FF0A55A)、校验和、SPL大小等信息。ROM Code就是靠这个签名头来判断介质是否可启动的。

这个选择逻辑在实际调试中非常有用。比如你烧写了eMMC但启动失败,可以插一张SD卡来验证SPL是否正常。如果SD卡能启动,问题大概率出在eMMC的烧写流程上。

2.7 实践建议

说了这么多理论,最后给几条实在的建议:

  • 调试从串口开始:SPL和U-Boot proper都有串口输出。如果串口没任何输出,先检查硬件连接和时钟配置
  • 善用LED:在SPL早期代码里加GPIO点灯,比等串口输出快得多。我习惯在board_init_f开头点一个灯,确认SPL已经跑起来了
  • 保留ROM Code的调试信息:瑞芯微的ROM Code可以通过特定GPIO输出调试信息。具体方法看芯片手册,这个功能在排查启动设备检测问题时特别有用
  • 理解链接脚本的约束:SPL的大小、起始地址、DDR地址,这些参数必须在移植前确认清楚。改错一个,整个启动链就断了