3. 设备树核心概念
做嵌入式的人,几乎都绕不过U-Boot。我刚开始接触它的时候,第一反应是——这代码量也太大了。但其实它的顶层结构非常清晰,说白了就是一套「架构分离、板级独立、配置驱动」的框架。今天我们就把它拆开看看。
3.1 顶层目录布局:一眼看懂U-Boot的家底
U-Boot的顶层目录,每个文件夹都有明确的职责。我习惯把它们分成三类:核心代码、板级相关、构建工具。
| 目录 | 职责 | 我的理解 |
|---|---|---|
arch/ |
CPU架构相关代码(ARM、RISC-V、x86等) | 芯片底层的「方言」 |
board/ |
具体板卡的初始化代码 | 每块板子的「身份证」 |
configs/ |
默认配置文件(defconfig) | 板级功能的「开关清单」 |
dts/ |
设备树源文件(.dts / .dtsi) | 硬件拓扑的「说明书」 |
drivers/ |
各类外设驱动 | 功能模块的「积木盒」 |
include/ |
全局头文件、配置宏 | 整个项目的「公共词典」 |
common/ |
通用命令和框架代码 | U-Boot的「骨架」 |
lib/ |
通用库函数(crc、sha、lzma等) | 谁都能用的「工具箱」 |
net/ |
网络协议栈 | 网络功能的「引擎」 |
fs/ |
文件系统支持 | 读写存储的「翻译官」 |
关键认知:U-Boot的目录设计遵循「关注点分离」。你想改CPU初始化,去arch/;想改板级外设,去board/;想改引脚定义,去dts/。别在错误的地方找答案。
3.2 arch/ 目录:CPU架构的「方言区」
进入arch/,你会看到arm/、riscv/、x86/等子目录。每个架构下又按SoC厂商细分。以ARM为例:
arch/arm/
├── cpu/ # CPU核心初始化(如armv7、armv8)
├── dts/ # 架构级设备树(.dtsi)
├── include/ # 架构相关头文件
├── lib/ # 架构通用函数(如cache、mmu)
├── mach-*/ # 各SoC厂商代码(mach-sunxi、mach-rockchip等)
└── config.mk # 架构编译规则
我记得第一次移植时,在arch/arm/cpu/armv8/start.S里找异常向量表,找了半天。后来才明白——启动流程是架构相关的,板级差异只体现在外设初始化上。这个认知帮我省了很多时间。
3.3 board/ 目录:板级代码的「身份证」
board/目录下,每个板卡厂商一个子目录。比如:
board/ti/
├── am335x/ # BeagleBone Black等
├── dra7xx/ # DRA7系列
└── common/ # TI通用板级代码
每个板级目录通常包含:
board.c— 板级初始化函数(board_init_f、board_init_r)Kconfig— 板级配置选项Makefile— 编译规则MAINTAINERS— 维护者信息
我的习惯:添加新板卡时,先找一个硬件相似的现有板级目录,直接复制一份再改。比如你的板子用全志H3,就从board/sunxi/下手。别从零开始写,那是自找麻烦。
3.4 configs/ 目录:板级功能的「开关清单」
每个板卡在configs/下都有一个xxx_defconfig文件。它本质上是Kconfig的默认配置快照。举个例子:
# configs/sandbox_defconfig
CONFIG_SYS_TEXT_BASE=0x00000000
CONFIG_SANDBOX=y
CONFIG_SANDBOX_SDL=y
CONFIG_CMD_BOOTM=y
CONFIG_CMD_GPIO=y
这里每一行都是一个开关。你想想看,U-Boot的构建系统就是靠这个文件,决定哪些功能编译进去,哪些不编译。我见过不少新手直接改.config,结果make distclean后全丢了——永远只改defconfig文件。
3.5 dts/ 目录:硬件拓扑的「说明书」
设备树是U-Boot和Linux共享的硬件描述语言。在dts/目录下:
dts/
├── arch/ # 按架构分(arch/arm/dts/)
│ └── arm/
│ ├── am335x-boneblack.dts
│ ├── am33xx.dtsi # SoC级通用定义
│ └── am335x.dtsi # 系列通用定义
└── Makefile # 设备树编译规则
设备树的层级关系:
.dtsi— SoC级或系列级通用定义(如时钟、中断控制器).dts— 板级具体定义(如GPIO引脚、外设地址)
我曾经踩过的坑:在.dts里重复定义了.dtsi中已有的节点,导致编译报错。记住——.dtsi是「基础模板」,.dts只做「增量覆盖」。别重复造轮子。
3.6 Kconfig与Makefile构建系统:U-Boot的「大脑」
U-Boot使用Kconfig管理配置,用Makefile控制编译。这套组合拳,说白了就是:你选什么功能,它就编译什么代码。
3.6.1 Kconfig配置体系
Kconfig文件分布在各个目录,顶层Kconfig通过source语句引入子目录配置:
# 顶层 Kconfig
source "arch/Kconfig"
source "board/Kconfig"
source "drivers/Kconfig"
source "fs/Kconfig"
source "net/Kconfig"
source "dts/Kconfig"
每个子目录的Kconfig定义自己的配置项。比如:
# drivers/gpio/Kconfig
config GPIO_SUNXI
bool "Sunxi GPIO driver"
depends on ARCH_SUNXI
help
Say Y here to support GPIO on Allwinner Sunxi SoCs.
配置流程:
make xxx_defconfig— 加载默认配置make menuconfig— 图形化调整(可选)make— 根据.config编译
3.6.2 Makefile编译规则
U-Boot的Makefile体系分三层:
| 层级 | 文件 | 作用 |
|---|---|---|
| 顶层 | Makefile |
入口,定义全局规则、链接脚本 |
| 架构层 | arch/arm/config.mk |
架构编译选项(如-march=armv8-a) |
| 板级层 | board/xxx/Makefile |
板级源文件列表 |
一个典型的板级Makefile:
# board/ti/am335x/Makefile
obj-y += board.o
obj-y += mux.o
obj-$(CONFIG_SPL_BUILD) += spl.o
这里obj-y表示总是编译,obj-$(CONFIG_xxx)表示条件编译。嗯,这套规则和Linux内核完全一致,如果你做过内核驱动,上手会很快。
3.7 如何添加一个新的板级支持:实战五步法
现在,假设我们要为一块新板卡myboard(基于全志H3 SoC)添加U-Boot支持。我会按这五步走:
Step 1:创建板级目录
mkdir -p board/sunxi/myboard
cp board/sunxi/board.c board/sunxi/myboard/
# 修改board.c中的板级初始化函数
Step 2:创建设备树
cp dts/arch/arm/sun8i-h3-orangepi-pc.dts dts/arch/arm/sun8i-h3-myboard.dts
# 修改model、memory、regulator等节点
Step 3:创建默认配置文件
cp configs/orangepi_pc_defconfig configs/myboard_defconfig
# 修改CONFIG_DEFAULT_DEVICE_TREE指向新dts
# 调整CONFIG_SYS_TEXT_BASE等板级参数
Step 4:注册到Kconfig
在board/sunxi/Kconfig中添加:
config TARGET_MYBOARD
bool "Support myboard"
select ARCH_SUNXI
help
Support for the myboard board based on H3.
同时在顶层arch/arm/Kconfig的source链中确保能引用到。
Step 5:验证编译
make myboard_defconfig
make -j4
# 检查u-boot.bin和u-boot-sunxi-with-spl.bin是否生成
核心原则:添加板级支持,本质上是「复制-修改-注册」三部曲。别想着重写框架,U-Boot的框架已经很成熟了,你只需要填好自己的那块拼图。
3.8 知识结构总览
下面这张图,把U-Boot的源码结构和板级添加流程串起来了。我建议你把它存下来,每次移植前看一眼。
3.9 避坑指南
我曾经犯过的错:
- 设备树节点名不匹配:在
.dts中定义的节点名,和驱动中compatible字符串对不上。U-Boot不会报错,但驱动就是加载不了。检查compatible是第一步。 - defconfig遗漏关键选项:比如忘了开
CONFIG_SPL,结果SPL根本没编译。建议对照参考板卡的defconfig逐行核对。 - Kconfig依赖链断裂:新板卡依赖某个架构选项,但Kconfig里没写
select。编译时一堆未定义宏。用make menuconfig检查依赖是否满足。
我的调试技巧:添加新板卡后,先别急着加功能。用make V=1编译一次,看编译了哪些文件。如果板级代码没被编译,八成是Kconfig或Makefile没配好。这个习惯帮我省了无数排查时间。