RK SDK U-Boot 启动流程分析
1. U-Boot 多段式启动概述
目前多数 U-Boot 启动方案使用多段式加载启动 U-Boot:
BootROM --> Pre-Loader (TPL/SPL) --> U-Boot启动阶段每个过程讲解如下:
1.1 BootROM
这是芯片制造时固化在片内 ROM 中的代码,在芯片上电时首先被执行,负责根据启动配置查找启动介质并加载其中的 Pre-Loader 程序;BootROM 阶段还没有初始化 DRAM,只能使用片内 SRAM 作为运行内存。片内 SRAM 速度快但容量较小,通常无法容纳完整的 U-Boot,因此系统采用多段式加载启动。
1.2 Pre-Loader
Pre-Loader 由 BootROM 从存储设备中加载并执行,负责DDR初始化,建立最小运行环境,选启动介质,定位并读取下一级镜像等功能;(不同芯片厂商的Pre-Loader设计也不同,比如RK又将Pre-Loader分为TPL和SPL两部分,RK的TPL运行在SRAM只负责DRAM初始化,其他任务则由SPL负责,SPL运行在DRAM中)
1.3 U-Boot proper
U-Boot 由 SPL 加载到 DRAM 并运行。U-Boot 会完成重定位,初始化更多外设,并加载 Kernel、设备树以及可选的 initramfs;普通 ext4、UBIFS 或 NFS 根文件系统通常由 Linux 内核在启动过程中挂载,而不是由 U-Boot 直接加载。
2. ARM Trusted Firmware(TF-A/ATF)
ARM Trusted Firmware 是 Arm 处理器的可信启动与安全运行时固件框架。完整的 TF-A 参考启动架构可以包含负责加载后续镜像的 BL2,以及常驻 EL3 的 BL31;在当前 RK SDK 中,后续镜像实际由 DDR bin 和 U-Boot SPL 加载,Rockchip BL31 主要负责建立 EL3 运行环境、提供运行时服务并进入普通世界。
2.1 TrustZone、安全世界与普通世界
Arm 把系统分成了安全世界与普通世界两个执行环境:
- 安全世界(Secure World):用于运行 OP-TEE 等安全操作系统,处理密钥、加解密、安全存储和身份认证等敏感任务。
- 普通世界(Non-secure World):用于运行 U-Boot、Linux 和普通应用程序,负责设备管理、文件系统、网络通信等常规任务。
2.2 BL 阶段与异常级
| 启动阶段 | 异常级 | 所属世界 | 说明 |
|---|---|---|---|
| BL1 | EL3 | 安全世界 | 最早期可信启动固件 |
| BL2 | S-EL1 或 EL3 | 安全世界 | 平台初始化和镜像加载阶段,实际异常级由平台配置决定; |
| BL31 | EL3 | 安全世界 | 常驻运行时固件,提供电源管理、安全调用处理,并管理安全世界与普通世界之间的切换。 |
| BL32 | S-EL1 | 安全世界 | 【可选】可信操作系统,例如 OP-TEE。 |
| BL33 | NS-EL2 或 NS-EL1 | 普通世界 | 普通世界引导程序,例如 U-Boot、UEFI。 |
2.3 TF-A 通用启动流程
2.3.1 BL1:BootROM
芯片复位后,CPU 从 BootROM 开始执行。不同平台装载 BL2 的方式并不完全相同;在当前 RK U-Boot流程中,BootROM 先运行承担 DDR 初始化职责的 DDR bin,DDR 初始化完成并返回 BootROM 后,再由 BootROM 加载 SPL。
2.3.2 BL2:平台初始化
在 TF-A 参考架构中,BL2 完成 DRAM 等必要硬件的初始化,并按平台要求配置内存的安全访问权限,随后将 BL31、可选的 BL32 和 BL33 加载到内存中
准备完成后,BL2 将后续镜像的入口等信息传给 BL31,并跳转到 BL31 执行。此时 BL32 和 BL33 虽然已经加载到内存,但尚未开始运行。
2.3.3 BL31:EL3 运行时固件
BL31 设置 EL3 异常向量表,初始化安全监控和电源管理服务,为后续的 CPU 启动、休眠、复位以及安全世界切换做好准备。
如果存在 BL32,BL31 先进入 BL32,使安全操作系统完成初始化,完成后返回 BL31;如果没有 BL32,则直接准备进入 BL33。
2.3.4 BL32:可信操作系统(可选)
BL32 通常是 OP-TEE,负责初始化安全内存、页表、异常处理和可信服务等运行环境。
OP-TEE 是运行在安全世界中的小型安全操作系统,在 U-Boot 启动前初始化,并在 Linux 运行期间常驻,主要为可信应用提供隔离执行、密钥管理、密码学运算和安全存储等能力。Linux 应用通过libteec打开可信应用会话并提交命令,请求经/dev/tee*和内核 OP-TEE 驱动,通过共享内存传递参数、执行 SMC 指令进入 BL31,再由 BL31 切换到 OP-TEE 执行对应可信应用,完成后沿原路径返回结果。
2.3.5 BL33:普通世界引导程序
BL33 通常是 U-Boot 或 UEFI,负责进一步初始化设备、加载内核和设备树等启动文件,最终启动操作系统。
BL33 的异常级由平台配置决定,可以是 NS-EL2 或 NS-EL1;
3. Rockchip 启动路线与镜像组织
3.1 Rockchip 文档中的启动阶段
RK U-Boot启动方案也使用多段式加载启动U-Boot,与主线uboot不同的是,RK将Pre-Loader分为TPL和SPL两部分,以下是瑞芯微的说明文档:
+--------+----------------+----------+-------------+---------+ | Boot | Terminology #1 | Actual | Rockchip | Image | | stage | | program | Image | Location| | number | | name | Name | (sector)| +--------+----------------+----------+-------------+---------+ | 1 | Primary | ROM code | BootRom | | | | Program | | | | | | Loader | | | | | | | | | | | 2 | Secondary | U-Boot |idbloader.img| 0x40 | pre-loader | | Program | TPL/SPL | | | | | Loader (SPL) | | | | | | | | | | | 3 | - | U-Boot | u-boot.itb | 0x4000 | including u-boot and atf | | | | uboot.img | | only used with miniloader | | | | | | | | | ATF/TEE | trust.img | 0x6000 | only used with miniloader | | | | | | | 4 | - | kernel | boot.img | 0x8000 | | | | | | | | 5 | - | rootfs | rootfs.img | 0x40000 | +--------+----------------+----------+-------------+---------+3.2 Miniloader 与 U-Boot SPL 两种路线
Rockchip 常见的两种路线主要区别在于 DDR 初始化之后由谁继续加载后续固件:闭源 Miniloader 路线使用 Rockchip 提供的闭源 DDR bin 和 Miniloader,分别完成 DDR 初始化和后续固件加载;U-Boot SPL 路线使用闭源 DDR bin 完成 DDR 初始化,再由源码构建的 U-Boot SPL 加载后续镜像,便于自行编译、调试和修改。两条路线中的 DDR 初始化组件通常都不是开源的。两者后续进入 BL31、初始化可选的 BL32,再运行 BL33 的流程基本一致。下文使用“闭源 DDR bin + 开源 U-Boot SPL”路线分析 RK SDK U-Boot 启动流程。
4. RK U-Boot 的 BL1~BL33 映射
| 阶段 | 对应固件 | 主要任务 | 完成后的去向 |
|---|---|---|---|
| BL1 | Rockchip BootROM | 从启动介质加载早期程序 | 进入 DDR bin,并在其返回后加载 SPL |
| BL2 | DDR bin + U-Boot SPL | 初始化 DDR,加载 BL31、BL32、BL33,并按当前安全配置执行完整性或签名校验,准备交接参数 | 进入 BL31 |
| BL31 | Rockchip TF-A | 建立 EL3 运行环境、安全监控和电源管理服务 | 先初始化 BL32,再进入 BL33 |
| BL32 | OP-TEE | 建立可信运行环境,提供安全服务 | 初始化后返回 BL31,并保持常驻 |
| BL33 | U-Boot proper | 完成设备初始化,加载内核和设备树 | 启动 Linux |
4.1 BL1:BootROM
芯片上电或复位后,CPU 首先执行片内 BootROM。此时 DDR 尚未初始化,BootROM 从启动介质识别并读取早期引导镜像,将承担 TPL 功能角色的闭源 DDR bin 加载到片内 SRAM 中执行。DDR bin 完成 DRAM 初始化后返回 BootROM,BootROM 再将 U-Boot SPL 加载到 DRAM 中执行。
4.2 BL2:DDR bin + SPL
在本文对 BL 阶段的功能映射中,BL2 对应闭源 DDR bin 与源码构建的 U-Boot SPL。DDR bin 先完成 DRAM 初始化并返回 BootROM,随后 BootROM 加载并运行 SPL。
SPL 解析uboot.imgFIT,将 BL31、BL32 和 BL33 加载到各自的目标地址,并检查镜像完整性
4.3 BL31:EL3 安全监控环境
RK BL31使用闭源固件,BL31接收 SPL 提供的入口参数,建立 EL3 异常处理、上下文和运行时服务。如果存在 BL32 固件,则将执行控制权传递给可信操作系统 (OP-TEE) 镜像 BL32,BL32在初始化完成OP-TEE之后会返回BL31阶段,BL31再进入非安全世界的 BL33/U-Boot,如果没有
BL32 固件,则BL31直接进入非安全世界的 BL33/U-Boot。
4.4 BL32:OP-TEE
当前 RK SDK 使用预编译的 BL32/OP-TEE 镜像。BL32 本身就是安全载荷 OP-TEE,它会初始化自己的安全内存、页表、异常处理和可信服务环境,初始化完成后返回 BL31。
BL31 → BL32 初始化 → 返回 BL31 → BL33BL32 并不直接跳转到 U-Boot安全世界到普通世界的切换由 BL31 完成。
4.5 BL33:U-Boot proper
BL33 运行完整 U-Boot,完成设备初始化并启动 Linux。
5. BL33:U-Boot proper 详细流程
5.1 U-Boot 汇编入口
U-Boot 从_start开始执行,经过 ARMv8 早期汇编初始化后进入_main。
_main的主要工作是:
- 设置临时栈。
- 为全局数据结构
gd分配空间。 - 初始化临时
gd。 - 调用
board_init_f()。 - 重定位 U-Boot。
- 切换到新的栈和
gd。 - 清空
BSS。 - 调用
board_init_r()。
核心调用关系:
_start └─ reset └─ _main ├─ board_init_f_alloc_reserve() ├─ board_init_f_init_reserve() ├─ board_init_f() ├─ relocate_code() ├─ clear BSS └─ board_init_r()5.2board_init_f()
board_init_f()是重定位前的初始化阶段。
这一阶段运行时:
- U-Boot 还没有完成正式重定位。
- 可用堆空间有限。
- 使用临时栈和临时
gd。
主要流程可以整理为:
board_init_f() │ ├─ fdtdec_setup() │ └─ 定位 U-Boot 自带 DTB │ ├─ arch_cpu_init() │ └─ ARM/SoC 早期初始化 │ ├─ mach_cpu_init() │ └─ Rockchip 平台初始化 │ ├─ initf_dm() │ └─ 建立早期 Driver Model │ ├─ timer_init() ├─ env_init() ├─ serial_init() ├─ console_init_f() │ └─ 串口开始输出日志 │ ├─ dram_init() │ └─ 获取 DRAM 容量和布局 │ └─ 规划重定位空间5.2.1 重定位内存规划
U-Boot 从 DDR 顶部向下预留空间,包括:
- MMU 页表。
- Framebuffer。
- U-Boot 重定位后的代码。
- malloc 堆。
bd结构。- 新的
gd。 - 设备树副本。
- 新栈。
关键过程:
setup_dest_addr reserve_mmu reserve_video reserve_uboot reserve_malloc reserve_board reserve_global_data reserve_fdt reserve_stacks setup_reloc5.3 U-Boot 重定位
board_init_f()返回后,汇编代码调用:
relocate_code(gd->relocaddr)将 U-Boot 从最初的加载位置复制到规划好的 DDR 高地址。
重定位完成后:
- 修正代码和数据中的重定位项。
- 切换到新的栈。
- 切换到新的
gd。 - 清空重定位后映像的
BSS。 - 进入
board_init_r()。
初始 U-Boot 映像 │ │ relocate_code() ▼ DDR 高地址中的正式 U-Boot │ ├─ 新 gd ├─ 新栈 └─ 清空 BSS5.4board_init_r()
board_init_r()是 BL33 最核心的初始化阶段。它依次执行init_sequence_r[]中的函数,大致分为以下几组。
5.4.1 建立完整运行环境
initr_reloc initr_caches initr_reloc_global_data initr_malloc sysmem_initr interrupt_init主要完成:
- 启用和配置缓存。
- 更新重定位后的全局数据。
- 建立完整堆空间。
- 初始化 U-Boot 内存区域管理。
- 初始化中断相关环境。
5.4.2 建立初始 live tree 和驱动模型
initr_of_live() initr_dm()此时建立的是基于 U-Boot 自带 DTB 的 live tree 和 DM 设备。
5.4.3 建立早期最小环境变量
SDK 执行:
initr_env_nowhere()建立一个最小临时环境,其中的主要地址(以RK3576为例)为:
scriptaddr = 0x40500000 pxefile_addr_r= 0x40600000 fdt_addr_r = 0x48300000 kernel_addr_r = 0x40400000 kernel_addr_c = 0x45480000 ramdisk_addr_r= 0x4a2000005.5 板级初始化与 Kernel DTB V2
board_init_r()随后调用平台的board_init():
RK SDK 中的核心顺序是:
board_init() │ ├─ board_debug_init() ├─ optee_client_init() ├─ init_kernel_dtb() ├─ early_download() ├─ clks_probe() ├─ regulators_enable_boot_on() ├─ io_domain_init() ├─ set_armclk_rate() ├─ dvfs_init() └─ rk_board_init()5.5.1 初始化 OP-TEE 客户端
BL32/OP-TEE 已经在 BL33 之前启动。
这里的:
optee_client_init();不是重新加载 OP-TEE,而是让 U-Boot 建立与已经运行的 OP-TEE 之间的通信能力。
后续 U-Boot 可以通过:
U-Boot → SMC → BL31 → OP-TEE请求安全服务,例如:
- 读取安全启动状态。
- 读取 OEM 解锁状态。
- 访问安全存储。
- 配合 AVB 验证。
- 通知 OP-TEE U-Boot 阶段即将结束。
5.5.2 提前加载 Kernel DTB
init_kernel_dtb()会在进入命令行、执行bootcmd之前读取 Kernel DTB。
当前正常读取链路是:
boot 分区 └─ boot.img(FIT) └─ resource 子镜像 └─ rk-kernel.dtb │ └─ 加载到 fdt_addr_r=0x48300000随后执行:
- 检查 FDT 头。
- 检查 DTB hash(存在 hash 时)。
- 执行板级 early fixup。
- 在存在有效 DTBO 且满足条件时应用 DT overlay。
- 构建 Kernel DTB live tree。
- 扫描 Kernel DTB 设备。
这里还需要注意两个源码边界:当前dtb_check_ok()在比较根节点compatible之前直接返回成功,因此平台兼容性检查实际上没有执行;同时board_init()没有检查init_kernel_dtb()的返回值,所以 Kernel DTB 读取失败不一定会在此处立即停止 U-Boot。若读取在切换前失败,U-Boot 可能继续保留原来的 U-Boot DTB,后续能否正常启动取决于具体驱动和启动路径。
5.5.3 切换到存储环境变量
Kernel DTB 和启动介质初始化完成后,执行:
initr_env_switch()该函数先导出早期最小环境,然后销毁临时环境并从 eMMC 等介质导入持久化环境,接着将早期环境追加到存储环境中,部分同名变量会覆盖存储环境;bootargs另外使用追加逻辑处理。因此这里不是两个环境的简单无条件合并。
随后继续初始化:
- 串口。
- 控制台。
- 显示。
- USB。
- MMC。
- 网络。
- 存储设备。
- 命令系统。
- 其他驱动。
5.6main_loop()
board_init_r()的最后调用:
run_main_loop() └─ main_loop()main_loop()的流程是:
main_loop() │ ├─ cli_init() ├─ 执行 preboot ├─ bootdelay_process() ├─ autoboot_command() │ └─ 执行 bootcmd │ └─ 如果自动启动失败或被打断 └─ cli_loop(),进入 U-Boot 命令行执行默认bootcmd命令加载启动内核, 如果自动启动失败或被打断,则进入 U-Boot 命令行