嵌入式Linux学习指南之设备树——Linux内核设备树编译机制深度解析
仓库已经开源!所有教程,主线内核移植,跑新版本imx-linux/uboot都在这里!欢迎各位大佬观摩!喜欢的话点个⭐!
这是笔者在维护 imx-forge 项目时记录的一篇技术笔记,希望能帮助大家理解 Linux 内核中设备树的编译机制。
引言
如果你在开发嵌入式 Linux 系统,一定接触过设备树(Device Tree)。那些 .dts 文件最终是如何变成内核可识别的 .dtb 二进制文件的?这个过程中 GCC 和 DTC 又是如何协作的?
今天我们就来深入剖析 Linux 内核中设备树的两阶段编译流程,看看这背后巧妙的设计。
两阶段编译流程概览
Linux 内核处理设备树源文件(.dts)时,采用了一个精妙的两阶段编译策略:
源文件 (.dts)
↓
【阶段1】GCC 预处理 → 处理 #include 和宏定义
↓
临时文件 (.dts.tmp)
↓
【阶段2】DTC 编译 → 生成二进制格式
↓
目标文件 (.dtb)
为什么要分两步?
直接用 DTC 编译不就行了吗?为什么要先用 GCC 处理一遍?这个设计带来了几个显著优势:
✅ 完整的 C 预处理器支持:可以使用 #include、#define、#ifdef 等熟悉的语法
✅ 强大的依赖管理:自动追踪头文件变化,实现增量编译
✅ 灵活的条件编译:根据不同配置生成不同版本的设备树
✅ 与内核构建系统无缝集成:统一管理编译流程
深入源码:编译命令分析
让我们从内核源码入手,看看这个编译过程是如何实现的。核心逻辑位于 scripts/Makefile.dtbs 文件的第 132-137 行:
quiet_cmd_dtc = DTC $(quiet_dtb_check_tag) $@
cmd_dtc = \
$(HOSTCC) -E $(dtc_cpp_flags) -x assembler-with-cpp -o $(dtc-tmp) $< ; \
$(DTC) -o $@ -b 0 $(addprefix -i,$(dir $<) $(DTC_INCLUDE)) \
$(DTC_FLAGS) -d $(depfile).dtc.tmp $(dtc-tmp) ; \
cat $(depfile).pre.tmp $(depfile).dtc.tmp > $(depfile) \
$(cmd_dtb_check)
这个命令看似复杂,实际上就是三个步骤的串联。我们逐个拆解。
阶段 1:GCC 预处理
$(HOSTCC) -E $(dtc_cpp_flags) -x assembler-with-cpp -o $(dtc-tmp) $<
这里的参数很有讲究:
$(HOSTCC):使用主机系统的 GCC 编译器-E:只进行预处理,不编译(关键!)$(dtc_cpp_flags):预处理标志(稍后详解)-x assembler-with-cpp:将输入视为汇编语言,但启用 C 预处理器-o $(dtc-tmp):输出到临时文件.dts.tmp$<:输入的.dts源文件
预处理标志的定义(第 127 行):
dtc_cpp_flags = -Wp,-MMD,$(depfile).pre.tmp -nostdinc -I $(DTC_INCLUDE) -undef -D__DTS__
每个参数都有其深意:
| 参数 | 作用 | 设计意图 |
|---|---|---|
-Wp,-MMD,file | 生成依赖文件 | 追踪头文件变化,支持增量编译 |
-nostdinc | 禁用标准 C 头文件路径 | 隔离设备树编译环境,避免污染 |
-I $(DTC_INCLUDE) | 只添加设备树特定的包含路径 | 精确控制可访问的头文件 |
-undef | 取消所有预定义宏 | 避免编译器内置宏干扰 |
-D__DTS__ | 定义设备树编译宏 | 允许条件编译 |
为什么使用 -x assembler-with-cpp?
这是个巧妙的技巧。它告诉 GCC:"把输入文件当汇编语言处理,但启用 C 预处理器"。这样做的好处是:
✅ 支持完整的 C 预处理语法(#include、#define、#ifdef)
✅ 不要求符合 C 语法(设备树毕竟不是 C 代码)
✅ 允许设备树特有的语法结构
阶段 2:DTC 编译
$(DTC) -o $@ -b 0 $(addprefix -i,$(dir $<) $(DTC_INCLUDE)) \
$(DTC_FLAGS) -d $(depfile).dtc.tmp $(dtc-tmp)
参数解析:
$(DTC):设备树编译器-o $@:输出文件(.dtb)-b 0:设备树版本为 0(自动检测)-i ...:添加 include 搜索路径$(DTC_FLAGS):DTC 编译标志(如警告控制)-d $(depfile).dtc.tmp:生成 DTC 依赖文件$(dtc-tmp):输入文件(预处理后的临时文件)
include 路径展开:
$(addprefix -i,$(dir $<) $(DTC_INCLUDE))
假设 $< 是 arch/arm/boot/dts/board.dts,这行会展开为:
-i arch/arm/boot/dts/ -i scripts/dtc/include-prefixes
阶段 3:依赖合并
cat $(depfile).pre.tmp $(depfile).dtc.tmp > $(depfile)
将预处理依赖和 DTC 依赖合并,形成完整的依赖关系链。
include-prefixes 机制:架构无关的巧妙设计
这是内核设备树编译系统中最优雅的设计之一。
DTC_INCLUDE 定义
DTC_INCLUDE := $(srctree)/scripts/dtc/include-prefixes
目录结构
scripts/dtc/include-prefixes/
├── arc -> ../../../arch/arc/boot/dts
├── arm -> ../../../arch/arm/boot/dts
├── arm64 -> ../../../arch/arm64/boot/dts
├── dt-bindings -> ../../../include/dt-bindings
├── microblaze -> ../../../arch/microblaze/boot/dts
├── mips -> ../../../arch/mips/boot/dts
├── nios2 -> ../../../arch/nios2/boot/dts
├── openrisc -> ../../../arch/openrisc/boot/dts
├── powerpc -> ../../../arch/powerpc/boot/dts
├── riscv -> ../../../arch/riscv/boot/dts
├── sh -> ../../../arch/sh/boot/dts
└── xtensa -> ../../../arch/xtensa/boot/dts
工作原理
使用符号链接将架构特定的 DTS 目录映射到统一的 include-prefixes 目录。这样做的好处:
✅ 架构无关的 include 路径:可以用 <dt-bindings/...> 这样的统一写法
✅ 自动适配当前编译的架构:编译 ARM 时自动指向 ARM 目录
✅ 简化跨平台设备树的编写:同一份设备树可以在不同架构间复用
实际示例:
在设备树中可以这样写:
#include <dt-bindings/interrupt-controller/irq.h>
#include "imx6ull.dtsi" // 自动查找当前架构的目录
编译时:
dt-bindings会被解析为include/dt-bindingsimx6ull.dtsi会在arch/arm/boot/dts/中查找
依赖关系管理:双重保险
内核构建系统非常重视依赖追踪,对于设备树编译,它生成了两个阶段的依赖文件:
1. 预处理依赖(.pre.tmp)
由 gcc -E 的 -MMD 选项生成,记录:
.dts文件包含的所有头文件#include指令引用的文件
2. DTC 依赖(.dtc.tmp)
由 dtc 的 -d 选项生成,记录:
- DTC 工具内部的依赖
- 引用的其他设备树文件
3. 合并依赖
cat $(depfile).pre.tmp $(depfile).dtc.tmp > $(depfile)
将两个依赖文件合并,形成完整的依赖关系链。
增量编译的威力
完整的依赖信息使得内核构建系统能够:
✅ 只重新编译修改过的文件:大大加快编译速度 ✅ 精确追踪头文件变化:一个头文件的修改会触发所有依赖它的文件重新编译 ✅ 支持并行编译:依赖关系明确,可以安全并行
实战案例:完整的编译过程
让我们通过一个具体的例子,看看整个编译流程是如何运作的。
示例设备树文件
文件:arch/arm/boot/dts/board.dts
// SPDX-License-Identifier: (GPL-2.0 OR MIT)
/dts-v1/;
#include "imx6ull.dtsi"
#include "board-common.dtsi"
#include <dt-bindings/interrupt-controller/irq.h>
/ {
model = "Test Board";
compatible = "test,test-board";
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x10000000>;
};
};
实际编译过程
阶段 1:预处理
gcc -E \
-Wp,-MMD,board.dts.pre.tmp \
-nostdinc \
-I scripts/dtc/include-prefixes \
-undef -D__DTS__ \
-x assembler-with-cpp \
-o board.dts.tmp \
arch/arm/boot/dts/board.dts
生成的 board.dts.tmp(预处理后):
// ... imx6ull.dtsi 的内容展开 ...
// ... board-common.dtsi 的内容展开 ...
// ... irq.h 的内容展开 ...
/dts-v1/;
/ {
model = "Test Board";
compatible = "test,test-board";
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x10000000>;
};
};
阶段 2:DTC 编译
dtc -o board.dtb \
-b 0 \
-i arch/arm/boot/dts/ \
-i scripts/dtc/include-prefixes \
-Wno-unique_unit_address \
-d board.dtc.tmp \
board.dts.tmp
生成文件:
board.dtb- 二进制设备树文件board.dtc.tmp- DTC 依赖文件board.dts.tmp- 预处理后的临时文件
阶段 3:依赖合并
cat board.dts.pre.tmp board.dtc.tmp > .board.dtb.d
关键参数详解
DTC_FLAGS 常见设置
DTC_FLAGS += -Wno-unique_unit_address \
-Wno-unit_address_vs_reg \
-Wno-avoid_unnecessary_addr_size \
-Wno-alias_paths \
-Wno-interrupt_map \
-Wno-simple_bus_reg
这些标志禁用了一些设备树编译器的警告,原因包括:
- 设备树可能包含多个相似的节点(如多个串口)
- 某些验证规则在不同架构下不适用
- 历史兼容性考虑
符号输出选项(-@)
DTC_FLAGS += $(if $(filter $(patsubst $(obj)/%,%,$@), $(base-dtb-y)), -@)
如果设备树是基础 DTB(支持 overlay),则添加 -@ 选项:
- 作用:生成符号信息,允许设备树 overlay 动态添加节点
- 应用场景:可插拔设备、BeagleBone Cape 等
编译产物链:从源码到内核
完整的编译链路:
源文件:
board.dts
↓ [gcc -E 预处理]
临时文件:
board.dts.tmp (预处理后的 DTS)
↓ [dtc 编译]
board.dtb (二进制设备树)
↓ [包装成汇编]
board.dtb.S
↓ [汇编器]
board.dtb.o (目标文件)
↓ [链接器]
内核镜像或模块
为什么要包装成目标文件?
- ✅ 将设备树链接到内核镜像,统一管理
- ✅ 支持模块化设备树(可以动态加载)
- ✅ 统一的构建流程,与其他内核代码保持一致
总结与思考
Linux 内核的设备树编译机制体现了几个重要的设计原则:
1. 分离关注点
预处理和编译分离,每个工具做它最擅长的事:
- GCC 擅长处理 C 预处理器语法
- DTC 擅长编译设备树
2. 依赖管理
完整的依赖追踪确保:
- 增量编译的正确性
- 构建系统的效率
- 变更影响的可预测性
3. 跨平台支持
通过符号链接实现架构无关:
- 简化设备树编写
- 提高代码复用性
- 降低维护成本
4. 构建系统集成
与 Make 构建系统无缝集成:
- 统一的编译接口
- 一致的依赖管理
- 标准化的输出格式
关键要点回顾
⭐ 使用 gcc -E 进行预处理:支持完整的 C 预处理器语法,增强表达能力
⭐ 使用 -nostdinc 隔离环境:避免系统头文件污染,确保可重复构建
⭐ 通过符号链接实现架构无关:优雅的跨平台解决方案
⭐ 双重依赖文件确保正确性:预处理和编译两阶段都生成依赖信息
扩展阅读
如果你想深入了解设备树的更多细节,可以参考以下资源:
- 设备树规范(Devicetree Specification) - 官方规范文档
- Linux 内核设备树文档 - 内核官方文档
- DTC 工具源码 - 设备树编译器源码
相关阅读
- 入门 · 环境搭建 · 00 · Qt6 安装踩坑指南 - 相似度 80%
- 深入理解Linux模块——模块参数与内核调试:让模块"活"起来的魔法 - 相似度 80%
- 深入理解Linux模块——内核模块编译与加载详解:从 Makefile 到 insmod 的完整旅程 - 相似度 80%