第四章 U-Boot源码分析(一):顶层Makefile与目录结构

好,咱们正式开始啃U-Boot的源码。说实话,很多做移植的朋友一上来就急着改代码,结果改了半天连编译都过不了。我个人习惯是,先花半天时间把源码骨架摸清楚。你想想看,连Makefile怎么工作的都不知道,怎么加自己的板子?

这一章我们重点看四个东西:顶层Makefile的执行逻辑、arch/目录的CPU体系结构布局、board/目录的板级支持包结构、以及configs/目录下的默认配置怎么写。掌握了这些,你才算真正拿到了U-Boot移植的钥匙。

4.1 顶层Makefile:U-Boot的编译引擎

顶层Makefile是整个U-Boot编译的入口。我刚开始看的时候也觉得头大,几千行的Makefile,谁受得了?但其实核心逻辑就几条主线。

4.1.1 整体执行流程

U-Boot的编译分两步走:先配置(make xxx_defconfig),再编译(make)。配置阶段生成.config文件,编译阶段根据.config决定哪些文件参与构建。

核心流程:

  1. 配置阶段:make xxx_defconfig → 从configs/目录复制默认配置 → 生成.config
  2. 依赖生成:make → 解析.config → 生成include/autoconf.mk(C头文件宏定义)
  3. 编译链接:逐目录递归编译 → 链接生成u-boot、u-boot.bin等目标文件

嗯,这里要注意:include/autoconf.mk是Makefile和C代码之间的桥梁。你在.config里配了CONFIG_XXX=y,它就会变成Makefile里的变量和C代码里的宏。

4.1.2 关键变量与目标

变量/目标 作用 我的备注
ARCH 目标CPU架构,如arm、riscv、x86 移植第一件事就是设这个
CROSS_COMPILE 交叉编译工具链前缀 比如arm-linux-gnueabihf-
CONFIG_SYS_TEXT_BASE U-Boot的链接基地址 这个错了直接起不来
u-boot.bin 最终生成的二进制镜像 烧到板子上就靠它
u-boot.srec Motorola S-record格式 有些调试器用这个

我曾经在一个项目里,因为CROSS_COMPILE没设对,编译出来的U-Boot在板子上跑起来就崩。查了两天才发现是工具链版本不匹配。所以我的建议是:编译前先确认工具链,用$(CROSS_COMPILE)gcc -v看一眼版本。

4.1.3 递归编译机制

顶层Makefile不会自己编译所有文件。它通过scripts/Makefile.build这个通用编译规则,递归进入每个子目录。每个子目录里都有一个Makefile,里面用obj-y、obj-$(CONFIG_XXX)这种形式列出要编译的文件。

# 示例:drivers/net/Makefile 中的典型写法
obj-$(CONFIG_DRIVER_ETH) += eth.o
obj-$(CONFIG_DRIVER_DM9000) += dm9000.o
obj-$(CONFIG_DRIVER_CS8900) += cs8900.o

说白了,obj-y就是“无论如何都编译”,obj-$(CONFIG_XXX)就是“如果配置了XXX就编译”。这种设计让U-Boot的模块化做得非常好,加一个新驱动只需要改一个Makefile。

小技巧:如果你想临时加一个调试文件,又不想改.config,可以直接在对应目录的Makefile里加一行obj-y += my_debug.o。但记得调试完删掉,不然提交代码时会被人骂。

4.2 arch/目录:CPU体系结构的家

arch/目录存放的是与CPU体系结构相关的代码。不同架构的CPU,启动方式、寄存器布局、中断处理都不一样,这些差异全在这里隔离。

4.2.1 目录结构概览

arch/
├── arm/          # ARM架构(最常用)
│   ├── cpu/      # CPU核心代码(如armv7、armv8)
│   ├── include/  # 架构相关头文件
│   ├── lib/      # 架构通用库函数
│   ├── mach-*/   # 具体SoC厂商代码(mach-sunxi、mach-rockchip等)
│   └── dts/      # 设备树源文件
├── riscv/        # RISC-V架构
├── x86/          # x86架构
├── sandbox/      # 用于测试的模拟架构
└── ...           # 其他架构

我个人觉得,arch/arm/是移植的重点。因为绝大多数嵌入式板子都用ARM。你看mach-*目录,每个目录对应一家SoC厂商,比如全志的sunxi、瑞芯微的rockchip、NXP的imx。移植的时候,你大概率要在这里加代码。

4.2.2 启动流程中的arch角色

U-Boot的启动第一阶段(汇编阶段)就在arch/arm/cpu/里。以ARMv8为例:

  1. reset向量:CPU上电后第一条指令,在arch/arm/cpu/armv8/start.S里
  2. 设置异常向量表:中断、同步异常的处理入口
  3. 初始化CPU模式:从EL3切到EL2或EL1
  4. 初始化C运行环境:设置栈指针、清零BSS段
  5. 跳转到board_init_f():进入C语言阶段

