4. U-Boot 源码结构解析:目录树、关键文件功能分析
拿到一份U-Boot源码,很多人第一反应是懵的。几百个文件夹,几千个文件,从哪看起?
我刚开始接触U-Boot时也这样。后来带团队做移植,慢慢摸清了门道。说白了,U-Boot的源码结构是有规律的。你掌握了这个规律,看源码就像看地图一样清晰。
4.1 顶层目录树:一张导航图
我们先从顶层看起。U-Boot的根目录大概有这些核心目录:
| 目录/文件 | 作用 | 我的经验 |
|---|---|---|
| arch/ | 架构相关代码(ARM、RISC-V、x86等) | 移植时90%的修改都在这里 |
| board/ | 板级初始化代码 | 每个板子一个文件夹,我习惯从这里开始看 |
| common/ | 通用功能模块(main_loop、命令解析等) | 启动流程的“大脑”在这里 |
| drivers/ | 外设驱动(串口、网卡、MMC等) | 调试时最常翻的目录 |
| include/ | 头文件、配置宏定义 | configs/子目录放板级配置,别搞混 |
| dts/ | 设备树源文件(.dts/.dtsi) | 新板子必须自己写 |
| net/ | 网络协议栈 | TFTP/NFS下载全靠它 |
| fs/ | 文件系统支持(ext4、fat等) | 启动内核时用到 |
| tools/ | 主机端工具(mkimage等) | 打包镜像必备 |
board/ 和 arch/ 下的文件。这两个目录能告诉你这个板子是怎么初始化的。
嗯,这里要特别提一下 arch/ 目录。它下面按CPU架构分,比如 arch/arm/、arch/riscv/。ARM下面又分 cpu/(启动汇编)、mach-xxx/(SoC级初始化)、lib/(架构通用函数)。
4.2 关键文件之一:start.S —— 一切从这里开始
U-Boot启动的第一条指令在哪?在 arch/arm/cpu/armv7/start.S(ARMv7架构为例)。
这个文件是纯汇编写的。为什么用汇编?因为C语言运行时环境还没建立。你想想看,栈指针都没设好,怎么调用函数?
start.S 主要干这几件事:
- 设置CPU为SVC模式 —— 关中断、设栈指针
- 初始化关键硬件 —— 时钟、DDR控制器(极简版)
- 重定位U-Boot自身 —— 从Flash拷贝到DDR
- 跳转到C入口 —— 调用
board_init_f()
我曾经在一个项目中,start.S里的DDR初始化时序配错了,导致U-Boot死活跑不起来。查了两天才发现是某个寄存器值写反了。嗯,从那以后我每次改start.S都会反复对三遍手册。
核心要点:start.S 是U-Boot的“第一口气”。它挂了,后面全白搭。调试时如果串口没输出,先查start.S的执行路径。
4.3 关键文件之二:board.c —— 板级初始化的“总指挥”
从start.S跳出来后,控制权交给了 board_init_f() 和 board_init_r()。这两个函数都在 common/board_f.c 和 common/board_r.c 里。
为什么分两个阶段?
- board_init_f:在Flash里执行,做早期初始化。这时候U-Boot还在Flash里跑,不能随便用全局变量。
- board_init_r:重定位到DDR后执行。这时候U-Boot在DDR里,可以正常使用全局变量、调用驱动框架。
这两个函数内部维护了一个初始化函数指针数组。说白了,就是按顺序调用一堆初始化函数:
// board_init_f 的典型调用序列(简化版)
init_fnc_t *init_sequence_f[] = {
setup_machine, // 设置机器类型
init_baud_rate, // 初始化串口波特率
serial_init, // 串口初始化
dram_init, // DDR初始化
setup_dest_addr, // 计算重定位地址
reserve_xxx, // 预留内存空间
NULL, // 结束标记
};
我个人的经验是:如果板子启动到一半卡住了,就在这个数组里加调试打印。一个一个函数排除,很快能找到问题。
4.4 关键文件之三:common/main.c —— 交互的“心脏”
初始化完成后,U-Boot进入主循环。入口是 main_loop(),在 common/main.c 里。
main_loop 的逻辑其实很简单:
- 检查是否有自动启动命令(比如bootcmd环境变量)
- 如果有,执行自动启动(通常是加载内核)
- 如果没有或用户打断,进入命令行交互模式
命令行模式的核心是 readline() 和 run_command()。readline读取用户输入,run_command解析并执行命令。
这里有个细节:U-Boot的命令是通过 U_BOOT_CMD 宏注册的。每个命令对应一个结构体,包含命令名、帮助信息、执行函数指针。
// 一个典型的命令注册示例
U_BOOT_CMD(
mycmd, // 命令名
3, // 最大参数个数
1, // 是否可重复执行
do_mycmd, // 执行函数
"my command - do something",
"详细帮助信息..."
);
我在调试网络驱动时,经常自己加调试命令。比如 mdio_read、phy_dump,方便查看PHY寄存器状态。这个技巧很实用。
4.5 整体启动流程:一张图看懂
下面这张图总结了U-Boot从上电到命令行提示符的完整流程:
4.6 我的调试建议
最后分享几个实用技巧:
- 加打印是王道 —— 在start.S里用
puts()或串口直接输出,在board_init_f数组里加printf()。别怕代码乱,调通了再删。 - 看链接脚本 ——
u-boot.lds决定了各段在内存中的布局。我遇到过因为链接顺序不对导致函数调用崩溃的坑。 - 环境变量别乱设 ——
bootcmd写错了,板子可能永远启不了内核。我习惯先用printenv看看默认值。
一句话总结:start.S 是入口,board.c 是骨架,main.c 是灵魂。搞懂这三个文件,U-Boot源码对你来说就是透明的。