4. U-Boot源码结构:目录、关键文件与编译系统
U-Boot 的源码,说实话,我第一次打开的时候也懵了一下。目录多,文件杂,感觉像个大杂烩。但干 BSP 这么多年,我慢慢摸清了它的套路。说白了,它的结构很有章法,你只要抓住几个关键点,就能快速定位到你想要的东西。
这一节,我就带你捋一捋 U-Boot 的目录结构、三个核心文件,以及它的编译系统是怎么工作的。这些东西搞明白了,后面做分区整合你心里就有底了。
4.1 目录结构:U-Boot 的“骨架”
U-Boot 的顶层目录,我习惯把它分成几大类:平台相关、通用功能、驱动、工具脚本。你想想看,一个 Bootloader 要支持那么多板子,代码肯定得分层。
下面这个表,是我觉得最核心的几个目录,你重点记住它们:
| 目录名 | 作用 | 我的备注 |
|---|---|---|
arch/ |
存放 CPU 架构相关代码,比如 ARM、RISC-V、x86 | 我们做 ARM 的,主要看 arch/arm/ |
board/ |
存放具体板级代码,每个板子一个子目录 | 你新加一个板子,主要就是在这里动手 |
common/ |
U-Boot 的通用核心逻辑,比如命令行、环境变量 | main.c 就在这里,后面会细讲 |
drivers/ |
各种外设驱动,串口、网卡、Flash、MMC 等等 | 驱动模型化之后,这里结构清晰了很多 |
include/ |
头文件,特别是 configs/ 下的板级配置 |
改配置经常要动这里 |
lib/ |
通用库函数,比如字符串操作、CRC 校验 | 嗯,这里的东西比较底层 |
net/ |
网络协议栈,TFTP、NFS 都靠它 | 调试阶段,网络功能是救命稻草 |
cmd/ |
命令行命令的实现,比如 md、mw、bootm |
你想加个自定义命令,就在这里加 |
scripts/ |
编译脚本、Kconfig 语法文件 | Kconfig/Makefile 的辅助文件 |
board/ 和 include/configs/ 这两个目录。一个管硬件初始化,一个管功能配置。这两个搞清楚了,板子就跑起来了。
4.2 关键文件:U-Boot 的“灵魂”
目录是骨架,那文件就是灵魂。U-Boot 启动过程中,有三个文件你无论如何都绕不开。我按启动顺序给你讲讲。
4.2.1 start.S:一切从这里开始
这个文件在 arch/arm/cpu/armv7/ 或者类似的路径下。它是 U-Boot 的第一行代码,用汇编写的。
它干了什么事?说白了就三件:
- 设置 CPU 模式:从启动的 SVC 模式切到我们需要的模式,关中断。
- 初始化关键硬件:比如关 MMU 和 Cache、设置栈指针 SP。这时候 C 环境还没准备好,只能用汇编。
- 跳转到 C 代码:最后调用
_main,进入 C 语言的世界。
我记得有一次,板子上电就挂,死活跑不起来。最后用 JTAG 单步跟踪,发现是 start.S 里栈指针设错了,导致后面调用 C 函数时栈溢出。嗯,从那以后,我每次移植都先检查 SP 的值。
// 一个简化的 start.S 片段,感受一下
reset:
/* 设置 CPU 为 SVC32 模式 */
mrs r0, cpsr
bic r0, r0, #0x1f
orr r0, r0, #0xd3
msr cpsr, r0
/* 关中断,设置栈指针 */
ldr sp, =CONFIG_SYS_INIT_SP_ADDR
bic sp, sp, #7 /* 8 字节对齐 */
/* 跳转到 C 语言的 board_init_f */
bl board_init_f
4.2.2 board.c:板级初始化的“总指挥”
这个文件在 board/ 目录下,每个板子都有自己的 board.c。它是板级初始化的核心。
它主要干了这些事:
- board_init_f():前期初始化,比如初始化串口、定时器、DRAM 控制器。这时候 DRAM 还没好,代码在 SRAM 里跑。
- board_init_r():后期初始化,DRAM 已经可用了,把 U-Boot 自身从 Flash 搬到 DRAM 里,然后初始化各种外设。
- misc_init_r():一些杂项初始化,比如设置板子 ID、读取 EEPROM。
board_init_f 和 board_init_r 的执行环境完全不同。前者在 SRAM 里,栈空间有限;后者在 DRAM 里,空间充裕。你在写代码时,千万别把大数组定义在 board_init_f 里,否则栈会爆。我曾经就犯过这个错,查了整整一天。
4.2.3 common/main.c:命令行交互的“心脏”
这个文件在 common/ 目录下。它实现了 U-Boot 的命令行主循环。
流程很简单:
- 初始化完所有硬件后,进入
main_loop()。 - 检查有没有预设的启动命令(比如
bootcmd环境变量)。如果有,直接执行。 - 如果没有,或者用户按了任意键,就进入命令行交互模式,等待用户输入命令。
你想想看,我们平时敲的 printenv、setenv、bootm 这些命令,最终都是通过 main.c 里的命令解析器来调度的。
// main_loop 的简化逻辑
void main_loop(void)
{
char *s;
// 检查 bootdelay 和 bootcmd
s = env_get("bootcmd");
if (s != NULL) {
// 执行预设的启动命令
run_command_list(s, -1, 0);
}
// 进入命令行循环
for (;;) {
// 读取用户输入
len = cli_readline(CONFIG_SYS_PROMPT);
// 解析并执行命令
if (len > 0)
run_command(lastcommand, 0);
}
}
4.3 编译系统:Kconfig / Makefile
U-Boot 的编译系统,早期用的是传统的 make xxx_config 方式。后来为了统一管理,引入了 Kconfig 机制。现在你看到的 U-Boot,基本都是 Kconfig + Makefile 的组合。
4.3.1 Kconfig:配置的“菜单”
Kconfig 文件定义了各种配置选项,比如:
- 选择 CPU 架构(ARM、RISC-V)
- 选择板子型号(
TARGET_MY_BOARD) - 开启某个功能(
CONFIG_CMD_NET)
你执行 make menuconfig 时,看到的那个图形界面,就是 Kconfig 生成的。它把所有的配置选项组织成树形结构,方便你开关。
// 一个 Kconfig 片段示例
menu "Board selection"
config TARGET_MY_BOARD
bool "Support my_board"
select ARM
select DM
help
This is my custom board.
endmenu
config CMD_NET
bool "networking commands"
default y
help
Enable networking commands like tftp, ping.
4.3.2 Makefile:编译的“流水线”
Makefile 负责把配置变成实际的二进制文件。U-Boot 的 Makefile 体系很庞大,但核心逻辑就几条:
- 顶层 Makefile:定义编译目标、链接脚本、生成
u-boot.bin。 - 子目录 Makefile:每个目录下的
Makefile或Kbuild,告诉编译系统这个目录下有哪些文件要编译。 - 配置生成:
make xxx_defconfig会根据configs/下的默认配置,生成.config文件。
我个人建议,你不需要把整个 Makefile 看完。你只需要知道:
- 怎么加一个新文件:在对应目录的
Makefile里加一行obj-y += my_file.o。 - 怎么加一个新驱动:在
drivers/下建目录,写 Kconfig 和 Makefile。
make xxx_defconfig→ 生成.config(基于 Kconfig)make→ 读取.config,编译所有选中的源文件(基于 Makefile)- 输出
u-boot.bin、u-boot.srec等文件
为了让你更直观地理解 U-Boot 的源码结构和编译流程,我画了一张图:
这张图把 U-Boot 的源码结构、关键文件和编译流程串在了一起。你从左边看起,理解目录是干什么的;然后看中间,知道启动时哪几个文件在干活;最后看右边,明白怎么把代码变成二进制。
嗯,这一节的内容就到这里。U-Boot 的源码结构,说白了就是“平台相关 + 通用功能 + 编译系统”这三板斧。你只要把 start.S、board.c、main.c 这三个文件搞明白,再学会用 Kconfig 和 Makefile,后面做分区整合就顺手多了。