2. U-Boot源码结构分析:目录结构、关键文件与启动流程

好,咱们直接进入正题。U-Boot 的源码,说实话,第一次打开的时候确实有点懵。目录多,文件杂,不知道从哪看起。我当年刚接触的时候,也是对着屏幕发了好一会儿呆。

但别怕,这东西有规律。你只要抓住了 arch、board、config 这三个核心目录,再顺着启动流程走一遍,基本就通了。今天我就带你把这层窗户纸捅破。

2.1 顶层目录结构:别被吓到

U-Boot 的顶层目录,大概有二十多个。但真正跟你移植工作强相关的,其实就那么几个。我把它们分成了三类,你一看就明白:

分类 目录名 一句话说明
平台相关 arch/ 存放 CPU/架构相关的代码。ARM、x86、RISC-V 都在这里。
板级相关 board/ 每个板子一个文件夹。你的硬件差异,全在这里改。
配置相关 include/configs/ 板级的头文件配置。比如 DDR 参数、外设使能。
通用驱动 drivers/ 串口、网卡、MMC、USB 等驱动。一般不用动。
通用工具 tools/ mkimage 等打包工具。编译时会用到。
通用库 lib/、common/ 字符串处理、命令行解析等公共函数。
我的习惯: 拿到一个新板子,我第一件事就是去 board/ 和 include/configs/ 目录下,找一个跟它最像的参考板。复制粘贴,然后改。这比从零开始写快得多。

2.2 三个核心目录:arch、board、config

这三个目录,是你移植工作的主战场。咱们一个一个说。

2.2.1 arch/:CPU 的骨架

arch/ 目录下,按 CPU 架构分了子目录。以最常用的 ARM 为例:

arch/arm/
├── cpu/           # CPU 核心代码,比如 cache、MMU 的开关
│   ├── armv7/     # Cortex-A7/A8/A9 等
│   └── armv8/     # Cortex-A53/A72 等
├── dts/           # 设备树文件 .dts 和 .dtsi
├── include/       # 架构相关的头文件
├── lib/           # 架构相关的库函数
└── mach-*/        # 具体 SoC 厂商的代码,比如 mach-sunxi/、mach-imx/

这里要注意的是 mach-*/ 目录。每个 SoC 厂商,比如全志、NXP、Rockchip,都会在这里放一些初始化代码。我遇到过一个问题:某款全志 H3 的板子,串口死活不输出。查了半天,发现是 mach-sunxi/ 里的时钟初始化代码,跟我的晶振频率不匹配。

2.2.2 board/:板子的血肉

board/ 目录下,每个板子一个文件夹。里面通常包含:

  • board.c:板级初始化函数,比如 board_init()、dram_init()。
  • Kconfig:板级的配置菜单。
  • Makefile:编译脚本。

说白了,你的板子有什么特殊外设,上电要先初始化什么,都在这里写。

2.2.3 include/configs/:板子的灵魂

这个目录下的头文件,是 U-Boot 的“总开关”。比如:

include/configs/sunxi-common.h
include/configs/mx6ul_14x14_evk.h

里面定义了大量的宏:

  • CONFIG_SYS_TEXT_BASE:U-Boot 的运行地址。
  • CONFIG_NR_DRAM_BANKS:DDR 的 bank 数量。
  • CONFIG_BOOTCOMMAND:默认的启动命令。
我曾经踩过的坑: 修改 CONFIG_SYS_TEXT_BASE 后,忘了同步修改链接脚本。结果 U-Boot 启动后,直接跑飞了。记住,这个地址必须跟你的硬件设计一致。

2.3 启动流程源码级解析

这部分是重点。U-Boot 的启动,说白了就三个阶段。我画了张图,你配合着看:

