3. 设备树源文件(.dts)与编译:dts文件结构、dtsi包含文件、dtc编译器使用、生成dtb文件

设备树源文件(.dts)是描述硬件拓扑结构的文本文件,经过设备树编译器(dtc)编译后生成二进制设备树 Blob(.dtb),供内核在启动时解析。本章深入剖析 .dts 文件结构、.dtsi 包含机制、dtc 编译器的使用以及生成 .dtb 的完整流程。

3.1 dts 文件结构

一个标准的 .dts 文件由根节点、若干子节点以及属性组成。其基本语法结构如下:

/dts-v1/;

/ {
    model = "MyBoard RevA";
    compatible = "vendor,myboard-reva", "vendor,myboard";

    #address-cells = <1>;
    #size-cells = <1>;

    memory@80000000 {
        device_type = "memory";
        reg = <0x80000000 0x10000000>;
    };

    chosen {
        bootargs = "console=ttyS0,115200 root=/dev/mmcblk0p2";
        stdout-path = &uart0;
    };

    soc {
        #address-cells = <1>;
        #size-cells = <1>;
        ranges;

        uart0: serial@10000000 {
            compatible = "vendor,uart-ip";
            reg = <0x10000000 0x1000>;
            interrupts = <0 5 4>;
            clock-frequency = <24000000>;
            status = "okay";
        };

        gpio0: gpio@10001000 {
            compatible = "vendor,gpio-ip";
            reg = <0x10001000 0x1000>;
            interrupts = <0 6 4>;
            gpio-controller;
            #gpio-cells = <2>;
            status = "disabled";
        };
    };
};
关键语法要点:
  • /dts-v1/; 必须位于文件头部,声明使用设备树 v1 格式。
  • 根节点用 / 表示,所有硬件设备都是根节点的子节点。
  • 节点名称格式:node-name@unit-address,其中 unit-address 通常为设备基地址。
  • 属性值类型:字符串(双引号)、32位整数(尖括号)、字节序列(方括号)、字符串列表(逗号分隔)。
  • #address-cells 和 #size-cells 定义子节点 reg 属性中地址和长度的字数。

3.2 dtsi 包含文件

.dtsi 文件是设备树的包含文件,类似于 C 语言中的头文件。它用于存放 SoC 公共部分、IP 核定义、通用外设配置等,被多个 .dts 文件引用,实现代码复用。

文件类型 用途 示例
.dtsi SoC 级公共定义,包含 CPU 核、中断控制器、总线、通用外设 IP soc.dtsi, imx6ull.dtsi
.dts 板级具体描述,包含内存大小、板载外设、引脚复用、设备状态 myboard.dts

包含语法使用 #include 指令,与 C 预处理器兼容:

// myboard.dts
/dts-v1/;
#include "soc.dtsi"
#include "myboard-pinfunc.h"

/ {
    model = "MyBoard RevB";
    compatible = "vendor,myboard-revb";

    memory@80000000 {
        reg = <0x80000000 0x20000000>;  // 512MB
    };

    &uart0 {
        status = "okay";
        pinctrl-0 = <&pinctrl_uart0>;
    };

    &gpio0 {
        status = "okay";
    };
};
引用节点语法: 使用 &label 语法可以引用 .dtsi 中定义的节点标签,在板级文件中覆盖或追加属性,无需重写整个节点。

下图展示了 .dts 与 .dtsi 之间的包含关系与编译流程:

dts与dtsi包含关系及编译流程 dts 与 dtsi 包含关系及编译流程 soc.dtsi SoC 公共定义 myboard.dts 板级具体描述 dtc 编译器 设备树编译器 myboard.dtb 二进制设备树 Blob 包含机制说明 • #include "soc.dtsi" • &uart0 { status = "okay"; } • 引用标签覆盖属性

3.3 dtc 编译器使用

设备树编译器(dtc)是 Linux 内核源码中提供的工具,用于将 .dts 源文件编译为 .dtb 二进制文件,也支持反编译操作。dtc 源码位于内核源码树的 scripts/dtc/ 目录下。

3.3.1 基本编译命令

# 编译 dts 为 dtb
dtc -I dts -O dtb -o myboard.dtb myboard.dts

# 反编译 dtb 为 dts
dtc -I dtb -O dts -o myboard.dts myboard.dtb

# 编译并输出依赖关系
dtc -I dts -O dtb -o myboard.dtb myboard.dts -d myboard.d

# 编译时指定包含路径
dtc -I dts -O dtb -o myboard.dtb myboard.dts -i ./include
选项 说明 常用值
-I 输入格式 dts(源文件)、dtb(二进制)、fs(文件系统)
-O 输出格式 dtb(二进制)、dts(源文件)、yaml(YAML)
-o 输出文件名 myboard.dtb
-i 添加包含搜索路径 -i ./include
-d 生成依赖文件(Makefile 友好) -d myboard.d
-@ 生成插件节点(overlay 支持) 无参数
-W 启用特定警告 -W no-unit_address_vs_reg
常见编译错误与解决方法:
  • “FATAL ERROR: Unable to parse input tree”:检查 .dts 文件语法,确认分号、花括号、引号是否匹配。
  • “WARNING: unit_address_vs_reg”:节点名中的 unit-address 与 reg 属性首地址不匹配,需对齐。
  • “Error: soc.dtsi: No such file or directory”:使用 -i 指定包含路径,或将 .dtsi 放在当前目录。