避坑指南:我曾经在移植时,start.S里的栈指针设小了,结果board_init_f()里一调用函数就栈溢出,板子直接死机。调试这种问题特别痛苦,因为连串口都还没初始化。我的建议是:栈大小至少给4KB,别抠门。

4.3 board/目录:板级支持包的核心

board/目录存放的是具体板卡的代码。一个SoC可以对应多块板子,每块板子的DDR型号、GPIO分配、外设接口都可能不同,这些差异就在board/里处理。

4.3.1 目录组织方式

board/
├── allwinner/           # 全志系列
│   ├── sunxi/           # 通用sunxi板级代码
│   └── orangepi_zero2/  # 香橙派Zero2专用
├── rockchip/            # 瑞芯微系列
│   ├── evb_rk3399/      # RK3399 EVB板
│   └── nanopi_neo4/     # NanoPi NEO4
└── ...

每个板级目录里通常包含:

  • board.c:板级初始化函数,如board_init()、dram_init()
  • Kconfig:板级配置选项
  • MAINTAINERS:维护者信息
  • Makefile:编译规则

4.3.2 板级初始化流程

从board_init_f()到board_init_r(),中间经历了重定位。我画个图帮你理解:

U-Boot板级初始化流程 start.S (汇编) board_init_f() 重定位 (relocate) board_init_r() 进入主循环 (main_loop) 关键说明 board_init_f(): 初始化DDR、串口 重定位: 将U-Boot复制到DDR高位 board_init_r(): 初始化所有外设 main_loop: 等待用户输入命令

你看这个流程,board_init_f()和board_init_r()是板级代码的两个核心函数。前者在Flash里运行,负责初始化DDR和串口;后者在DDR里运行,负责初始化所有外设。中间的重定位是关键——U-Boot把自己从Flash复制到DDR的高地址区域,这样才能跑得更快。

经验之谈:调试板级代码时,我习惯在board_init_f()里加串口打印。因为这时候串口已经初始化了,能看到U-Boot到底卡在哪一步。比如加一句debug("board_init_f: start\n");,配合CONFIG_DEBUG宏,能省不少调试时间。

4.4 configs/目录:默认配置的仓库

configs/目录里放的是所有板子的默认配置文件。文件名格式通常是<板子名>_defconfig。比如orangepi_zero2_defconfig就是香橙派Zero2的默认配置。

4.4.1 配置文件的结构

一个典型的_defconfig文件长这样:

CONFIG_ARM=y
CONFIG_ARCH_SUNXI=y
CONFIG_MACH_SUN8I_H3=y
CONFIG_DRAM_CLK=408
CONFIG_DRAM_ZQ=123
CONFIG_DRAM_ODT_EN=y
CONFIG_SYS_TEXT_BASE=0x4A000000
CONFIG_MMC=y
CONFIG_MMC_SUNXI=y
CONFIG_USB=y
CONFIG_USB_EHCI_HCD=y
CONFIG_USB_OHCI_HCD=y
CONFIG_ETH_DESIGNWARE=y
CONFIG_PHY_REALTEK=y

每一行就是一个配置选项。以CONFIG_开头,=y表示启用,=n表示禁用,=<值>表示参数。这些配置最终会生成.config文件,然后被scripts/kconfig/里的工具解析。

4.4.2 配置的层次结构

U-Boot的配置是有层次的,不是所有选项都在_defconfig里。实际上:

配置来源 优先级 说明
_defconfig 最高 板级默认配置,用户可修改
Kconfig默认值 中 各目录Kconfig文件里定义的默认值
arch/Kconfig 低 架构级默认值

举个例子,你在_defconfig里写了CONFIG_MMC=y,但drivers/mmc/Kconfig里可能默认就是y,所以你不写也行。但如果你要禁用MMC,就必须在_defconfig里写CONFIG_MMC=n来覆盖默认值。

避坑指南:我曾经犯过一个错误——在_defconfig里写了一大堆配置,结果编译出来U-Boot太大了,放不进Flash。后来才发现,很多配置其实用默认值就行,不需要显式写出来。我的建议是:只写与默认值不同的配置,保持_defconfig精简。一般50行以内就够了。

4.4.3 如何添加新板子的配置

如果你要移植一块新板子,添加配置的步骤很简单:

  1. 在configs/目录下创建myboard_defconfig
  2. 参考同SoC的现有配置,复制一份并修改
  3. 运行make myboard_defconfig生成.config
  4. 运行make menuconfig微调配置
  5. 运行make savedefconfig生成精简版的_defconfig

第5步很多人不知道。make savedefconfig会自动去掉那些与Kconfig默认值相同的配置项,只保留差异项。这样生成的_defconfig既干净又容易维护。

核心要点总结:

  • 顶层Makefile是编译引擎,理解obj-y和obj-$(CONFIG_XXX)的机制
  • arch/目录隔离CPU架构差异,移植时重点关注mach-*目录
  • board/目录处理板级差异,board_init_f()和board_init_r()是核心
  • configs/目录存放默认配置,用make savedefconfig保持配置精简