U-Boot 源码结构解析:启动流程、关键目录与 Kconfig 配置系统
U-Boot 这玩意儿,说白了就是嵌入式系统的“第一口奶”。板子上电后,CPU 跑的第一段正经代码就是它。我这些年移植 U-Boot 到各种稀奇古怪的板子上,踩过的坑能装一箩筐。今天咱们就把 U-Boot 的源码结构、启动流程和配置系统掰开揉碎了讲清楚。
U-Boot 启动流程分析
先看整体流程。U-Boot 启动分两个阶段:SPL(Secondary Program Loader)和 U-Boot 本体。SPL 是精简版,负责初始化最基础的硬件(时钟、DDR),然后加载完整的 U-Boot。
核心启动阶段:
- Stage 1 (SPL):汇编阶段,设置异常向量、初始化 CPU 模式、关闭中断、初始化 DDR
- Stage 2 (U-Boot proper):C 语言阶段,board_init_f() → relocate_code() → board_init_r()
我刚开始做移植时,总以为 SPL 可有可无。直到有一次在某个 ARM9 板子上,DDR 初始化代码写错了,SPL 直接跑飞,连串口都没打印。嗯,从那以后我再也不敢轻视 SPL 了。
具体启动顺序是这样的:
- reset 向量:CPU 上电后从 0x00000000 或 0xFFFF0000 取指,跳转到 _start
- lowlevel_init:关看门狗、初始化时钟、设置栈指针
- spl_init:初始化串口、GPIO、定时器
- dram_init:检测并初始化 DDR 控制器
- spl_load_image:从 SPI Flash / SD 卡 / NAND 加载 U-Boot 到 DDR
- spl_jump_to_image:跳转到 U-Boot 入口
为什么 SPL 要用汇编写开头?因为这时候 C 环境还没准备好——栈指针没设、BSS 段没清、全局变量不能用。你想想看,连个函数调用都做不了,只能用汇编硬扛。
我的经验:调试 SPL 时,最有效的办法是在 lowlevel_init 里加 GPIO 翻转。串口可能还没初始化,但 GPIO 是死的,翻一下就能用逻辑分析仪看到。我曾经靠这个办法在 3 小时内定位了一个 DDR 时序问题。
关键目录与文件功能
U-Boot 源码目录看着吓人,其实核心就那么几个。我列个表,你对照着看:
| 目录/文件 | 功能 | 我常用的场景 |
|---|---|---|
| arch/ | CPU 架构相关代码(arm、riscv、x86 等) | 修改 start.S 里的异常向量 |
| board/ | 板级初始化代码(厂商名/板子名) | 添加新板子时,复制参考板然后改 |
| common/ | 通用命令和框架代码 | 添加自定义命令时在这里加 |
| drivers/ | 外设驱动(串口、网卡、MMC、SPI 等) | 移植新网卡芯片时重点看 |
| include/configs/ | 板级配置头文件(.h) | 裁剪功能、调整内存布局 |
| dts/ | 设备树源文件(.dts / .dtsi) | 描述硬件拓扑,U-Boot 用它来知道外设地址 |
| tools/ | 镜像打包工具(mkimage 等) | 制作 U-Boot 镜像时用 |
我个人习惯,拿到一个新板子,第一件事就是看 board/ 和 include/configs/ 这两个目录。前者告诉你板子怎么初始化,后者告诉你功能怎么配。
举个例子,include/configs/ 里的头文件,以前是 U-Boot 的“配置中心”。现在虽然大部分配置迁移到了 Kconfig,但有些关键参数(比如 CONFIG_SYS_TEXT_BASE、CONFIG_NR_DRAM_BANKS)还是留在这里。你改错了,板子直接变砖。
注意:别把 include/configs/ 里的宏和 Kconfig 的配置搞混了。前者是“硬编码”的板级参数,后者是“可选项”。我曾经在某个项目里,把 CONFIG_SYS_TEXT_BASE 写到了 Kconfig 里,结果编译出来的镜像地址对不上,SPL 跳转时直接跑飞。查了两天才发现。
配置系统 Kconfig 详解
Kconfig 是 Linux 内核那套配置系统的移植版。U-Boot 从 2014 年开始逐步迁移到 Kconfig,现在大部分功能选项都在这里了。
Kconfig 的核心文件是 Kconfig 和 arch/arm/Kconfig(以 ARM 为例)。它们定义了各种配置选项的依赖关系和默认值。
看一个实际例子:
config CMD_BOOTM
bool "bootm command"
default y
help
Boot an application image from memory.
This command is used to boot Linux kernel images.
这个配置项控制 bootm 命令是否编译进 U-Boot。如果你用 booti 或 bootz,可以把 CMD_BOOTM 关掉,省点空间。
Kconfig 的依赖关系很有意思。比如:
config FIT
bool "Flattened Image Tree (FIT)"
select FIT_SIGNATURE if FIT_SIGNATURE_ENABLED
help
This option allows you to boot images in FIT format.
这里 select 关键字表示:如果你选了 FIT,并且 FIT_SIGNATURE_ENABLED 也开了,那 FIT_SIGNATURE 会自动被选中。这种“自动依赖”机制,省去了手动配置的麻烦。
我的建议:做产品时,用 make menuconfig 来配置 U-Boot。别手写 .config 文件,容易漏依赖。我见过有人直接改 .config,结果漏了某个 select 依赖,编译出来的 U-Boot 功能不全,折腾了一周才发现。
Kconfig 的层级结构是这样的:
- 顶层 Kconfig:定义架构、板子、通用选项
- arch/arm/Kconfig:ARM 架构特有选项(CPU 类型、MMU 等)
- board/xxx/Kconfig:板级选项(DDR 大小、外设使能)
- cmd/Kconfig:命令选项(bootm、sf probe 等)
- drivers/Kconfig:驱动选项(网卡、MMC、SPI 等)
你执行 make menuconfig 时,看到的界面就是这些 Kconfig 文件“拼”出来的。每个选项都有 prompt(显示文字)、type(bool、int、string 等)、depends on(依赖条件)。
举个例子,你想让 U-Boot 支持从 SD 卡启动:
config MMC
bool "MMC/SD/SDIO support"
select MMC_WRITE if MMC_WRITE_ENABLED
help
Enable support for MMC/SD/SDIO devices.
选了 MMC 之后,还要选具体的驱动:
config MMC_SUNXI
bool "Allwinner Sunxi MMC controller"
depends on MMC && ARCH_SUNXI
help
This selects support for the Allwinner Sunxi MMC controller.
看到没?depends on MMC && ARCH_SUNXI 表示:只有当你选了 MMC 并且目标芯片是 Allwinner Sunxi 时,这个选项才会出现。这种“条件显示”机制,让配置界面干净很多。
配置系统核心逻辑:
make menuconfig→ 读取 Kconfig 生成配置界面- 用户选择 → 写入
.config文件 make→ 根据.config生成autoconf.mk和config.h- 编译时,源码通过
#ifdef CONFIG_XXX条件编译
下面这张图,把 U-Boot 的源码结构、启动流程和配置系统的关系画清楚了:
这张图把三者的关系画得很清楚。源码目录提供代码,Kconfig 控制哪些代码被编译进去,启动流程决定代码的执行顺序。你改任何一个环节,都得考虑对其他两个的影响。
避坑指南:我曾经在移植 U-Boot 到新板子时,直接复制了参考板的 .config 文件。结果板子死活起不来。后来发现,参考板用的是 SPI Flash,我的板子用的是 NAND Flash,但 .config 里没开 NAND 驱动。Kconfig 的依赖关系没处理好,编译出来的 U-Boot 根本不支持 NAND 读取。从那以后,我每次移植都老老实实跑一遍 make menuconfig,逐个确认关键选项。
最后说一句,Kconfig 的 defconfig 文件也很重要。每个板子都有一个默认配置,比如 configs/xxx_defconfig。你执行 make xxx_defconfig 时,U-Boot 会读取这个文件,生成初始的 .config。然后你再跑 make menuconfig 微调。这个流程,我建议你形成肌肉记忆。