2、设备树基础概念:设备树(DT)、设备树源文件(DTS)、设备树编译(DTC)、设备树二进制(DTB)的定义与作用

设备树(Device Tree)是嵌入式 Linux 系统中用于描述硬件平台资源的数据结构,它解决了传统内核中大量硬编码板级信息(如寄存器地址、中断号、时钟频率、GPIO 复用关系等)导致的代码冗余与维护困难问题。在 ARM、RISC-V、PowerPC 等架构中,设备树已成为内核与硬件之间的标准“硬件描述语言”。本章将围绕设备树(DT)、设备树源文件(DTS)、设备树编译器(DTC)和设备树二进制(DTB)四个核心概念,从定义、作用、编译流程到实际工程应用展开系统讲解。

设备树核心概念与编译链路 DTS 设备树源文件 *.dts / *.dtsi DTC 设备树编译器 dtc 工具 DTB 设备树二进制 *.dtb 编译 生成 Linux 内核 解析 DTB → 生成 platform 设备 Bootloader U-Boot / ATF 硬件平台 CPU / 外设 / 总线 加载 传递 DTS → DTC → DTB → Bootloader → 内核 → 硬件驱动匹配 驱动匹配 / 资源申请

2.1 设备树(DT)的核心原理与设计动机

核心原理:设备树本质上是一种树形数据结构,每个节点(node)描述一个硬件设备或总线接口,节点包含若干属性(property),属性以键值对形式存储硬件参数。例如,一个 UART 控制器的节点会包含寄存器基地址(reg)、中断号(interrupts)、时钟源(clocks)等属性。内核通过解析这棵树,即可获知“板上有什么设备、设备挂在哪里、需要哪些资源”,从而动态创建对应的 platform 设备并匹配驱动。

设计动机:在设备树出现之前,ARM Linux 内核中充斥着大量 arch/arm/mach-xxx 板级文件,每块开发板都需要在 C 代码中硬编码硬件资源。这种方式的弊端非常明显:

  • 代码冗余:同一 SoC 的不同板卡,板级文件大量重复,维护成本极高。
  • 内核与硬件耦合:更换外设或调整内存大小,必须重新编译内核。
  • 社区分裂:各厂商的板级代码风格不一,难以统一维护。

设备树将硬件描述从内核代码中剥离,形成独立的数据文件,使得同一份内核镜像可以支持多种硬件平台,只需在启动时传入不同的 DTB 即可。这一设计思想与 ACPI(高级配置与电源接口)在 x86 平台的角色类似,但设备树更轻量、更灵活,适合嵌入式场景。

关键理解:设备树不是“驱动代码”,而是“硬件拓扑的静态描述”。它告诉内核“有什么”,但不告诉内核“怎么用”——具体驱动逻辑仍由内核中的设备驱动模块完成。

2.2 设备树源文件(DTS)的语法结构与编写规范

定义与作用:DTS(Device Tree Source)是设备树的源文件,采用类似 C 语言的宏定义和头文件机制,以文本形式描述硬件。DTS 文件通常以 .dts 为扩展名,公共的 SoC 级描述放在 .dtsi(Device Tree Source Include)文件中,板级差异部分放在 .dts 文件中,通过 #include 指令组合。

一个典型的 DTS 文件结构如下:

// 例:imx6ull-evk.dts 简化片段
/dts-v1/;

#include "imx6ull.dtsi"   // SoC 级公共描述

/ {
    model = "Freescale i.MX6ULL EVK Board";
    compatible = "fsl,imx6ull-evk", "fsl,imx6ull";

    chosen {
        stdout-path = &uart1;
    };

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

    &uart1 {
        pinctrl-names = "default";
        pinctrl-0 = <&pinctrl_uart1>;
        status = "okay";
    };
};

语法要点:

  • 根节点:用 / 表示,整个设备树的起点。
  • 节点命名:格式为 node-name@unit-address,其中 unit-address 通常是寄存器或总线地址,用于区分同名节点。
  • 属性格式:属性值可以是字符串("string")、32 位整数数组(<1 2 3>)、字节数组([0x00 0x01])或布尔值(空属性表示 true)。
  • 标签(label):用 &label 引用节点,便于在板级文件中追加或修改属性。
  • 宏与头文件:DTS 支持 C 预处理器,可使用 #define、#include,但需在编译时加 -I 指定头文件路径。
属性名 含义 典型值示例
compatible 驱动匹配的关键字符串,格式为“厂商,型号” "fsl,imx6ull-uart"
reg 寄存器地址与长度,可多组 <0x02020000 0x4000>
interrupts 中断号与触发类型 <0 32 4>(GIC SPI 32 高电平)
clocks 时钟引用,通常配合 clock-names <&clks IMX6ULL_CLK_UART1>
status 设备状态,"okay" 使能,"disabled" 禁用 "okay" / "disabled"
pinctrl-0 引脚复用配置引用 <&pinctrl_uart1>

2.3 设备树编译器(DTC)与编译流程

定义与作用:DTC(Device Tree Compiler)是将 DTS 源文件编译为 DTB 二进制文件的工具。它类似于 C 语言的编译器,负责语法检查、宏展开、节点合并和二进制编码。DTC 工具通常随内核源码一起发布,位于 scripts/dtc/ 目录下,也可单独安装(apt install device-tree-compiler)。

