3、设备树文件结构:.dts、.dtsi、.dtb、.dtbo文件的区别与编译流程

设备树源文件(DTS)是嵌入式 Linux 驱动开发中描述硬件拓扑的核心载体。理解不同后缀文件(.dts、.dtsi、.dtb、.dtbo)的定位、差异以及从源文件到二进制镜像的编译流程,是驱动工程师必须掌握的基础技能。本节将系统梳理这些文件类型及其转换关系。

3.1 四种文件类型概述

设备树体系包含四种主要文件格式,分别对应开发阶段的不同角色:

文件后缀 全称 角色定位 是否可读 典型用途
.dts Device Tree Source 顶层源文件 是(文本) 描述单板级硬件,包含根节点和板级外设
.dtsi Device Tree Source Include 公共头文件 是(文本) 存放 SoC 公共定义、IP 核模板、复用节点
.dtb Device Tree Blob 二进制镜像 否(二进制) Bootloader 加载并传递给内核的硬件描述
.dtbo Device Tree Blob Overlay 叠加层二进制 否(二进制) 运行时动态修改或扩展设备树(如 FPGA 加载)
关键区分: .dts 是单板级完整描述,.dtsi 是 SoC 级公共描述,二者通过 #include 机制组合;.dtb 是编译后的最终产物,.dtbo 是用于运行时热插拔的增量补丁。

3.2 文件结构对比与包含关系

一个典型的设备树源文件体系呈现“SoC 公共层 → 单板层”的层次结构。.dtsi 文件定义 SoC 内部 IP 核、时钟、中断控制器等通用节点,.dts 文件通过 #include 引入 .dtsi,然后补充或覆盖板级特有的外设(如 GPIO 按键、LED、特定 I2C 设备)。

示例: 以下代码展示 .dtsi 与 .dts 的典型组合关系。
// imx6ull.dtsi(SoC 公共定义)
/ {
    #address-cells = <1>;
    #size-cells = <1>;
    
    soc {
        #address-cells = <1>;
        #size-cells = <1>;
        ranges;
        
        uart1: serial@02020000 {
            compatible = "fsl,imx6ul-uart", "fsl,imx-uart";
            reg = <0x02020000 0x4000>;
            interrupts = <0 26 4>;
            status = "disabled";
        };
        
        gpio1: gpio@0209c000 {
            compatible = "fsl,imx6ul-gpio", "fsl,imx-gpio";
            reg = <0x0209c000 0x4000>;
            interrupts = <0 66 4>;
            gpio-controller;
            #gpio-cells = <2>;
        };
    };
};

// myboard.dts(单板级文件)
#include "imx6ull.dtsi"

/ {
    model = "MyBoard i.MX6ULL";
    compatible = "myboard,imx6ull", "fsl,imx6ull";
    
    chosen {
        stdout-path = &uart1;
    };
    
    leds {
        compatible = "gpio-leds";
        led0: led-0 {
            label = "heartbeat";
            gpios = <&gpio1 3 GPIO_ACTIVE_LOW>;
            linux,default-trigger = "heartbeat";
        };
    };
};

&uart1 {
    status = "okay";
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_uart1>;
};
注意: .dtsi 中的节点通常将 status = "disabled",由 .dts 通过 &uart1 引用语法将其改为 "okay",实现“SoC 默认关闭,单板按需开启”的设计模式。

3.3 编译流程:从 .dts 到 .dtb

设备树编译工具链的核心是 DTC(Device Tree Compiler)。其工作流程分为预处理、编译、二进制生成三个阶段:

  1. 预处理阶段: C 预处理器(cpp)处理 #include、#define、#ifdef 等宏指令,将 .dts 和所有引用的 .dtsi 合并为一个完整的临时源文件。
  2. 编译阶段: DTC 解析合并后的源文件,检查语法、节点标签、属性值合法性,生成中间表示。
  3. 二进制生成阶段: DTC 将中间表示序列化为扁平化设备树(FDT)二进制格式,输出 .dtb 文件。
编译命令示例:
# 完整编译(包含预处理)
dtc -I dts -O dtb -o myboard.dtb myboard.dts

# 手动执行预处理后再编译
cpp -nostdinc -I include -undef -x assembler-with-cpp myboard.dts myboard_preprocessed.dts
dtc -I dts -O dtb -o myboard.dtb myboard_preprocessed.dts

# 反编译 .dtb 为可读的 .dts(调试用)
dtc -I dtb -O dts -o myboard_decompiled.dts myboard.dtb

以下 SVG 图展示了从源文件到二进制镜像的完整编译流水线:

设备树编译流程 设备树编译流程:.dts → .dtb myboard.dts imx6ull.dtsi 预处理 (cpp) #include 展开 宏定义替换 合并临时 .dts (完整树结构) DTC 编译 语法检查 → 节点排序 → 扁平化 FDT 二进制序列化 myboard.dtb 关键工具 cpp:C 预处理器 dtc:设备树编译器

3.4 .dtbo 叠加层与动态编译

