2. DTS文件结构:DTS、DTSI、DTB、DTC的关系与编译流程

设备树(Device Tree)在Linux内核驱动开发中扮演着硬件描述的核心角色。理解DTS、DTSI、DTB、DTC这四个关键概念及其转换流程,是掌握设备树编程的第一步。本章将系统梳理这些文件类型的功能定位、语法差异以及从源文件到二进制文件的完整编译链路。

2.1 核心概念与文件类型

设备树体系包含四种主要文件形态,它们分别处于开发、编译、分发和运行的不同阶段:

文件后缀 全称 本质 可读性 典型用途
.dts Device Tree Source 文本源文件 人类可读 板级设备树主文件
.dtsi Device Tree Source Include 文本头文件 人类可读 SoC公共定义、外设复用
.dtb Device Tree Blob 二进制文件 机器可读 Bootloader传递给内核
.dtc Device Tree Compiler 编译工具 可执行程序 将.dts/.dtsi编译为.dtb
关键区分: .dtsi 与 .dts 在语法上完全一致,区别仅在于用途约定。.dtsi 通常被 .dts 通过 #include 包含,用于存放SoC级通用描述,避免重复定义。

2.2 文件关系与包含机制

一个典型的板级设备树项目,通常由一个主 .dts 文件通过 #include 预处理器指令引入多个 .dtsi 文件,形成层次化的描述结构:

// 主文件:imx6ull-myboard.dts
#include "imx6ull.dtsi"          // SoC 级定义
#include "imx6ull-pinfunc.h"     // 引脚复用宏定义
#include "myboard-lcd.dtsi"      // 板级外设模块

/ {
    model = "MyBoard i.MX6ULL";
    compatible = "myboard,imx6ull", "fsl,imx6ull";

    memory {
        device_type = "memory";
        reg = <0x80000000 0x20000000>;  // 512MB RAM
    };

    &uart1 {
        pinctrl-names = "default";
        pinctrl-0 = <&pinctrl_uart1>;
        status = "okay";
    };
};
包含路径解析: DTC 在编译时会调用 C 预处理器(cpp),因此 .dts 中可以使用 #include、#define、#ifdef 等预处理指令。头文件搜索路径通过 -I 参数指定,通常包括内核源码的 arch/arm/boot/dts/ 目录。

2.3 编译流程:从源文件到二进制文件

DTC 编译器将可读的 .dts 源文件转换为紧凑的 .dtb 二进制文件,整个过程分为三个阶段:

  1. 预处理阶段: C预处理器(cpp)处理 #include、#define、#ifdef 等指令,展开所有宏和头文件,生成一个完整的、无依赖的临时 .dts 文件。
  2. 解析与语法检查阶段: DTC 解析器读取预处理后的 .dts,构建内部节点树,检查语法错误、属性合法性、节点唯一性等。
  3. 二进制输出阶段: DTC 将验证通过的设备树结构序列化为 FDT(Flattened Device Tree)格式的 .dtb 文件,包含节点、属性、字符串表、内存保留区等。
常见编译错误: 节点名称重复、属性值类型不匹配、引用的 &label 未定义、reg 属性地址长度不匹配等。使用 dtc -I dts -O dtb -o output.dtb input.dts 编译时,务必检查终端输出的错误信息。

2.4 编译与反编译命令

DTC 工具支持正向编译和反向反编译,方便开发者调试和验证设备树内容:

# 正向编译:.dts → .dtb
dtc -I dts -O dtb -o myboard.dtb myboard.dts

# 反编译:.dtb → .dts(还原为可读文本)
dtc -I dtb -O dts -o myboard.dts myboard.dtb

# 查看 .dtb 内部结构(不生成文件)
dtc -I dtb -O dts /sys/firmware/devicetree/base | head -100

# 编译时指定头文件搜索路径
dtc -I dts -O dtb -o myboard.dtb -I /path/to/include myboard.dts
调试技巧: 在Linux系统运行时,可以通过 /sys/firmware/devicetree/base 目录直接查看当前内核使用的设备树结构,该目录以文件系统形式呈现了 .dtb 的节点和属性。

2.5 编译流程SVG架构图

下图展示了从 .dts/.dtsi 源文件到 .dtb 二进制文件的完整编译链路,以及反编译和运行时访问的路径:

DTS/DTSI → DTB 编译与反编译流程 .dts 板级主文件 设备树源文件 .dtsi 头文件 SoC/模块公共定义 .h 宏定义头文件 引脚/时钟宏 C预处理器 (cpp) 展开 #include / #define 生成完整 .dts DTC 编译器 语法检查 + 序列化 生成 FDT 格式 .dtb 二进制文件 扁平设备树 Blob /sys/firmware/ 内核运行时访问 dtc -I dtb -O dts 反编译为 .dts 编译命令示例 dtc -I dts -O dtb \ -o board.dtb board.dts

2.6 编译产物与内核加载

编译生成的 .dtb 文件在系统启动过程中由 Bootloader(如 U-Boot)加载到内存,并将起始地址传递给内核。内核在启动早期解析 FDT 格式,构建设备树结构,驱动框架据此完成设备枚举和资源分配:

# U-Boot 中加载设备树的典型命令序列
load mmc 0:1 ${fdt_addr_r} imx6ull-myboard.dtb
load mmc 0:1 ${kernel_addr_r} zImage
bootz ${kernel_addr_r} - ${fdt_addr_r}
多板支持策略: 同一份内核镜像可以搭配不同的 .dtb 文件运行在不同硬件上。内核通过 compatible 属性匹配板级设备树,实现“一次编译,多处运行”的灵活部署。

2.7 常见问题与避坑指南

  • 头文件路径错误: 编译时未指定 -I 参数导致 #include 找不到 .dtsi 文件。解决方案:确保包含路径指向内核源码的 arch/arm/boot/dts/ 目录。
  • 节点标签重复: 多个 .dtsi 中定义了相同名称的标签(label),导致编译冲突。解决方案:使用 &label 引用时确保标签唯一,或通过 /delete-node/ 删除冲突节点。
  • 属性值类型不匹配: 如 reg 属性中地址长度与 #address-cells 不一致。解决方案:严格遵循 #address-cells 和 #size-cells 的定义。
  • 反编译后无法重新编译: 反编译生成的 .dts 可能包含 __symbols__ 等内部节点,直接编译会报错。解决方案:使用 -f 强制编译,或手动清理无关节点。
生产环境注意: 切勿在运行时直接修改 /sys/firmware/devicetree/base 下的文件,该目录是内核内存中设备树的只读映射。修改设备树必须重新编译 .dtb 并更新 Bootloader 加载的文件。