编译操作流程:

  1. 预处理:使用 C 预处理器(cpp)展开 #include 和 #define,生成纯 DTS 文本。
  2. 语法解析:DTC 解析 DTS 文本,构建内部树形结构。
  3. 节点合并:将 .dtsi 与 .dts 中相同标签的节点内容合并,板级文件覆盖 SoC 级默认值。
  4. 二进制编码:将树形结构转换为扁平二进制格式(FDT,Flattened Device Tree),生成 DTB 文件。
# 手动编译示例
cpp -nostdinc -I include -undef -D__DTS__ -x assembler-with-cpp \
    board.dts | dtc -I dts -O dtb -o board.dtb

# 内核编译时自动处理
make dtbs  # 在 arch/arm/boot/dts/ 目录下生成所有 .dtb

反编译与调试:DTC 也支持将 DTB 反编译为 DTS,便于排查问题:

dtc -I dtb -O dts -o decompiled.dts board.dtb
经验提醒:在开发阶段,建议在 DTS 中开启 #address-cells 和 #size-cells 的显式声明,避免因默认值不同导致地址解析错误。同时,编译时加上 -Werror 选项,将警告视为错误,提前暴露问题。

2.4 设备树二进制(DTB)的加载与内核解析

定义与作用:DTB(Device Tree Blob)是编译后的二进制文件,是内核实际消费的数据格式。Bootloader(如 U-Boot)在启动内核前,将 DTB 加载到内存指定地址,并通过寄存器(如 r2,ARM 架构)将 DTB 地址传递给内核。内核启动早期,在 setup_arch() 阶段解析 DTB,构建 platform_device 列表,随后与驱动进行匹配。

DTB 在启动流程中的位置:

阶段 动作 说明
Bootloader 加载 DTB 到内存 通常位于 0x83000000 附近(ARM32)
内核早期 解析 DTB 头部 校验 magic number(0xd00dfeed)
内存初始化 读取 memory 节点 确定物理内存范围
设备创建 遍历节点生成设备 为每个 compatible 匹配的节点创建 platform_device
驱动绑定 of_match_table 匹配 驱动通过 of_match_table 与节点 compatible 匹配

2.5 实战场景:从 DTS 修改到 DTB 生效的完整闭环

场景背景:某工业控制板基于 NXP i.MX6ULL 平台,原设计使用 UART2 作为调试串口,但硬件改版后调试串口改为 UART3,且 UART3 的引脚从 GPIO1_IO20/21 改为 GPIO1_IO28/29。需要修改设备树并验证。

初始现象:内核启动后,UART3 无输出,dmesg 中无 ttymxc2 设备注册信息。

分析判断过程:

  1. 确认硬件原理图:UART3_TXD 连接 GPIO1_IO28,UART3_RXD 连接 GPIO1_IO29。
  2. 查看原 DTS 中 UART3 节点,发现 pinctrl-0 引用的是 pinctrl_uart3,但该引脚组定义在 imx6ull.dtsi 中仍为旧引脚。
  3. 使用 dtc -I dtb -O dts 反编译当前 DTB,确认实际生效的引脚配置。

实际操作步骤:

// 1. 在板级 DTS 中覆盖 pinctrl 定义
&pinctrl_uart3 {
    fsl,pins = <
        MX6UL_PAD_GPIO1_IO28__UART3_DCE_TX  0x1b0b1
        MX6UL_PAD_GPIO1_IO29__UART3_DCE_RX  0x1b0b1
    >;
};

// 2. 确认 UART3 节点状态
&uart3 {
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_uart3>;
    status = "okay";
};

// 3. 重新编译 DTB
make imx6ull_myboard.dtb

// 4. 将新 DTB 拷贝到 TFTP 目录或 SD 卡 boot 分区
cp arch/arm/boot/dts/imx6ull_myboard.dtb /tftpboot/

// 5. 在 U-Boot 中加载并启动
tftp 0x83000000 imx6ull_myboard.dtb
bootz 0x80800000 - 0x83000000

结果对比:修改后内核启动日志中出现 ttymxc2 设备,U-Boot 控制台输出正常,stty -F /dev/ttymxc2 115200 可正常通信。

风险与代价:引脚复用配置错误可能导致 GPIO 冲突或外设异常;修改 DTS 后必须同步更新 Bootloader 中的 DTB,否则内核仍使用旧配置。建议在版本管理中同时维护 DTS 源文件与编译产物 DTB,并记录硬件改版说明。

常见误区:不要直接修改 .dtsi 文件来适配板级差异,应通过板级 .dts 中的 &label 覆盖。否则会影响同 SoC 其他板卡的编译。另外,status = "disabled" 的节点不会创建设备,排查外设问题时先检查该属性。

2.6 总结复盘与知识结构

设备树的四个核心概念构成了完整的硬件描述链路:DTS 是源文件(人类可读)→ DTC 是编译器(工具)→ DTB 是二进制产物(机器可读)→ 内核解析 DTB 生成设备。理解这一链路是后续编写设备树驱动的基础。在实际工程中,建议遵循以下原则:

  • 分离 SoC 与板级:SoC 级公共描述放在 .dtsi,板级差异放在 .dts。
  • 使用标签覆盖:尽量使用 &label 方式修改节点,避免整体重写。
  • 编译验证前置:每次修改 DTS 后立即编译,使用 dtc -Werror 检查语法。
  • 反编译辅助排障:内核实际使用的 DTB 可能与源码不一致,反编译确认是有效手段。