1. U-Boot概述

大家好,我是你们的嵌入式固件讲师。今天咱们来聊聊U-Boot——这个嵌入式世界里几乎绕不开的"看门大爷"。

先问大家一个问题:你按下开发板电源键之后,到Linux内核开始打印启动信息之前,中间发生了什么?

嗯,这中间就是U-Boot的舞台。

1.1 U-Boot是什么?

U-Boot,全称是"Universal Boot Loader",通用引导加载程序。说白了,它就是一块芯片上电后运行的第一个用户可控的程序。

我刚开始做嵌入式那会儿,总觉得Bootloader就是个"跳板",没啥技术含量。后来踩了坑才明白——没有U-Boot,你的Linux内核根本跑不起来。

U-Boot支持什么?我列几个关键点:

  • 支持多种架构:ARM、x86、RISC-V、MIPS……几乎你能想到的主流CPU架构它都支持
  • 支持多种存储介质:NAND Flash、NOR Flash、SD卡、eMMC、SPI Flash……
  • 支持多种文件系统:FAT、ext4、UBIFS、JFFS2……
  • 支持网络协议:TFTP、NFS、DHCP……方便你通过网络下载内核

我个人习惯把U-Boot比作"嵌入式系统的BIOS"。但比PC的BIOS灵活得多——你可以修改它、裁剪它、甚至完全重写它的功能。

核心要点:U-Boot是硬件初始化和操作系统加载之间的桥梁。没有它,你的嵌入式设备就是一坨冰冷的硅片。

1.2 U-Boot在嵌入式系统中的角色

U-Boot到底干了哪些活?我把它拆成三个阶段来讲:

第一阶段:硬件初始化

芯片上电后,内部ROM会执行一段固化代码,然后跳转到U-Boot。U-Boot的第一件事就是初始化硬件:

  • 设置CPU时钟频率
  • 初始化DDR内存控制器
  • 配置串口(这样你才能看到打印信息)
  • 初始化存储控制器(才能读Flash/SD卡)

我记得有一次项目,板子死活不打印任何信息。查了两天,最后发现是DDR时序参数配错了。嗯,U-Boot的硬件初始化就是这么关键——错一个参数,整个系统就哑巴了。

第二阶段:引导加载

硬件初始化完成后,U-Boot会从存储介质中读取Linux内核镜像(通常是zImage或Image),然后把它加载到内存中。这个过程可以简单到一条命令:

# 从eMMC读取内核到内存地址0x80000000
mmc read 0x80000000 0x800 0x8000

# 启动内核
bootm 0x80000000

当然,实际项目中不会这么手动操作。我们会把启动参数写到环境变量里,让U-Boot自动执行。

第三阶段:传递参数

U-Boot在跳转到内核之前,会做一件重要的事——传递设备树(Device Tree)和内核启动参数。比如:

setenv bootargs 'console=ttyS0,115200 root=/dev/mmcblk0p2 rw'
bootz 0x80000000 - 0x83000000

这里bootz后面的三个参数分别是:内核地址、initrd地址(没有就填-)、设备树地址。

我的经验:量产项目中,U-Boot的环境变量一定要固化好。我曾经见过一个项目,因为环境变量被意外擦除,导致整批板子无法启动。后来我们都在生产脚本里加了"env save"的校验步骤。

1.3 U-Boot源码结构

U-Boot的源码结构,说白了就是一个大型的C语言工程。我建议你下载一份最新代码(git clone https://source.denx.de/u-boot/u-boot.git),对照着看。

核心目录结构如下:

目录/文件 作用
arch/ 架构相关代码。比如arch/arm/放ARM相关,arch/x86/放x86相关
board/ 板级支持包。每个开发板或产品在这里有自己的文件夹
common/ 通用功能模块。比如命令行解析、环境变量管理
drivers/ 各种外设驱动。串口、网卡、Flash、I2C、SPI……
include/ 头文件。特别是configs/目录下,放着各板子的配置文件
net/ 网络协议栈。TFTP、NFS、DHCP等
fs/ 文件系统支持。FAT、ext4、UBIFS等
cmd/ 命令行命令的实现。比如mmc、fatload、bootm等
tools/ 主机端工具。比如mkimage(用于制作U-Boot格式的镜像)

你想想看,这个结构是不是很清晰?架构相关放arch/,板子相关放board/,驱动放drivers/。这种分层设计,让U-Boot能支持上千种不同的板子。

避坑指南:我曾经在移植U-Boot时,直接修改了arch/arm/下的通用代码,结果导致其他ARM板子都编译不过。正确的做法是:板级修改放board/目录,架构修改放arch/目录,别混着来。

下面我用一张SVG图来展示U-Boot的整体架构和启动流程:

U-Boot 架构与启动流程 芯片上电 / ROM代码 SPL (Secondary Program Loader) U-Boot 主程序 (u-boot.bin) 硬件初始化 → 驱动加载 → 命令行环境 硬件初始化 CPU时钟 / DDR / 串口 驱动与存储 MMC / NAND / SPI / 网卡 命令行与环境变量 bootcmd / bootargs / env 加载 Linux 内核 + 设备树 启动内核 (bootm/bootz)

这张图展示了U-Boot从芯片上电到启动内核的完整流程。注意SPL那一层——有些芯片内部RAM太小,装不下完整的U-Boot,就需要一个"迷你版"的SPL先初始化DDR,再把完整的U-Boot加载到内存中。

小技巧:查看你的板子是否使用SPL,可以看源码目录下有没有spl/文件夹。如果有,说明你的U-Boot是分两阶段加载的。

最后说说源码的编译方式。U-Boot的编译和Linux内核很像:

# 先配置(以树莓派3为例)
make rpi_3_defconfig

# 然后编译
make -j4

# 编译完成后会生成:
# u-boot.bin    —— 主程序
# u-boot.img    —— 带U-Boot头部的镜像
# u-boot.srec   —— S-Record格式(用于串口下载)

我个人习惯在编译前先执行make menuconfig,图形化界面里可以裁剪不需要的功能。量产项目里,我一般会把网络命令、USB支持、多余的文件系统都去掉,能省下不少空间。