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 了。

具体启动顺序是这样的:

  1. reset 向量:CPU 上电后从 0x00000000 或 0xFFFF0000 取指,跳转到 _start
  2. lowlevel_init:关看门狗、初始化时钟、设置栈指针
  3. spl_init:初始化串口、GPIO、定时器
  4. dram_init:检测并初始化 DDR 控制器
  5. spl_load_image:从 SPI Flash / SD 卡 / NAND 加载 U-Boot 到 DDR
  6. 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 的源码结构、启动流程和配置系统的关系画清楚了:

U-Boot 源码结构与启动流程 U-Boot 源码结构与启动流程 源码目录结构 arch/ — CPU 架构代码 board/ — 板级初始化 drivers/ — 外设驱动 include/configs/ — 板级配置 dts/ — 设备树源文件 启动流程 ① reset 向量 → _start ② lowlevel_init (时钟/DDR) ③ spl_init (串口/GPIO) ④ dram_init (DDR 初始化) ⑤ spl_load_image (加载 U-Boot) ⑥ spl_jump_to_image (跳转) Kconfig 配置系统 顶层 Kconfig arch/arm/Kconfig board/xxx/Kconfig cmd/Kconfig drivers/Kconfig 说明:源码目录提供代码,Kconfig 控制编译选项,启动流程串联执行 三者配合,才能让 U-Boot 在目标板子上跑起来 输出产物:u-boot.bin / u-boot-spl.bin / u-boot.img

这张图把三者的关系画得很清楚。源码目录提供代码,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 微调。这个流程,我建议你形成肌肉记忆。