3、U-Boot详解:U-Boot的启动流程、环境变量、设备树传递与内核引导

好,咱们今天聊聊U-Boot。说实话,在Android系统启动这条链上,U-Boot是个承上启下的角色。它不像BootROM那样藏在芯片内部,也不像Linux内核那样庞大复杂。但它干的事,恰恰是最容易出问题的一环。

我个人习惯把U-Boot比作一个「临时管家」。芯片上电后,BootROM把这位管家请出来,管家负责把家里的灯(硬件)都点亮,然后去请真正的主人——Linux内核。主人来了,管家就退场了。就这么简单,但细节里全是坑。

3.1 U-Boot的启动流程:从SPL到Main U-Boot

U-Boot的启动不是一步到位的。现在的芯片越来越复杂,SRAM空间就那么点,根本放不下完整的U-Boot。所以分了两步走:

  • SPL(Secondary Program Loader):第一级加载器,体积小,负责初始化DDR和基本外设
  • Main U-Boot:完整的U-Boot,功能齐全,从DDR中运行

流程大致是这样的:

ROM Code → SPL → Main U-Boot → Linux Kernel

我在项目中遇到过一个问题:SPL阶段DDR初始化失败,导致Main U-Boot根本加载不进来。查了半天,发现是DDR时序参数配错了。嗯,这里要注意,SPL阶段的代码通常跑在SRAM里,空间极其有限,所以调试手段也很有限。我建议你在开发板上先确认SPL能正常打印日志,再往下走。

关键点:SPL的职责就是「把Main U-Boot请进DDR」。如果这一步卡住了,后面的所有事情都免谈。

3.2 环境变量:U-Boot的配置中心

U-Boot的环境变量,说白了就是一个键值对数据库。它存储在存储介质上(比如eMMC、NAND Flash),U-Boot启动时会加载到内存里。你可以在U-Boot命令行里用printenv查看,用setenv修改。

常用的环境变量有:

变量名 作用
bootcmd 自动启动时执行的命令,通常就是引导内核
bootargs 传递给内核的命令行参数
loadaddr 加载内核镜像到内存的地址
fdt_addr 设备树在内存中的地址
mmcdev 当前使用的MMC设备号

我曾经踩过一个坑:修改了bootargs后,忘记保存,结果下次启动还是用的旧参数。U-Boot的环境变量修改后,需要用saveenv命令保存到存储介质中。不然断电就丢了。你想想看,调试了半天发现是这个问题,是不是很崩溃?

小技巧:我习惯在bootcmd里加一个echo打印当前使用的环境变量版本号,方便排查问题。

3.3 设备树传递:U-Boot如何把硬件信息交给内核

设备树(Device Tree,简称DTB)是U-Boot和内核之间的「硬件说明书」。U-Boot负责把DTB加载到内存中,然后把它的地址通过寄存器(比如x0/r0)传递给内核。

传递过程大致如下:

  1. U-Boot从存储介质中读取DTB文件(通常是.dtb格式)
  2. 将DTB加载到内存中的指定地址(比如0x82000000
  3. 在跳转到内核之前,将DTB地址写入寄存器
  4. 内核启动后,从寄存器中读取DTB地址,解析硬件信息

这里有个容易忽略的点:DTB的加载地址不能和内核镜像的加载地址重叠。我见过有人把内核加载到0x80000000,DTB也放到0x80000000,结果内核启动时把DTB覆盖了,系统直接崩溃。

# 典型的U-Boot引导命令
setenv loadaddr 0x80000000
setenv fdt_addr 0x82000000

# 从eMMC加载内核和DTB
load mmc 0:1 ${loadaddr} /boot/Image
load mmc 0:1 ${fdt_addr} /boot/board.dtb

# 启动内核
booti ${loadaddr} - ${fdt_addr}

警告:不同架构的启动命令不同。ARM64用booti,ARM32用bootm,RISC-V用bootelf。别搞混了,我刚开始做的时候就在这上面浪费了半天。

3.4 内核引导:U-Boot的最后一步

当U-Boot完成了所有准备工作,就该跳转到内核了。这一步其实很简单,就是把CPU的控制权交给内核的入口地址。但简单不代表不重要。

U-Boot在跳转前需要做几件事:

  • 关闭MMU和Cache(有些架构需要)
  • 设置CPU为SVC模式
  • 清零BSS段
  • 设置栈指针
  • 传递参数(设备树地址、内核入口等)

我记得有一次,内核启动后一直卡在Uncompressing Linux...这一步。查来查去,发现是U-Boot在跳转前没有关闭D-Cache,导致内核解压时数据不一致。说白了,就是U-Boot的Cache和内核的Cache冲突了。

所以,我建议你在移植U-Boot时,仔细检查arch/arm/lib/bootm.c或对应架构的启动代码,确保跳转前的状态是干净的。

核心原则:U-Boot在跳转内核前,应该把自己「擦干净」。就像管家离开前,要把房间收拾好一样。

3.5 实战中的常见问题

最后,我总结几个我在项目中遇到的高频问题,希望能帮你少走弯路:

  • DTB版本不匹配:U-Boot使用的DTB和内核版本不一致,导致某些外设无法识别。解决办法是确保DTB和内核来自同一份源码编译。
  • 环境变量损坏:存储介质坏块或意外断电导致环境变量丢失。我建议在量产时使用默认环境变量,并开启冗余备份。
  • 启动延迟过长:U-Boot在等待用户输入时超时时间太长。可以修改CONFIG_BOOTDELAY为0或1,加快启动速度。
  • 内存地址冲突:内核、DTB、ramdisk的加载地址重叠。建议在项目初期就规划好内存布局,并写进文档里。

嗯,U-Boot这部分内容其实挺多的,但核心就是这几块。你只要把启动流程、环境变量、设备树传递和内核引导这四个环节搞清楚了,大部分问题都能迎刃而解。下次咱们聊聊Linux内核的启动,那才是真正的大头。