.dtbo(Device Tree Blob Overlay)是设备树叠加层技术,允许在系统运行时动态添加或修改设备节点,无需重新编译整个 .dtb。典型应用场景包括 FPGA 动态加载、扩展板热插拔、外设模块使能。

叠加层源文件使用 .dts 格式编写,但必须包含 /plugin/; 声明和 &node 引用语法:

// fpga_accelerator.dts(叠加层源文件)
/dts-v1/;
/plugin/;

/ {
    fragment@0 {
        target = &amba;
        __overlay__ {
            #address-cells = <1>;
            #size-cells = <1>;
            
            fpga_acc: acc@43c00000 {
                compatible = "xlnx,axi-acc-1.0";
                reg = <0x43c00000 0x10000>;
                interrupts = <0 55 4>;
            };
        };
    };
};

编译叠加层时需添加 -@ 选项以生成符号表:

# 编译叠加层 .dtbo
dtc -@ -I dts -O dtb -o fpga_accelerator.dtbo fpga_accelerator.dts

# 运行时加载叠加层(通过 configfs)
mkdir /sys/kernel/config/device-tree/overlays/fpga
cat fpga_accelerator.dtbo > /sys/kernel/config/device-tree/overlays/fpga/dtbo
叠加层 vs 完整 .dtb: 完整 .dtb 描述整个系统的硬件拓扑,在启动时一次性加载;叠加层 .dtbo 只描述增量变化,通过内核的 overlay 机制在运行时合并到基础设备树中,实现“热插拔”硬件描述。

3.5 文件类型对比总结

下表从多个维度对比四种文件类型,帮助快速决策在开发中应使用哪种格式:

对比维度 .dts .dtsi .dtb .dtbo
文件格式 文本 文本 二进制 二进制
是否包含根节点 是 否(仅片段) 是 否(仅片段)
编译输入/输出 输入 输入(被包含) 输出 输出
加载时机 — — Bootloader → 内核启动 内核运行时
修改影响范围 全板 所有引用该 .dtsi 的单板 全系统 仅叠加节点
调试难度 低(文本可读) 低 中(需反编译) 中(需反编译)

3.6 常见编译错误与排查

设备树编译过程中常见的错误类型及解决方法:

  • 语法错误: 缺少分号、花括号不匹配、属性值格式错误。DTC 会提示行号,使用 dtc -I dts -O dtb -o output.dtb input.dts 2>&1 查看详细错误。
  • 标签未定义: 引用了 &node_label 但 .dtsi 中未定义该标签。检查 #include 路径是否正确,或标签名是否拼写错误。
  • 地址/大小单元不匹配: #address-cells 和 #size-cells 设置与 reg 属性长度不一致。确保父节点与子节点的 cell 数量对齐。
  • 叠加层符号缺失: 编译 .dtbo 时未加 -@ 选项,导致运行时无法解析叠加目标。务必使用 dtc -@ 编译叠加层。
避坑指南: 在大型项目中,.dtsi 的嵌套包含容易导致节点重复定义。建议使用 #ifndef 保护或保持 SoC 厂商提供的 .dtsi 文件不被修改,所有板级定制放在 .dts 中通过 &node 引用覆盖。

以下 SVG 图展示四种文件类型在开发与运行阶段的关系:

设备树文件类型关系图 设备树文件类型关系与生命周期 开发阶段(文本源文件) .dtsi SoC 公共定义 .dts 单板级描述 #include dtc 编译 运行阶段(二进制镜像) .dtb 基础设备树 .dtbo 叠加层 Bootloader 加载 运行时加载 内核设备树合并 基础树 + 叠加层 → 完整硬件视图 开发输出产物 .dts → .dtb(完整板级) .dts(plugin)→ .dtbo(叠加)

3.7 内核中的设备树编译集成

在实际的 Linux 内核构建系统中,设备树编译被集成到 Kbuild 框架中。开发者只需在 arch/<arch>/boot/dts/Makefile 中声明目标 .dtb 文件,内核构建系统会自动处理依赖关系和编译流程:

# arch/arm/boot/dts/Makefile 示例片段
dtb-$(CONFIG_SOC_IMX6ULL) += \
    imx6ull-14x14-evk.dtb \
    imx6ull-myboard.dtb

# 编译命令(内核顶层目录执行)
make ARCH=arm dtbs

执行 make dtbs 时,Kbuild 会:

  • 扫描 Makefile 中所有 dtb-y 目标
  • 自动查找对应的 .dts 源文件
  • 解析 #include 依赖,确保 .dtsi 文件路径正确
  • 调用 DTC 编译生成 .dtb 文件
  • 支持 DTC_FLAGS 传递额外参数(如 -@ 用于叠加层)
最佳实践: 在内核源码树中,.dts 和 .dtsi 文件统一存放在 arch/<arch>/boot/dts/ 目录下。不要手动调用 dtc 编译,而是通过内核的 Kbuild 系统管理,以确保依赖关系和编译选项的一致性。