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 主要干这几件事:

  1. 设置CPU为SVC模式 —— 关中断、设栈指针
  2. 初始化关键硬件 —— 时钟、DDR控制器(极简版)
  3. 重定位U-Boot自身 —— 从Flash拷贝到DDR
  4. 跳转到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,               // 结束标记
};

我个人的经验是:如果板子启动到一半卡住了,就在这个数组里加调试打印。一个一个函数排除,很快能找到问题。

注意:board_init_f 里不能使用全局变量!因为这时候全局变量还在Flash里,写操作会无效。我见过有人在这里用全局变量做计数,结果怎么都加不上,查了半天。

4.4 关键文件之三:common/main.c —— 交互的“心脏”

初始化完成后,U-Boot进入主循环。入口是 main_loop(),在 common/main.c 里。

main_loop 的逻辑其实很简单:

  1. 检查是否有自动启动命令(比如bootcmd环境变量)
  2. 如果有,执行自动启动(通常是加载内核)
  3. 如果没有或用户打断,进入命令行交互模式

命令行模式的核心是 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从上电到命令行提示符的完整流程:

U-Boot 启动流程总览 阶段1:start.S(汇编) 设置SVC模式/关中断 初始化时钟/DDR 设置栈指针 阶段2:board_init_f(C语言) 串口/时钟/GPIO初始化 DRAM初始化 计算重定位地址 阶段3:重定位(Flash→DDR) 阶段4:board_init_r(DDR中) 初始化驱动框架 环境变量加载 使能中断 阶段5:main_loop(命令行)

4.6 我的调试建议

最后分享几个实用技巧:

  • 加打印是王道 —— 在start.S里用 puts() 或串口直接输出,在board_init_f数组里加 printf()。别怕代码乱,调通了再删。
  • 看链接脚本 —— u-boot.lds 决定了各段在内存中的布局。我遇到过因为链接顺序不对导致函数调用崩溃的坑。
  • 环境变量别乱设 —— bootcmd 写错了,板子可能永远启不了内核。我习惯先用 printenv 看看默认值。

一句话总结:start.S 是入口,board.c 是骨架,main.c 是灵魂。搞懂这三个文件,U-Boot源码对你来说就是透明的。