2. 设备树基础:起源、语法规则与编译方法

设备树(Device Tree)是嵌入式 Linux 系统中用于描述硬件拓扑结构的数据结构,它替代了早期 ARM Linux 内核中大量硬编码的 board-*.c 文件,实现了内核与硬件描述的分离。本章将系统讲解设备树的起源、语法规则、文件格式及编译方法,为后续 DRM 显示驱动的设备树配置打下坚实基础。

2.1 设备树的起源与设计动机

在 Linux 内核早期(2.6 及之前版本),ARM 架构的板级支持包(BSP)通常在内核源码中直接包含 arch/arm/mach-* 目录下的 board-*.c 文件,这些文件通过静态结构体描述硬件资源(如中断号、内存地址、时钟频率等)。这种方式的弊端随着 ARM SoC 的多样化而愈发明显:

  • 代码冗余:每增加一款开发板,就需要新增或修改大量 C 代码。
  • 耦合度高:硬件描述与内核驱动代码紧密绑定,升级内核时需同步维护板级文件。
  • 二进制 Blob 依赖:部分厂商通过 ATAG(ARM Tagged List)传递参数,缺乏标准化。

设备树(Device Tree)最初源于 Open Firmware(IEEE 1275)规范,PowerPC 架构率先采用。2011 年,ARM Linux 社区决定全面引入设备树,从 Linux 3.x 开始逐步淘汰 board-*.c。设备树的核心思想是:将硬件描述从内核源码中剥离,以树形结构文本文件(.dts)的形式独立存在,由 bootloader(如 U-Boot)传递给内核解析。

关键优势:同一份内核镜像可以支持多种硬件平台,只需更换设备树文件即可。DRM 显示驱动中,管脚复用、显示接口类型(HDMI/DSI/LVDS)、背光控制等硬件参数均通过设备树节点描述。

设备树在 ARM Linux 中的演进时间线 board-*.c 时代 Linux 2.6 ~ 3.0 硬件描述硬编码 引入设备树 Linux 3.2 ~ 3.10 ARM 社区全面采用 成熟期 Linux 4.x ~ 5.x Overlay / 动态配置 现代 Linux 6.x+ ACPI / DT 共存 设备树使 ARM Linux 从“一板一内核”走向“一内核多板”,DRM 驱动通过 DT 节点获取显示控制器参数

2.2 设备树语法规则

设备树源文件(.dts)采用树形结构,由节点(node)、属性(property)和标签(label)组成。每个节点代表一个硬件设备或总线,属性描述设备的配置参数。

2.2.1 节点(Node)

节点是设备树的基本单元,格式为:

[label:] node-name[@unit-address] {
    [property-definitions]
    [child-nodes]
};
  • node-name:节点名称,通常为通用设备类型(如 i2c、spi、display)。
  • @unit-address:可选,表示设备在总线上的地址(如寄存器基址、I2C 从机地址)。
  • label:可选,为节点定义唯一标识符,便于其他节点引用(如 &dsi0)。

DRM 驱动常见节点示例:显示控制器节点通常命名为 display-controller@xxxx,DSI 接口节点命名为 dsi@xxxx,HDMI 发送器节点命名为 hdmi@xxxx。

2.2.2 属性(Property)

属性是键值对,用于描述节点的硬件特性。常用属性类型如下表:

属性类型 语法示例 说明 DRM 相关场景
字符串 compatible = "rockchip,rk3399-dsi"; 设备兼容性标识,驱动匹配的关键 DRM 桥接芯片、显示控制器匹配
32 位整数 reg = <0xff960000 0x1000>; 寄存器基址和长度 显示控制器寄存器映射
整数数组 interrupts = <0 45 4>; 中断号、触发类型 显示控制器中断配置
布尔值 status = "okay"; 或 "disabled" 设备启用状态 控制显示接口是否使能
phandle 引用 panel = &lcd_panel; 引用其他节点 连接显示面板节点
GPIO 描述 reset-gpios = <&gpio3 RK_PB5 GPIO_ACTIVE_LOW>; GPIO 引脚描述 显示复位、背光控制

2.2.3 标签(Label)

标签为节点提供别名,便于在设备树包含文件(.dtsi)或覆盖文件(.dtso)中引用。标签定义在节点名称前,使用 &label 语法引用:

// 定义标签
dsi0: dsi@ff960000 {
    compatible = "rockchip,rk3399-dsi";
    reg = <0xff960000 0x1000>;
    ...
};

// 在其他文件中引用标签并追加属性
&dsi0 {
    status = "okay";
    panel = <&lcd_panel>;
};

注意:标签引用必须在编译时解析,不能循环引用。DRM 驱动中常用标签连接显示控制器、桥接芯片和面板节点,形成完整的显示链路。

设备树节点、属性与标签结构示例 / (根节点) soc@0 dsi0: dsi@ff960000 标签: dsi0 hdmi0: hdmi@ff940000 标签: hdmi0 dsi0 节点属性示例 compatible, reg, interrupts, clocks, status hdmi0 节点属性示例 compatible, reg, edid, ddc-i2c-bus, hpd-gpios

2.3 dts / dtsi / dtb 文件格式

设备树涉及三种主要文件格式,它们之间的关系如下表:

