嵌入式Linux学习指南之设备树——Linux内核设备树编译机制深度解析仓库已经开源所有教程主线内核移植跑新版本imx-linux/uboot都在这里欢迎各位大佬观摩喜欢的话点个⭐仓库地址https://github.com/Awesome-Embedded-Learning-Studio/imx-forge静态网页https://awesome-embedded-learning-studio.github.io/imx-forge/这是笔者在维护 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)这个命令看似复杂实际上就是三个步骤的串联。我们逐个拆解。阶段 1GCC 预处理$(HOSTCC)-E$(dtc_cpp_flags)-xassembler-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 代码✅ 允许设备树特有的语法结构阶段 2DTC 编译$(DTC)-o$-b0$(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这行会展开为-iarch/arm/boot/dts/-iscripts/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; memory80000000 { device_type memory; reg 0x80000000 0x10000000; }; };实际编译过程阶段 1预处理gcc-E\-Wp,-MMD,board.dts.pre.tmp\-nostdinc\-Iscripts/dtc/include-prefixes\-undef-D__DTS__\-xassembler-with-cpp\-oboard.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; memory80000000 { device_type memory; reg 0x80000000 0x10000000; }; };阶段 2DTC 编译dtc-oboard.dtb\-b0\-iarch/arm/boot/dts/\-iscripts/dtc/include-prefixes\-Wno-unique_unit_address\-dboard.dtc.tmp\board.dts.tmp生成文件board.dtb- 二进制设备树文件board.dtc.tmp- DTC 依赖文件board.dts.tmp- 预处理后的临时文件阶段 3依赖合并catboard.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%