2、U-Boot基础:编译、配置与烧录,SPL与U-Boot proper的区别
好,咱们正式开始啃U-Boot这块硬骨头。很多做嵌入式的朋友,一提到U-Boot就觉得头大——代码量大、配置复杂、动不动就起不来。其实说白了,U-Boot就是一个裸机程序,只不过它要干的事情特别多:初始化硬件、加载内核、传递参数。今天我就带你把它拆开揉碎了讲清楚。
2.1 什么是U-Boot?它到底在干什么?
U-Boot的全称是Universal Boot Loader,通用引导加载程序。它负责把操作系统从存储介质(比如eMMC、NAND Flash、SD卡)里读出来,放到内存里,然后跳转过去执行。
我习惯把U-Boot比作一个“中间人”。上电后,CPU先跑一段固化在ROM里的代码(叫ROM Code),这段代码把U-Boot加载到SRAM里。U-Boot跑起来后,初始化DDR、时钟、存储控制器,最后把内核加载到DDR里,把控制权交给内核。整个过程就像接力赛,一棒传一棒。
核心要点:U-Boot不是操作系统,它只是一个裸机程序。它的生命周期很短——从上电到内核启动完成,它就退出了。
2.2 SPL与U-Boot proper:为什么需要两个阶段?
这个问题我当年刚接触时也困惑过。为什么不能一个U-Boot搞定所有事?原因很简单:片上SRAM太小了。
很多SoC内部的SRAM只有几十KB到几百KB,根本放不下完整的U-Boot(通常几百KB到几MB)。所以设计者想了个办法:分两阶段启动。
| 阶段 | 名称 | 存储位置 | 大小 | 主要任务 |
|---|---|---|---|---|
| 第一阶段 | SPL (Secondary Program Loader) | 片上SRAM | 16KB ~ 256KB | 初始化DDR、时钟,加载U-Boot proper到DDR |
| 第二阶段 | U-Boot proper | DDR | 200KB ~ 2MB | 完整初始化外设、加载内核、提供命令行 |
说白了,SPL就是个“小不点”,它只做最必要的事:把DDR初始化好,然后把U-Boot proper从Flash里读到DDR里,最后跳转过去。U-Boot proper才是那个功能完整的大家伙。
我的经验:调试启动问题时,先看SPL能不能跑起来。SPL起不来,后面全是白搭。我曾经在一个项目里,SPL死活加载不到DDR,查了两天才发现是DDR时序参数配错了。
2.3 U-Boot的编译与配置
编译U-Boot其实不复杂,但配置这一步容易踩坑。不同厂商、不同板子,配置方式千差万别。我一般按这个流程走:
- 选择目标板配置:
make <board_name>_defconfig - 自定义配置(可选):
make menuconfig - 编译:
make -j$(nproc) - 检查输出:查看生成的
u-boot.bin、u-boot-spl.bin、u-boot.img
举个例子,我用的是TI的AM335x平台:
# 配置
make am335x_evm_defconfig
# 如果需要修改配置
make menuconfig
# 编译,-j4表示4核并行
make -j4
# 编译完成后,在根目录下会生成:
# u-boot.bin —— U-Boot proper的二进制
# u-boot-spl.bin —— SPL的二进制
# u-boot.img —— 带头信息的U-Boot镜像
# MLO —— TI平台专用的SPL(带TI头信息)
注意:不同厂商对SPL的命名和格式要求不同。TI叫MLO,NXP叫SPL,Rockchip叫idbloader.img。烧录前一定要确认平台要求,否则板子变砖别怪我。
2.4 理解U-Boot的配置系统:Kconfig与设备树
U-Boot的配置系统跟Linux内核很像,都是基于Kconfig的。你运行make menuconfig时,看到的就是Kconfig生成的图形界面。每个选项对应一个CONFIG_XXX宏,这些宏最终会写入.config文件。
我个人习惯把配置分成三类:
- 板级配置:DDR大小、时钟频率、GPIO复用等,写在
include/configs/<board>.h里 - 功能配置:是否支持网络、文件系统、命令行等,通过Kconfig控制
- 设备树配置:硬件拓扑、外设地址、中断号等,写在
.dts文件里
设备树在U-Boot里越来越重要。从U-Boot 2018年开始,官方推荐用设备树来描述硬件。这样做的好处是:U-Boot和内核可以共用同一份设备树,减少维护成本。
避坑指南:我曾经在升级U-Boot版本时,发现网卡不工作了。查了半天,原来是新版本把网卡的设备树节点从u-boot.dtsi移到了u-boot.dts,而我用的旧设备树没同步更新。所以,设备树文件一定要跟U-Boot版本匹配。
2.5 烧录U-Boot到板子上
烧录方式取决于你的板子支持什么启动介质。常见的有:
| 启动介质 | 烧录工具 | 烧录位置 | 注意事项 |
|---|---|---|---|
| SD卡 | dd 或 balenaEtcher |
偏移量(如1MB偏移) | 注意块大小和对齐 |
| eMMC | fastboot 或 ums |
特定分区 | 需要先擦除再写入 |
| NAND Flash | nand write (U-Boot命令行) |
NAND块地址 | 注意坏块管理 |
| SPI Flash | sf write (U-Boot命令行) |
SPI Flash偏移 | 容量通常较小 |
以SD卡为例,我常用的烧录命令:
# 假设SD卡设备是/dev/sdb,注意确认设备名!
# 烧录SPL到偏移1MB处(很多SoC要求SPL放在这个位置)
sudo dd if=u-boot-spl.bin of=/dev/sdb bs=1k seek=1
# 烧录U-Boot proper到偏移2MB处
sudo dd if=u-boot.bin of=/dev/sdb bs=1k seek=2
# 同步并卸载
sync
sudo eject /dev/sdb
警告:用dd命令时,一定要确认of=指向的是SD卡,不是你的硬盘。我见过有人把/dev/sda(系统盘)当成/dev/sdb(SD卡)来烧录,结果系统直接挂了。建议烧录前先执行lsblk确认设备名。
2.6 SPL与U-Boot proper的启动流程
为了让你更直观地理解整个启动过程,我画了一张流程图:
从这张图可以看得很清楚:SPL是个“瘦子”,只做最核心的事——初始化DDR,然后加载U-Boot proper。U-Boot proper才是那个功能完整的“胖子”,它负责加载内核、文件系统,还提供命令行交互。
2.7 实战:从日志看SPL与U-Boot proper的切换
理论讲完了,咱们来点实战。启动时打开串口,你会看到类似这样的日志:
U-Boot SPL 2021.01 (Jan 15 2024 - 10:30:00 +0800)
Trying to boot from MMC1
U-Boot 2021.01 (Jan 15 2024 - 10:30:00 +0800)
CPU : AM335X-GP rev 2.1
I2C: ready
DRAM: 512 MiB
MMC: OMAP SD/MMC: 0
Net: eth0: ethernet@4a100000
Hit any key to stop autoboot: 0
=>
注意看:第一行是SPL打印的,它告诉你“我从MMC1启动了”。然后SPL加载U-Boot proper,U-Boot proper开始打印版本号、CPU信息、内存大小等。看到=>提示符,说明U-Boot proper已经跑起来了,你可以输入命令了。
我的调试技巧:如果SPL阶段就卡住了,连“Trying to boot from”都看不到,那问题大概率出在SPL本身——可能是DDR初始化失败,或者SPL烧录位置不对。如果能看到SPL日志但看不到U-Boot proper日志,那问题出在SPL加载U-Boot proper的过程中——可能是Flash驱动问题,或者U-Boot proper镜像损坏了。
2.8 本章小结
嗯,这一章的内容不少,但都是基础中的基础。我带你梳理一下重点:
- SPL vs U-Boot proper:SPL是“小不点”,在SRAM里跑,负责初始化DDR并加载U-Boot proper;U-Boot proper是“大家伙”,在DDR里跑,负责加载内核和提供命令行。
- 编译配置:用
make xxx_defconfig选择板级配置,用make menuconfig做自定义配置,最后make编译。 - 烧录:不同平台、不同介质,烧录方式不同。用
dd时一定要确认设备名,别把系统盘烧了。 - 日志分析:从串口日志可以判断启动到了哪个阶段,快速定位问题。