第3章:U-Boot源码结构分析
好,咱们今天来聊聊U-Boot的源码结构。说实话,我刚接触U-Boot那会儿,打开源码目录一看,好家伙,几百个文件夹,头都大了。但后来我摸清了一个规律——U-Boot的目录结构其实非常清晰,它遵循了Linux内核的布局思想。你只要抓住几个关键目录和文件,整个框架就豁然开朗了。
3.1 U-Boot目录结构概览
我们先从顶层目录看起。我个人习惯把U-Boot的目录分成三大块:平台无关代码、平台相关代码、以及构建系统。下面这张表是我整理的核心目录说明:
| 目录名 | 作用 | 我的备注 |
|---|---|---|
| arch/ | CPU架构相关代码(arm、riscv、x86等) | 移植时最常改的地方 |
| board/ | 具体板级初始化代码 | 每个板子一个子目录 |
| common/ | 通用功能模块(命令、控制台等) | 这里面的代码大部分不用动 |
| drivers/ | 设备驱动(串口、网卡、存储等) | 嗯,驱动开发的主战场 |
| include/ | 头文件和配置文件 | configs/下放板级配置 |
| lib/ | 通用库函数(字符串、CRC等) | 跟应用层编程差不多 |
| net/ | 网络协议栈(TFTP、NFS等) | 调试时经常用到 |
| cmd/ | 命令行命令实现 | 想加自定义命令?来这里 |
| tools/ | 主机端工具(mkimage等) | 打包镜像用的 |
你想想看,这么多目录,其实真正需要你深入理解的,也就那么几个。我建议你重点看arch/、board/和common/这三个。为什么?因为U-Boot的启动流程就藏在这三个目录里。
3.2 关键文件深度解析
3.2.1 start.S — 一切从这里开始
这是U-Boot的入口文件,通常放在arch/arm/cpu/armv7/start.S(ARM为例)。说白了,CPU上电后第一条指令就在这里。我刚开始看汇编代码时也觉得头大,但后来发现它其实就干三件事:
- 设置CPU为SVC模式,关中断
- 初始化关键硬件:关闭MMU和Cache、设置栈指针
- 跳转到C语言入口:调用
board_init_f()
这里有个细节我特别想强调——start.S中有一段代码叫“重定位”(relocation)。U-Boot会把自己从Flash拷贝到RAM中运行,这段逻辑就在start.S里。我曾经在一个项目里遇到启动失败,查了两天才发现是重定位地址配置错了。嗯,这种坑踩过一次就记住了。
/* 典型的start.S片段 */
reset:
/* 设置SVC模式,关中断 */
mrs r0, cpsr
bic r0, r0, #0x1f
orr r0, r0, #0xd3
msr cpsr, r0
/* 关闭MMU和Cache */
mrc p15, 0, r0, c1, c0, 0
bic r0, r0, #0x0005
mcr p15, 0, r0, c1, c0, 0
/* 设置栈指针 */
ldr sp, =CONFIG_SYS_INIT_SP_ADDR
/* 跳转到C代码 */
bl board_init_f
3.2.2 board.c — 板级初始化的心脏
这个文件在board/你的板子/board.c里。它实现了board_init_f()和board_init_r()两个关键函数。我习惯把前者叫“前阶段初始化”,后者叫“后阶段初始化”。
board_init_f()干了什么?说白了就是初始化那些在重定位前就必须可用的硬件:
- 串口(不然你连打印都看不到)
- 定时器(用于延时)
- GPIO(控制LED、复位等)
- DRAM控制器(内存必须能用)
board_init_r()则是重定位完成后执行的:
- 初始化所有驱动(网卡、存储等)
- 设置环境变量
- 进入主循环(等待用户输入命令)
我记得有一次调试一块新板子,串口死活没输出。查了半天,发现是board_init_f()里串口时钟配置错了。你想想看,串口都没输出,你连调试信息都看不到,那感觉就像在黑暗中摸索。后来我学乖了,先在board_init_f()里加个GPIO翻转,用示波器看波形,确认代码确实跑到了那里。
3.2.3 common.c — 通用功能的集散地
这个文件在common/common.c里,它实现了U-Boot的主循环。说白了,就是那个让你输入命令的shell。核心函数是main_loop():
void main_loop(void)
{
/* 处理自动启动 */
if (autoboot_command())
return;
/* 进入命令行循环 */
for (;;) {
len = readline(CONFIG_SYS_PROMPT);
if (len > 0)
run_command(len);
}
}
这里有个关键点——自动启动(autoboot)。U-Boot启动时会检查环境变量bootdelay,如果在倒计时内用户没有按键,就自动执行bootcmd里的命令(通常是启动内核)。这个逻辑就在common/autoboot.c里。我建议你把这个流程理清楚,因为产品发布时,自动启动的稳定性直接影响用户体验。
3.3 编译系统:Kconfig/Makefile
U-Boot的编译系统是从Linux内核学来的,使用Kconfig + Makefile的组合。说实话,刚开始接触Kconfig时我也觉得有点绕,但用顺手了就会发现它真的很强大。
3.3.1 Kconfig — 配置的图形化界面
Kconfig文件定义了所有可配置的选项。比如你想开启某个驱动,就在make menuconfig里勾选。这些选项最终会生成include/config/auto.conf,被Makefile读取。
我举个例子,假设你要添加一个新的板子:
# board/myboard/Kconfig
config TARGET_MYBOARD
bool "Support myboard"
select CPU_V7
help
This is my custom board based on ARM Cortex-A7.
然后在arch/arm/Kconfig里加上:
source "board/myboard/Kconfig"
这样,你在make menuconfig里就能看到“Support myboard”这个选项了。我个人习惯是先跑一遍make defconfig生成默认配置,再用make menuconfig微调,这样不容易漏掉关键选项。
3.3.2 Makefile — 构建的骨架
U-Boot的Makefile分为三层:
- 顶层Makefile:定义编译规则、链接脚本、生成u-boot.bin
- 架构Makefile(arch/arm/Makefile):定义CPU相关的编译选项
- 板级Makefile(board/myboard/Makefile):定义板级特有的源文件
这里有个小技巧——如果你想添加一个自定义文件到U-Boot中,只需要在对应目录的Makefile里加一行:
# board/myboard/Makefile
obj-y += my_custom_init.o
obj-y表示这个文件总是被编译。如果你希望条件编译,可以用obj-$(CONFIG_MY_FEATURE)。我曾经犯过一个错误,把文件加到了obj-后面但忘了加y,结果编译出来啥都没有。嗯,这种低级错误犯一次就够了。
make V=1查看详细的编译命令。这样你能看到编译器到底用了哪些参数,哪些文件被编译了。对于排查编译问题特别有用。
3.4 实战:如何快速定位代码
说了这么多,你可能觉得信息量有点大。别急,我给你一个实战方法——如何快速找到你想修改的代码:
- 先确定你要改什么:是CPU初始化?板级外设?还是命令?
- CPU相关:去
arch/目录下找 - 板级相关:去
board/目录下找 - 驱动相关:去
drivers/目录下找 - 命令相关:去
cmd/目录下找
举个例子,如果你想修改串口驱动:
- 先看
drivers/serial/目录 - 找到对应芯片的驱动文件(比如
ns16550.c) - 看它调用了哪些板级接口(通常在
board/目录下实现)
这个方法我用了十年,从来没失手过。你试试看,会发现U-Boot的代码结构其实很友好。
核心要点回顾:
- U-Boot目录分三大块:平台无关、平台相关、构建系统
- start.S是入口,负责最底层的CPU初始化
- board.c实现板级初始化,分前阶段和后阶段
- common.c提供主循环和命令处理
- Kconfig/Makefile构成灵活的编译系统
下一章,我们会深入U-Boot的启动流程,从第一条指令到命令行提示符出现,每一步都给你拆解清楚。到时候你会发现,理解了源码结构,启动流程就是水到渠成的事。