二级Bootloader(U-Boot):启动流程、命令行交互与环境变量

好,咱们今天聊聊U-Boot。说实话,在嵌入式Linux开发中,U-Boot几乎就是事实标准。我这些年经手的项目,从ARM到RISC-V,从几百兆的MCU到多核应用处理器,底层引导清一色都是它。你可能会问:为什么非要用U-Boot?直接让Linux内核自己启动不行吗?

嗯,这里有个关键问题——硬件初始化。CPU上电后,DDR控制器还没配置,时钟树还是默认状态,Flash控制器也没初始化。这时候你连内存都没有,怎么加载内核?所以我们需要一个"中间人",先把硬件环境搭好,再把内核请进来。这个中间人,就是U-Boot。

U-Boot的启动流程

U-Boot的启动流程,说白了分两阶段:SPL(Secondary Program Loader)和U-Boot proper。不过现在很多芯片把SPL也省了,直接一个完整的U-Boot搞定。我拿最典型的ARM64平台举例,它的启动路径大概是这样的:

  1. ROM代码:芯片内部固化的启动代码,从BootROM加载SPL到SRAM
  2. SPL阶段:初始化DDR、时钟、串口,然后从存储介质加载U-Boot proper
  3. U-Boot proper:完整的引导加载程序,提供命令行、环境变量、网络功能
  4. 启动内核:解析设备树、传递启动参数、跳转到内核入口

你可能会觉得,这步骤挺多啊。其实每个阶段都有它的道理。SPL体积小,能塞进芯片内部那点可怜的SRAM里。等DDR初始化好了,U-Boot proper才能舒舒服服地跑在内存里。

关键点:SPL和U-Boot proper的链接地址不同。SPL通常在SRAM中运行,U-Boot proper在DDR中运行。这两个阶段的代码编译时就要指定不同的链接脚本。

我记得有一次调试一块新板子,SPL能正常打印,但U-Boot proper就是加载不起来。查了半天,发现是DDR时序参数配错了。嗯,这种问题最头疼——因为DDR没初始化好,你连调试手段都有限。

命令行交互

U-Boot启动后,默认会等待几秒钟,让你按任意键进入命令行。这个等待时间由bootdelay环境变量控制。我个人习惯设成3秒,太长浪费时间,太短来不及打断。

进入命令行后,你面对的是一个精简版的shell。常用的命令我列一下:

命令 功能 我常用的场景
printenv 打印所有环境变量 检查启动参数是否正确
setenv 设置环境变量 修改bootargs或服务器IP
saveenv 保存环境变量到存储介质 修改后一定要保存,否则重启就丢了
tftp 通过TFTP下载文件 开发阶段最常用,不用反复烧写Flash
bootm 启动内核镜像 手动启动内核时用
fatload 从FAT分区加载文件 从SD卡或eMMC加载内核

举个例子,我经常这样手动启动内核:

# 设置服务器IP和板子IP
setenv serverip 192.168.1.100
setenv ipaddr 192.168.1.10

# 通过TFTP下载内核和设备树
tftp 0x80000000 zImage
tftp 0x83000000 board.dtb

# 设置启动参数
setenv bootargs console=ttyS0,115200 root=/dev/mmcblk0p2 rw

# 启动内核
bootm 0x80000000 - 0x83000000

小技巧bootm命令的三个参数分别是:内核地址、ramdisk地址(没有就填-)、设备树地址。这个顺序我经常记混,后来干脆写了个脚本,每次执行run bootcmd就完事了。

环境变量与启动参数传递

环境变量是U-Boot的灵魂。说白了,它就是一组键值对,存储在Flash或eMMC的特定分区里。U-Boot启动时会读取这些变量,决定怎么启动内核。

最重要的几个环境变量:

  • bootcmd:自动启动时执行的命令序列。比如fatload mmc 0:1 0x80000000 zImage; bootm 0x80000000
  • bootargs:传递给内核的命令行参数。内核启动时会解析这个字符串
  • bootdelay:自动启动前的等待秒数
  • baudrate:串口波特率,默认通常是115200

这里有个坑,我曾经踩过好几次——环境变量的存储格式。U-Boot默认用CRC校验来保护环境变量。如果你手动修改了Flash里的数据,没更新CRC,U-Boot会认为环境变量损坏,然后使用默认值。嗯,那时候你可能会发现,明明改了bootargs,重启后却还是老样子。

警告:永远不要直接修改Flash中的环境变量区域!用setenvsaveenv命令操作。否则CRC校验失败,所有修改都会丢失。

启动参数传递这块,U-Boot通过bootargs环境变量把信息传给内核。内核启动时,会解析这个字符串,获取根文件系统位置、控制台设备、内存大小等信息。典型的bootargs长这样:

console=ttyS0,115200 root=/dev/mmcblk0p2 rw rootfstype=ext4 init=/sbin/init

每个参数的含义:

  • console:指定控制台设备,内核的printk信息会从这里输出
  • root:根文件系统所在设备
  • rw:以读写方式挂载根文件系统
  • rootfstype:文件系统类型,告诉内核用什么驱动来挂载
  • init:第一个用户态进程,通常是/sbin/init

你可能会问:这些参数非得手动设吗?其实不用。U-Boot支持从设备树(Device Tree)中自动生成部分参数。比如内存大小、CPU数量这些信息,设备树里都有,U-Boot可以自动拼接成bootargs。我个人习惯是,硬件相关的参数放设备树,软件相关的参数(比如控制台、根文件系统)放bootargs。

实战建议:开发阶段用TFTP加载内核,方便调试。量产阶段把内核烧写到Flash或eMMC,用bootcmd自动加载。环境变量里可以保留一个备用启动方案,万一主方案失败,还能从网络启动救急。

最后说一句,U-Boot的启动流程虽然看起来复杂,但理解了它的设计思路——分阶段初始化、环境变量驱动、命令行交互——你就能灵活应对各种硬件平台。我见过不少工程师,遇到启动问题就慌了,其实只要顺着启动日志一步步排查,大部分问题都能定位到。嗯,下次咱们聊聊设备树,那个才是真正让人头疼的东西。