3. U-Boot源码结构解析:目录树、关键文件与配置系统
U-Boot 的源码,说实话,第一次打开的时候确实有点懵。我记得我刚入行那会儿,面对几十个文件夹、上千个文件,完全不知道从哪下手。后来做项目多了,慢慢摸清了它的套路。说白了,U-Boot 的源码结构虽然庞大,但逻辑非常清晰——它把「硬件相关」、「板级相关」、「通用逻辑」和「工具脚本」分得明明白白。
这一章,我就带你把这棵「源码树」从头到尾捋一遍。重点看三个东西:目录结构、三个关键文件、以及Kconfig/Makefile 配置系统。搞懂这些,你以后移植 U-Boot 就能少走很多弯路。
3.1 U-Boot 目录树分析
先看整体。U-Boot 的顶层目录大概有 20 多个,我挑最核心的给你讲。下面这个表格,是我自己总结的「必看目录清单」:
| 目录名 | 作用 | 我的建议 |
|---|---|---|
arch/ |
CPU 架构相关代码(ARM、RISC-V、x86 等) | 移植时最常改的地方 |
board/ |
具体板级初始化代码 | 每个板子一个文件夹 |
common/ |
通用命令和核心逻辑 | main.c 就在这里 |
drivers/ |
各种外设驱动(串口、网卡、MMC 等) | 裁剪时重点关注 |
include/ |
头文件和配置文件 | configs/ 子目录放板级配置 |
lib/ |
通用库函数(字符串、CRC、压缩等) | 一般不用动 |
net/ |
网络协议栈 | 支持 TFTP、NFS 等 |
fs/ |
文件系统支持(ext4、FAT、UBIFS 等) | 按需裁剪 |
tools/ |
主机端工具(mkimage、fdt 等) | 编译时会用到 |
scripts/ |
Kconfig 和构建脚本 | 配置系统的核心 |
嗯,这里要注意:arch/ 和 board/ 是移植工作的主战场。我做过一个项目,CPU 是 ARM Cortex-A7,板子是自研的。当时我只需要在 arch/arm/ 下找到对应的 CPU 目录,然后在 board/ 下新建一个板级文件夹,大部分工作就完成了。
board/ 下有没有类似的参考板。有的话直接复制一份改,比从头写快得多。
3.2 关键文件功能解读
U-Boot 启动过程中,有三个文件是绕不开的。我按启动顺序给你讲:start.S → board.c → main.c。
3.2.1 start.S:第一行代码
这是 U-Boot 的入口点。CPU 上电后,第一条指令就从这里开始。它主要干三件事:
- 设置 CPU 模式:关中断、设栈指针、切到 SVC 模式
- 初始化关键硬件:时钟、DDR 控制器、串口
- 重定位自身:把 U-Boot 从 Flash 拷贝到 DDR 中运行
我截一段典型的 ARM 启动代码给你看:
// arch/arm/cpu/armv7/start.S 片段
reset:
/* 设置 CPU 为 SVC 模式,关中断 */
mrs r0, cpsr
bic r0, r0, #0x1f
orr r0, r0, #0xd3
msr cpsr, r0
/* 初始化关键寄存器 */
bl cpu_init_cp15
bl cpu_init_crit
/* 跳转到 board_init_f */
bl board_init_f
为什么先关中断?因为这时候中断向量表还没设好,万一来了个中断,CPU 就不知道跳哪去了。我在项目中遇到过一个问题:板子偶尔启动卡死,查了两天才发现是 start.S 里没关看门狗,导致在 DDR 初始化过程中被复位了。
3.2.2 board.c:板级初始化
从 start.S 跳出来后,控制权交给 board_init_f() 和 board_init_r()。这两个函数都在 common/board.c 里。
board_init_f 负责「前期初始化」——这时候 U-Boot 还在 Flash 里运行,只能做轻量级工作:
- 初始化串口(为了能看到打印信息)
- 初始化定时器
- 初始化 DDR 控制器
- 规划内存布局
board_init_r 负责「后期初始化」——这时候 U-Boot 已经重定位到 DDR 了,可以大展拳脚:
- 初始化所有驱动(网卡、MMC、USB 等)
- 设置环境变量
- 使能中断
- 进入主循环
你想想看,为什么要分两个阶段?因为早期代码在 Flash 里跑,速度慢、空间小,只能做最基本的事。等 DDR 起来了,再把完整代码搬过去,后面就快了。
3.2.3 common/main.c:命令行主循环
这是 U-Boot 的「用户界面」。初始化完成后,就进入 main_loop():
// common/main.c 核心逻辑
void main_loop(void)
{
/* 检查是否有自动启动命令 */
if (autoboot_command())
return;
/* 进入命令行交互模式 */
cli_loop();
}
这里有两个分支:
- 自动启动:如果设置了
bootcmd环境变量,并且倒计时内没被用户打断,就自动执行启动命令(比如加载内核) - 命令行模式:否则进入交互式 Shell,等待用户输入命令
我个人习惯把 bootcmd 设成 tftp 0x82000000 zImage; bootz 0x82000000,这样开发时插上网线就能自动下载内核启动,省得每次手动敲命令。
3.3 配置系统:Kconfig / Makefile
U-Boot 的配置系统是从 Linux 内核学来的。说白了,就是用 make menuconfig 选功能,然后自动生成配置头文件。这套系统由两部分组成:
3.3.1 Kconfig:定义配置选项
Kconfig 文件定义了「有哪些配置项可以选」。比如你想支持网络功能,就会看到这样的定义:
// drivers/net/Kconfig 片段
config CMD_NET
bool "Enable networking commands"
default y
help
支持 ping、tftp 等网络命令
每个配置项都有类型(bool、int、string 等)、默认值和帮助信息。选上后,会在 include/config.h 里生成 #define CONFIG_CMD_NET 1。
3.3.2 Makefile:条件编译
Makefile 根据 Kconfig 生成的配置,决定编译哪些文件。比如:
// drivers/net/Makefile 片段
obj-$(CONFIG_CMD_NET) += net.o
obj-$(CONFIG_CMD_PING) += ping.o
如果 CONFIG_CMD_NET 为 y,就编译 net.o;否则跳过。这就是裁剪的基础——不选的功能,代码根本不编译。
3.4 本章知识结构图
下面这张图,把 U-Boot 源码的核心脉络画出来了。从目录结构到关键文件,再到配置系统,一条线串起来:
3.5 小结
这一章我们干了三件事:
- 把 U-Boot 目录树拆开看了,知道每个目录是干什么的
- 分析了三个关键文件——
start.S是入口、board.c是板级初始化、main.c是用户交互 - 搞懂了 Kconfig/Makefile 配置系统——说白了就是「选什么就编译什么」