3.3.2 编译流程详解

dtc 编译 .dts 到 .dtb 的内部流程分为四个阶段:

  1. 预处理阶段: 处理 #include、#define 等 C 预处理器指令,展开宏定义,合并所有包含的 .dtsi 文件。
  2. 解析阶段: 将预处理后的文本解析为抽象语法树(AST),检查节点、属性、标签的语法正确性。
  3. 语义检查阶段: 验证 #address-cells/#size-cells 一致性、中断映射有效性、属性值类型正确性。
  4. 二进制生成阶段: 将 AST 序列化为扁平化设备树(FDT)二进制格式,生成 .dtb 文件。
dtc 编译内部流程 dtc 编译内部流程(四阶段) 预处理阶段 #include / #define 展开 解析阶段 生成抽象语法树 语义检查阶段 地址/中断一致性验证 二进制生成阶段 序列化为 FDT 格式 输出产物 myboard.dtb — 扁平化设备树二进制文件,包含所有节点、属性、标签、字符串表、内存保留映射

3.4 生成 dtb 文件

生成 .dtb 文件是设备树裁剪的最终输出目标。在实际嵌入式开发中,通常通过内核构建系统(Kbuild)自动完成编译,也可以手动调用 dtc 进行编译。

3.4.1 使用内核 Kbuild 编译

在内核源码树中,将 .dts 文件放入 arch/<arch>/boot/dts/ 目录,然后在 Makefile 中添加目标:

# arch/arm/boot/dts/Makefile
dtb-$(CONFIG_SOC_MYBOARD) += myboard.dtb

执行内核编译命令:

make ARCH=arm myboard.dtb

Kbuild 会自动处理依赖关系、包含路径、预处理器调用,并调用 dtc 生成 .dtb 文件。

3.4.2 手动编译与验证

手动编译适用于快速原型验证或调试场景:

# 编译
dtc -I dts -O dtb -o myboard.dtb myboard.dts -i ./dtsi

# 验证 dtb 文件内容
dtc -I dtb -O dts myboard.dtb | head -50

# 检查 dtb 文件大小
ls -lh myboard.dtb

# 使用 fdtdump 查看原始结构
fdtdump myboard.dtb
生产环境建议:
  • 始终通过内核 Kbuild 系统编译 .dtb,确保依赖关系和版本一致性。
  • 在 .dts 文件中使用 #include 而非 /include/,以兼容 C 预处理器。
  • 编译时开启 -W 警告选项,提前发现节点地址不匹配、属性缺失等问题。
  • 将生成的 .dtb 与 bootloader(如 U-Boot)使用的设备树分开管理,避免版本冲突。

3.4.3 dtb 文件结构

编译生成的 .dtb 文件采用扁平化设备树(FDT)格式,其内部结构包含以下主要部分:

段名称 说明 大小(典型值)
FDT 头部 魔数、总大小、各段偏移、版本号、引导 CPU ID 40 字节
内存保留映射 保留内存区域的物理地址和大小列表 16 字节 × 保留区数量
结构块 节点和属性的树形结构,使用 token 标记(BEGIN_NODE, PROP, END_NODE 等) 数百到数千字节
字符串块 所有属性名的字符串表,通过偏移量引用 数十到数百字节
dtb 文件内部结构 dtb 文件内部结构(FDT 格式) FDT 头部(40 字节) 魔数 0xD00DFEED | 总大小 | 各段偏移 | 版本号 | 引导 CPU ID 内存保留映射(16 字节 × N) 物理地址 | 大小 | 物理地址 | 大小 ... 结构块(节点与属性树) BEGIN_NODE | PROP | PROP | END_NODE | BEGIN_NODE ... 字符串块(属性名字符串表) "compatible\0" "model\0" "reg\0" "status\0" ...

3.5 实战案例:从零编译一个最小设备树

以下是一个完整的实战流程,从编写 .dts 文件到生成 .dtb 并验证:

# 1. 创建最小 dts 文件
cat > minimal.dts << EOF
/dts-v1/;

/ {
    model = "MinimalBoard";
    compatible = "vendor,minimal";

    #address-cells = <1>;
    #size-cells = <1>;

    memory@80000000 {
        device_type = "memory";
        reg = <0x80000000 0x10000000>;
    };

    chosen {
        bootargs = "console=ttyS0,115200";
    };
};
EOF

# 2. 编译为 dtb
dtc -I dts -O dtb -o minimal.dtb minimal.dts

# 3. 反编译验证
dtc -I dtb -O dts minimal.dtb

# 4. 查看 dtb 文件信息
fdtdump minimal.dtb | head -20
输出示例:
// 反编译输出
/dts-v1/;

/ {
    model = "MinimalBoard";
    compatible = "vendor,minimal";
    #address-cells = <0x00000001>;
    #size-cells = <0x00000001>;

    memory@80000000 {
        device_type = "memory";
        reg = <0x80000000 0x10000000>;
    };

    chosen {
        bootargs = "console=ttyS0,115200";
    };
};

通过本章的学习,读者应掌握 .dts 文件的结构语法、.dtsi 包含机制、dtc 编译器的使用以及 .dtb 文件的生成与验证方法。这些技能是后续进行设备树裁剪与内核移植的基础。