第四章 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决定哪些文件参与构建。
核心流程:
- 配置阶段:
make xxx_defconfig→ 从configs/目录复制默认配置 → 生成.config - 依赖生成:
make→ 解析.config→ 生成include/autoconf.mk(C头文件宏定义) - 编译链接:逐目录递归编译 → 链接生成
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为例:
- reset向量:CPU上电后第一条指令,在
arch/arm/cpu/armv8/start.S里 - 设置异常向量表:中断、同步异常的处理入口
- 初始化CPU模式:从EL3切到EL2或EL1
- 初始化C运行环境:设置栈指针、清零BSS段
- 跳转到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(),中间经历了重定位。我画个图帮你理解:
你看这个流程,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 如何添加新板子的配置
如果你要移植一块新板子,添加配置的步骤很简单:
- 在
configs/目录下创建myboard_defconfig - 参考同SoC的现有配置,复制一份并修改
- 运行
make myboard_defconfig生成.config - 运行
make menuconfig微调配置 - 运行
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保持配置精简