第2章:U-Boot启动流程深度解析:从ROM Code到SPL再到U-Boot proper的三阶段启动
做瑞芯微平台开发,第一个要啃的硬骨头就是启动流程。很多刚入行的朋友拿着数据手册看半天,总觉得启动过程像个黑盒子——上电之后到底发生了什么?为什么有时候串口没输出,连个错误提示都没有?
我个人习惯,拿到一个新板子,第一件事就是把这套启动流程在脑子里过一遍。说白了,瑞芯微的启动就是一场接力赛:ROM Code跑第一棒,SPL跑第二棒,U-Boot proper跑最后一棒。每一棒交接的时候,都有可能出问题。
2.1 三阶段启动全景图
先看整体框架。我画了一张流程图,把这三个阶段的关系理清楚。你跟着箭头走一遍,心里就有底了。
这张图把整个流程串起来了。从上电到引导内核,中间经过三次跳转。每次跳转都伴随着硬件初始化的交接——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的执行流程大致如下:
- 从ROM Code跳转过来,此时CPU还在SRAM里运行
- 再次初始化时钟和PLL(ROM Code的配置可能比较保守)
- 初始化DDR控制器,配置时序参数
- 从存储介质读取U-Boot proper到DDR的指定地址
- 跳转到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的启动顺序检测。
具体流程是这样的:
- ROM Code读取芯片的启动引脚状态(BOOT_MODE寄存器),确定启动模式
- 按照预设的介质优先级依次尝试:eMMC Boot1 → eMMC Boot2 → SD卡 → SPI Flash → USB下载
- 对每个介质,读取IDB(Initial Data Block)的前512字节
- 校验IDB的签名和校验和,通过则加载SPL到SRAM
- 全部失败 → 进入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地址,这些参数必须在移植前确认清楚。改错一个,整个启动链就断了