文件扩展名 全称 格式 用途 是否可编译
.dts Device Tree Source 文本(ASCII) 顶层设备树源文件,描述具体板级硬件 是,编译为 .dtb
.dtsi Device Tree Source Include 文本(ASCII) 设备树包含文件,描述 SoC 公共部分或 IP 核 不单独编译,被 .dts 包含
.dtb Device Tree Blob 二进制(FDT 格式) 编译后的设备树,由 bootloader 加载并传递给内核 由 .dts 编译生成
.dtbo Device Tree Blob Overlay 二进制 设备树覆盖文件,用于运行时动态修改设备树 由 .dtso 编译生成

DRM 驱动开发中的典型文件组织:SoC 厂商提供 rk3399.dtsi 定义显示控制器、DSI、HDMI 等 IP 核的默认配置;板级厂商在 my-board.dts 中 #include "rk3399.dtsi",然后通过 &dsi0 标签追加具体面板参数、GPIO 引脚和时钟频率。

2.4 设备树编译方法

设备树源文件需要通过 Device Tree Compiler(dtc)编译为二进制 .dtb 文件。dtc 工具包含在 Linux 内核源码的 scripts/dtc/ 目录中,也可通过系统包管理器安装。

2.4.1 安装 dtc

# Ubuntu / Debian
sudo apt-get install device-tree-compiler

# Fedora / RHEL
sudo dnf install dtc

# 从内核源码编译
cd linux/scripts/dtc
make
sudo make install

2.4.2 基本编译命令

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

# 反编译 .dtb 为 .dts(调试用)
dtc -I dtb -O dts -o my-board-decompiled.dts my-board.dtb

# 编译时输出依赖信息
dtc -I dts -O dtb -o my-board.dtb my-board.dts -d my-board.d

# 检查 .dtb 语法(不输出文件)
dtc -I dts -O dtb my-board.dts -o /dev/null 2>&1

2.4.3 内核编译系统中的设备树编译

在内核源码树中,设备树编译集成在 Kbuild 系统中。通常通过以下方式编译:

# 编译指定板级的设备树
make ARCH=arm64 my-board.dtb

# 编译所有设备树
make ARCH=arm64 dtbs

# 编译内核时同时编译设备树
make ARCH=arm64 defconfig
make ARCH=arm64 -j4 Image dtbs

DRM 调试技巧:在开发阶段,可以使用 dtc -I dtb -O dts 反编译内核使用的 .dtb 文件,检查显示控制器节点是否被正确使能(status = "okay"),以及管脚复用(pinctrl)配置是否符合预期。

设备树编译流程 my-board.dts 顶层源文件 rk3399.dtsi SoC 包含文件 dtc Device Tree Compiler my-board.dtb 二进制 Blob 由 U-Boot 加载 传递给 Linux 内核解析

2.5 设备树在 DRM 显示驱动中的典型应用

在 DRM 框架中,设备树节点用于描述显示链路中的各个硬件组件。以下是一个简化的 DRM 显示链路设备树示例:

// 显示控制器节点
display-controller@ff8f0000 {
    compatible = "rockchip,rk3399-vop";
    reg = <0xff8f0000 0x400>;
    interrupts = <GIC_SPI 119 IRQ_TYPE_LEVEL_HIGH>;
    clocks = <&cru ACLK_VOP0>, <&cru HCLK_VOP0>;
    clock-names = "aclk", "hclk";
    status = "disabled";

    // 显示输出端口
    port {
        #address-cells = <1>;
        #size-cells = <0>;
        vop0_out_dsi: endpoint@0 {
            reg = <0>;
            remote-endpoint = <&dsi0_in_vop0>;
        };
    };
};

// DSI 控制器节点
dsi0: dsi@ff960000 {
    compatible = "rockchip,rk3399-dsi";
    reg = <0xff960000 0x1000>;
    clocks = <&cru SCLK_DSI0>, <&cru PCLK_DSI0>;
    clock-names = "ref", "pclk";
    status = "disabled";

    ports {
        #address-cells = <1>;
        #size-cells = <0>;
        port@0 {
            reg = <0>;
            dsi0_in_vop0: endpoint {
                remote-endpoint = <&vop0_out_dsi>;
            };
        };
        port@1 {
            reg = <1>;
            dsi0_out_panel: endpoint {
                remote-endpoint = <&panel_in_dsi>;
            };
        };
    };
};

// 显示面板节点
panel: panel@0 {
    compatible = "boe,tv080wum-nl0";
    reg = <0>;
    enable-gpios = <&gpio1 RK_PB3 GPIO_ACTIVE_HIGH>;
    backlight = <&backlight>;
    status = "disabled";

    port {
        panel_in_dsi: endpoint {
            remote-endpoint = <&dsi0_out_panel>;
        };
    };
};

常见错误:remote-endpoint 引用的 phandle 必须存在且类型匹配;status 属性必须显式设置为 "okay" 才能启用设备;时钟名称(clock-names)必须与驱动中 devm_clk_get() 使用的名称一致。

2.6 本章小结

设备树是嵌入式 Linux 显示驱动开发的基石。本章涵盖了以下核心知识点:

  • 起源:从 board-*.c 到设备树的演进,解决了硬件描述与内核代码耦合的问题。
  • 语法规则:节点(node)、属性(property)、标签(label)的定义与使用,以及 compatible、reg、interrupts、status 等关键属性。
  • 文件格式:.dts(源文件)、.dtsi(包含文件)、.dtb(二进制 Blob)的区别与协作关系。
  • 编译方法:使用 dtc 工具编译/反编译设备树,以及内核 Kbuild 系统中的集成方式。
  • DRM 应用:通过显示控制器、DSI 控制器、面板节点的 port 和 endpoint 连接,构建完整的显示链路描述。