二级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平台举例,它的启动路径大概是这样的:
- ROM代码:芯片内部固化的启动代码,从BootROM加载SPL到SRAM
- SPL阶段:初始化DDR、时钟、串口,然后从存储介质加载U-Boot proper
- U-Boot proper:完整的引导加载程序,提供命令行、环境变量、网络功能
- 启动内核:解析设备树、传递启动参数、跳转到内核入口
你可能会觉得,这步骤挺多啊。其实每个阶段都有它的道理。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中的环境变量区域!用setenv和saveenv命令操作。否则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的启动流程虽然看起来复杂,但理解了它的设计思路——分阶段初始化、环境变量驱动、命令行交互——你就能灵活应对各种硬件平台。我见过不少工程师,遇到启动问题就慌了,其实只要顺着启动日志一步步排查,大部分问题都能定位到。嗯,下次咱们聊聊设备树,那个才是真正让人头疼的东西。