1. ARM64平台U-Boot启动流程概述
在嵌入式Linux系统中,U-Boot作为最常用的Bootloader之一,承担着硬件初始化、引导操作系统内核的重要职责。ARM64架构的U-Boot启动流程与传统的ARM32架构有着显著差异,主要体现在异常级别(Exception Level)管理和多阶段引导设计上。
我曾在多个基于Cortex-A53/A72的ARM64平台上移植和调试U-Boot,发现其启动过程可以清晰地划分为两个阶段:SPL(Secondary Program Loader)和主U-Boot。这种设计主要是为了解决嵌入式系统启动时面临的内存限制问题。
关键点:现代ARM64 SoC通常内置小容量SRAM(几十KB到几百KB),而完整U-Boot镜像可能达到几百KB甚至MB级别,无法直接加载到SRAM运行。SPL作为精简版引导程序,只需完成最基础的硬件初始化(主要是DDR控制器),就能将主U-Boot加载到容量更大的DDR内存中。
2. SPL阶段深度解析
2.1 复位与异常级别初始化
当ARM64处理器上电复位时,会从芯片厂商预设的复位向量地址(常见的有0x0或0xFFFF0000)开始执行代码。根据我的实测经验,大多数BootROM会将CPU置于EL2异常级别(Hypervisor模式),这是ARMv8架构引入的重要特性。
c复制// 典型的异常级别切换代码示例(汇编)
switch_el:
mrs x0, CurrentEL
cmp x0, #0xC
b.eq 3f
cmp x0, #0x8
b.eq 2f
cmp x0, #0x4
b.eq 1f
3: ret
在SPL初始阶段,我们需要特别注意以下硬件状态配置:
- 关闭MMU和D-Cache:此时尚未建立内存映射,开启缓存会导致不可预知的访问异常
- 选择性开启I-Cache:可以显著提高代码执行效率
- 禁用中断和看门狗:防止启动过程被意外打断
2.2 早期硬件初始化(汇编实现)
在进入C语言环境前,必须通过汇编代码完成最基础的硬件环境搭建:
- 栈指针设置:在SRAM中划分栈空间(通常预留1-4KB),将sp寄存器指向SRAM高地址
- 串口初始化:配置UART时钟源、波特率(常见115200或921600)
- 时钟树配置:根据具体SoC调整CPU/总线时钟频率
assembly复制// ARM64汇编示例:栈指针初始化
ldr x0, =_stack_top // 获取栈顶地址
mov sp, x0 // 设置栈指针
调试技巧:在早期汇编阶段,可以通过向特定内存地址写入魔数(如0xDEADBEEF)来验证代码执行路径,配合JTAG调试器可以快速定位启动卡死问题。
3. 主U-Boot阶段详解
3.1 从SPL到主U-Boot的过渡
当SPL完成DDR初始化后,会将主U-Boot镜像从存储设备(如eMMC、SPI NOR)加载到DDR内存的指定位置。这个过程中有几个关键参数需要注意:
- 加载地址:必须与U-Boot链接脚本中的TEXT_BASE一致
- 镜像校验:通常会对u-boot.bin进行CRC32或SHA256校验
- 异常级别切换:根据需求决定是否从EL2降级到EL1
c复制// 典型的镜像加载代码
int spl_load_image(uint32_t offset, void *addr, size_t size)
{
struct mmc *mmc = find_mmc_device(0);
mmc_init(mmc);
return mmc->block_dev.block_read(0, offset, size, addr);
}
3.2 主U-Boot的C语言环境初始化
主U-Boot启动后,会经历以下关键流程:
- 重定位:将自身代码复制到链接地址(高地址内存)
- 全局数据结构初始化:gd_t、bd_t等
- 控制台系统初始化:实现printf等标准输出
- 驱动模型初始化:DM(Driver Model)子系统
c复制// 典型的板级初始化函数
int board_init(void)
{
/* 地址转换表初始化 */
enable_caches();
/* 关键外设时钟使能 */
clock_enable_periph_clock(PERIPH_ID_ETH);
/* GPIO默认状态配置 */
gpio_request(CONFIG_LED_GPIO, "sys_led");
gpio_direction_output(CONFIG_LED_GPIO, 1);
return 0;
}
4. 关键外设初始化流程
4.1 DDR控制器配置
DDR初始化是U-Boot中最容易出问题的环节之一。根据我的经验,需要特别注意:
- 时序参数校准:tRFC、tRP、tRCD等关键参数
- 阻抗匹配:需根据PCB设计调整ODT值
- 训练模式:部分高性能DDR4需要ZQ校准
c复制// 典型DDR初始化代码结构
void ddr_init(void)
{
/* 1. 复位DDR控制器 */
writel(0x1, DDR_CTRL_BASE + DDRCTRL_SRCTL0);
/* 2. 配置PHY参数 */
configure_ddr_phy();
/* 3. 执行内存训练 */
ddr_calibration();
/* 4. 启用DDR控制器 */
setbits_le32(DDR_CTRL_BASE + DDRCTRL_DFIMISC, 0x1);
}
4.2 存储设备初始化
U-Boot支持多种启动设备,常见初始化流程对比:
| 设备类型 | 初始化步骤 | 常见问题 |
|---|---|---|
| eMMC | 1. 时钟使能 2. 总线宽度设置 3. 切换高速模式 |
识别不到设备 CRC校验失败 |
| SPI NOR | 1. 片选配置 2. 时序参数设置 3. 4B地址模式使能 |
读写出错 擦除失败 |
| NAND | 1. ECC配置 2. 坏块检查 3. 时序调整 |
ECC纠错失败 坏块标记错误 |
5. 内核引导机制解析
5.1 设备树处理流程
现代U-Boot使用设备树(DTS)描述硬件信息,其处理流程包括:
- 编译时:将.dts编译为.dtb二进制文件
- 运行时:U-Boot可能动态修改设备树(如修正内存大小)
- 传递内核:将最终设备树地址写入x0寄存器(ARM64约定)
c复制// 典型的设备树修改示例
int fdt_fixup_board(void *blob)
{
int offset;
/* 修正内存节点 */
offset = fdt_path_offset(blob, "/memory");
fdt_setprop_u32(blob, offset, "reg", CONFIG_SYS_SDRAM_BASE);
/* 添加自定义属性 */
offset = fdt_path_offset(blob, "/soc");
fdt_setprop_string(blob, offset, "board-rev", "RevB");
return 0;
}
5.2 引导Linux内核
ARM64平台通过SMC或HVC指令进入EL2/EL3完成从U-Boot到Linux的切换:
- 准备参数:设备树地址、命令行参数、initrd信息
- 关闭缓存和MMU
- 跳转到内核入口点(通常Image头部有跳转指令)
c复制void boot_jump_linux(bootm_headers_t *images)
{
void (*kernel_entry)(void *fdt, void *res0, void *res1);
/* 获取内核入口点 */
kernel_entry = (void (*)(void*, void*, void*))images->ep;
/* 清理缓存 */
flush_cache_all();
/* 跳转执行 */
kernel_entry(images->ft_addr, NULL, NULL);
}
6. 调试技巧与常见问题
6.1 启动失败排查方法
根据我的调试经验,U-Boot启动问题可按以下流程排查:
- 确认执行到哪个阶段(通过LED或串口输出)
- 检查异常级别是否正确(通过CurrentEL寄存器)
- 验证内存访问(使用md/mw命令测试)
- 检查设备树兼容性(fdt list查看节点)
实用技巧:在关键函数入口添加串口打印,如:
printf("[%s] entry\n", __func__);
6.2 性能优化建议
- 启用I-Cache:在SPL早期阶段即可开启
- 优化重定位:减少拷贝数据量
- 并行初始化:对无依赖的外设同时初始化
- 延迟加载:非必要驱动延后加载
c复制// 缓存优化示例
void enable_caches(void)
{
/* 使能I-Cache */
icache_enable();
/* 仅在DDR初始化后使能D-Cache */
if (gd->flags & GD_FLG_RAM_READY)
dcache_enable();
}
7. 移植实践指南
7.1 新板卡支持要点
移植U-Boot到新ARM64平台时,需要重点关注:
- 创建板级目录:通常位于board/vendor/boardname
- 编写defconfig:指定CPU类型、外设驱动等
- 实现关键函数:
- lowlevel_init:早期汇编初始化
- board_init:外设基本配置
- dram_init:内存大���检测
makefile复制# 典型板级配置结构
board/
└── vendor/
└── boardname/
├── Kconfig
├── Makefile
├── board.c
└── dts/
└── board.dts
7.2 编译系统适配
现代U-Boot使用Kbuild系统,移植时需注意:
- 正确设置TEXT_BASE(链接地址)
- 配置DDR参数(通过头文件或Kconfig)
- 选择必要驱动(避免镜像过大)
bash复制# 典型编译命令
make BOARD_defconfig
make CROSS_COMPILE=aarch64-linux-gnu- -j8
经过多个项目的实践验证,理解U-Boot的完整启动流程对于嵌入式系统开发至关重要。特别是在调试启动失败问题时,掌握ARM64架构特性(如异常级别转换、缓存一致性)能够显著提高问题定位效率。建议开发者在实际项目中结合具体SoC文档和U-Boot源代码进行深入学习。
