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其实不复杂,但配置这一步容易踩坑。不同厂商、不同板子,配置方式千差万别。我一般按这个流程走:

  1. 选择目标板配置:make <board_name>_defconfig
  2. 自定义配置(可选):make menuconfig
  3. 编译:make -j$(nproc)
  4. 检查输出:查看生成的 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的启动流程

为了让你更直观地理解整个启动过程,我画了一张流程图:

U-Boot SPL与U-Boot proper启动流程 U-Boot SPL与U-Boot proper启动流程 上电复位 ROM Code(固化在芯片内) SPL(加载到SRAM) 初始化DDR & 时钟 加载U-Boot proper到DDR U-Boot proper(在DDR运行) 关键说明 • SPL在SRAM中运行 • U-Boot proper在DDR中运行 • SPL完成后跳转到DDR 常见问题 • SPL起不来:检查DDR配置 • U-Boot起不来:检查Flash驱动 • 内核起不来:检查设备树

从这张图可以看得很清楚: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时一定要确认设备名,别把系统盘烧了。
  • 日志分析:从串口日志可以判断启动到了哪个阶段,快速定位问题。