U-Boot 启动流程三阶段 U-Boot 启动流程三阶段 阶段一:SPL arch/arm/cpu/start.S board_init_f() 初始化 DDR、时钟 加载 U-Boot 到 DDR 阶段二:U-Boot 重定位 board_init_r() 重定位自身到 DDR 初始化中断向量表 初始化串口、环境变量 阶段三:命令行/启动内核 main_loop() 读取 bootcmd 加载内核到内存 bootm / bootz 启动 关键入口函数:_start → board_init_f() → board_init_r() → main_loop() 你移植时,主要修改 board_init_f() 和 board_init_r() 里的板级代码 各阶段核心文件 SPL阶段:arch/arm/cpu/start.S, spl.c 重定位阶段:board.c, board_init_r() 命令行阶段:common/main.c, cmd/bootm.c 配置头文件:include/configs/<board>.h

2.3.1 第一阶段:SPL(Secondary Program Loader)

SPL 是 U-Boot 的“迷你版”。为什么要有它?因为很多 SoC 的 SRAM 太小了,装不下完整的 U-Boot。SPL 只做一件事:初始化 DDR,然后把完整的 U-Boot 从 Flash 搬到 DDR 里。

入口在 arch/arm/cpu/start.S,汇编写的。你想想看,这时候 C 环境都还没建立呢。SPL 的流程大致是:

  1. 设置 CPU 为 SVC 模式,关中断。
  2. 初始化时钟、关闭 MMU 和 Cache。
  3. 调用 board_init_f(),初始化 DDR。
  4. 从 Flash/NAND/SD 卡读取完整的 U-Boot 到 DDR。
  5. 跳转到 U-Boot 的入口。
调试技巧: SPL 阶段没有串口输出是常事。我一般会在 start.S 里加几个 GPIO 翻转的代码,用逻辑分析仪看波形,确认程序跑到哪一步了。

2.3.2 第二阶段:U-Boot 重定位

完整的 U-Boot 被加载到 DDR 后,会先运行 board_init_r()。这个函数在 common/board_r.c 里。它做几件关键的事:

  • 重定位:U-Boot 把自己从加载地址,搬到运行地址。这一步是为了给内核腾出空间。
  • 初始化中断向量表:让异常处理能正常工作。
  • 初始化串口:这时候你才能在终端上看到 U-Boot 的启动信息。
  • 初始化环境变量:加载 bootargs、bootcmd 等。

这里有个细节:board_init_r() 里有一个巨大的初始化函数列表,叫 init_sequence_r[]。每个外设的初始化,都是一个函数指针。你如果想让某个外设早点初始化,可以调整这个列表的顺序。

2.3.3 第三阶段:命令行与启动内核

最后,进入 main_loop()。它会检查环境变量 bootdelay,如果在倒计时结束前你没有按任何键,就自动执行 bootcmd 里的命令。默认的 bootcmd 通常是:

# 从 MMC 读取内核到内存,然后启动
mmc read 0x42000000 0x800 0x4000
bootz 0x42000000

如果你按了键,就进入 U-Boot 的命令行,可以手动输入命令调试。

我的经验: 调试阶段,我习惯把 bootdelay 设成 -1,这样 U-Boot 启动后直接进命令行,不会自动启动内核。等所有外设都调通了,再改回正常值。

2.4 关键文件速查表

为了方便你以后查阅,我把关键文件整理成了表格:

文件路径 作用 移植时是否修改
arch/arm/cpu/start.S SPL 入口,汇编初始化 一般不修改
board/<vendor>/<board>/board.c 板级初始化函数 必须修改
include/configs/<board>.h 板级配置宏定义 必须修改
arch/arm/dts/<board>.dts 设备树文件 根据硬件修改
common/board_r.c 第二阶段初始化流程 一般不修改
common/main.c 命令行主循环 一般不修改

嗯,到这里,U-Boot 的源码骨架你应该心里有数了。下次拿到一个新板子,别急着改代码。先找到 board/ 和 include/configs/ 下的参考文件,读一遍,再动手。你会发现,移植工作其实没那么玄乎。