U-Boot 源码结构:从顶层到底层的设计哲学
拿到一份 U-Boot 源码,第一眼看到几十个目录,很多人会懵。我刚开始接触时也一样,对着 ls 输出发了好一会儿呆。但别怕,这套结构其实很有章法。说白了,它就是一套「硬件抽象 + 平台分层」的经典实践。
今天我们就把它拆开来看。我会带着你,从顶层目录一路走到最底层的驱动代码。嗯,这里要注意:不是每个目录都要背下来,而是理解它为什么这么放。
核心思想:U-Boot 的目录结构遵循「三权分立」——架构无关的通用代码、架构相关的核心代码、板级特定的配置代码,各司其职。
顶层目录结构:一眼看穿全局
先看顶层。你执行 ls 后,会看到大约 20 多个目录和文件。我挑几个最关键的来说:
| 目录/文件 | 作用 | 我的评价 |
|---|---|---|
arch/ |
CPU 架构相关代码(ARM、RISC-V、x86 等) | 移植新 CPU 时第一个要看的目录 |
board/ |
具体板卡的初始化代码 | 每个项目几乎都要改这里 |
common/ |
通用核心逻辑(命令解析、环境变量等) | 最稳定的部分,基本不动 |
drivers/ |
各种外设驱动(串口、网卡、存储等) | 调试硬件问题时泡在这里 |
include/ |
全局头文件和配置文件 | configs/ 子目录放板级配置 |
lib/ |
通用库函数(字符串、加密、压缩等) | 很少需要改 |
net/ |
网络协议栈 | TFTP 下载、NFS 挂载都靠它 |
fs/ |
文件系统支持(ext4、FAT、UBIFS 等) | 加载内核镜像时必经之路 |
cmd/ |
命令行命令的实现 | 每个命令一个文件,很清晰 |
我个人习惯,拿到一个新板子,先看 board/ 和 arch/,再翻 include/configs/ 下的头文件。这三个地方基本能告诉你这块板子是怎么启动的。
board 目录:板级定制的战场
board/ 目录是按厂商和板卡名组织的。比如:
board/
├── ti/
│ ├── am335x/
│ │ ├── board.c
│ │ ├── mux.c
│ │ └── Kconfig
│ └── omap5/
├── rockchip/
│ ├── evb_rk3399/
│ └── pinebook_pro/
└── my_company/
└── my_board/
每个板卡目录里,通常有:
- board.c:板级初始化函数,比如
board_init()、dram_init() - mux.c:引脚复用配置(ARM 平台常见)
- Kconfig:板级的配置选项
- MAINTAINERS:谁负责维护这块板子
我的经验:曾经接手一个项目,板子 DDR 参数配错了,死活起不来。最后发现是 board/ 下的 dram_init() 里时序参数写反了。从那以后,我每次移植新板子,第一件事就是对着原理图核对 board 目录里的初始化代码。
arch 目录:架构的灵魂
arch/ 目录按 CPU 架构分。以 ARM 为例:
arch/arm/
├── cpu/
│ ├── armv7/
│ ├── armv8/
│ └── arm11/
├── include/
│ └── asm/
├── lib/
│ ├── cache.c
│ ├── interrupts.c
│ └── stack.c
├── mach-*/ # 具体 SoC 厂商的代码
└── Kconfig
这里藏着 U-Boot 启动的最早期代码。比如 arch/arm/cpu/armv8/start.S,那是 CPU 复位后的第一条指令。我调试过几次启动失败的问题,最后都追到了这个汇编文件里。
为什么会这样?因为 arch/ 负责的是「架构级」的事情:
- 设置异常向量表
- 初始化 MMU 和 Cache
- 设置栈指针
- 跳转到 C 语言入口
这些代码一旦写错,板子就是「死给你看」。我曾经在移植 RISC-V 平台时,start.S 里一个跳转地址算错了,折腾了两天才找到问题。
common 目录:U-Boot 的「操作系统」
common/ 目录是 U-Boot 的核心逻辑所在。它不依赖具体硬件,提供的是「框架」:
common/
├── main.c # 主循环
├── command.c # 命令注册与分发
├── console.c # 控制台 I/O
├── env_common.c # 环境变量
├── image.c # 镜像解析
├── bootm.c # 启动内核的核心函数
└── cli.c # 命令行交互
我个人觉得,common/ 是 U-Boot 里最值得精读的部分。尤其是 bootm.c,它实现了镜像加载的完整流程——从解析镜像头、校验、解压,到跳转到内核入口。我们后面几章会反复提到它。
注意:不要随意修改 common/ 下的代码。这些文件被所有板卡共享,改一个地方可能影响几十个平台。我见过有人为了加一个调试打印,改了 main.c,结果其他板子启动时多输出了不该有的信息。
drivers 目录:硬件世界的桥梁
drivers/ 是 U-Boot 里最大的目录之一。它按设备类型组织:
drivers/
├── serial/ # 串口驱动
├── net/ # 网卡驱动
├── mmc/ # SD/eMMC 驱动
├── spi/ # SPI 控制器驱动
├── i2c/ # I2C 控制器驱动
├── gpio/ # GPIO 驱动
├── usb/ # USB 主机/设备驱动
├── mtd/ # NAND Flash、SPI Flash 驱动
└── video/ # 显示驱动
每个子目录里,通常有一个 Kconfig 和多个驱动文件。U-Boot 的驱动模型(Driver Model)从 2014 年开始引入,现在已经是主流。它把驱动分成了三步:
- 绑定(Bind):设备树节点和驱动匹配
- 探测(Probe):初始化硬件
- 操作(Operate):提供读写接口
我在调试网卡驱动时,就靠这套模型一步步定位问题。先看设备树里节点对不对,再看驱动有没有被 probe 成功,最后才去查寄存器配置。
一张图看懂整体结构
下面这张 SVG 图,把 U-Boot 源码的核心目录和它们之间的关系画了出来。你可以把它当作「导航地图」:
其他重要目录
除了上面四个核心目录,还有几个也值得了解:
- include/configs/:板级配置头文件。比如
am335x_evm.h里定义了 DDR 大小、环境变量位置、默认启动命令等。我每次移植新板子,都要在这里改几十行宏定义。 - lib/:通用库。比如
lib/zlib/用于解压 gzip 镜像,lib/crypto/用于校验镜像签名。 - net/:网络协议栈。U-Boot 的 TFTP 客户端就在这里实现。调试网络启动时,我经常要加打印看
net/里的数据包处理流程。 - fs/:文件系统支持。从 FAT32 读内核镜像,或者从 ext4 根文件系统加载设备树,都靠这里的代码。
- cmd/:每个命令一个文件。比如
cmd/bootm.c实现了bootm命令,cmd/load.c实现了loadb、loadx等下载命令。
一个小技巧:想快速了解一个命令的实现,就去 cmd/ 下找对应的 .c 文件。比如 cmd/load.c 里,你能看到 U-Boot 是怎么通过串口接收二进制数据的——包括校验、超时、重传等细节。
总结一下
U-Boot 的源码结构,说白了就是一套「分层 + 模块化」的设计。顶层目录划分了职责边界,arch/ 管 CPU,board/ 管板子,common/ 管逻辑,drivers/ 管外设。它们之间通过 Kconfig 和 Makefile 串联起来。
你想想看,这种设计的好处是什么?移植一个新板子时,你只需要改 board/ 和 arch/ 下的少量代码,其他部分基本不用动。这就是 U-Boot 能支持上千种板卡的原因。