3. Device Tree 基础:语法、节点与属性

Device Tree(设备树)是嵌入式 Linux 中描述硬件拓扑结构的核心机制。它使用一种树形数据结构,将 CPU、内存、外设、中断控制器、时钟和 GPIO 控制器等硬件资源以节点和属性的形式组织起来。DRM/KMS 显示驱动通过解析 Device Tree 来获取显示控制器的寄存器地址、中断号、时钟频率以及面板接口类型等关键参数,从而实现硬件无关的驱动设计。

核心概念: Device Tree 将硬件描述从内核代码中分离出来,使得同一份内核镜像可以适配多种硬件平台。对于显示驱动而言,只需修改 Device Tree 文件即可适配不同的 LCD 面板或 HDMI 输出接口。

3.1 Device Tree 语法基础

Device Tree 源文件(.dts)采用一种类似 C 语言的结构化文本格式,由节点(node)和属性(property)组成。每个节点代表一个硬件设备或总线,属性则描述该设备的配置参数。

3.1.1 基本语法规则

  • 节点定义: node-name@unit-address,其中 unit-address 通常为设备基地址。
  • 属性赋值: property-name = <value>; 或 property-name = "string";。
  • 标签(label): 使用 label: node-name 格式,便于在其他地方引用。
  • 包含文件: #include "filename.dtsi",用于复用通用硬件描述。
// 一个典型的 Device Tree 节点示例
/ {
    model = "Embedded DRM Board v1.0";
    compatible = "vendor,board-name";

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

        display_controller: display-controller@1e000000 {
            compatible = "vendor,display-controller";
            reg = <0x1e000000 0x1000>;
            interrupts = <0 42 4>;
            clocks = <&clk_gate DISPLAY_CLK>;
            status = "okay";
        };
    };
};
语法要点: 每个节点以分号结束,属性值支持多种数据类型:32 位整数(<0x1000>)、字符串("okay")、字节数组([01 02 03])以及 phandle 引用(&clk_gate)。

3.1.2 节点与属性的层级关系

Device Tree 的树形结构从根节点 / 开始,向下逐级展开。每个节点可以有子节点,形成硬件拓扑的层次化描述。显示驱动通常关注的节点路径如下:

Device Tree 节点层级结构(显示子系统) / (根节点) chosen memory soc leds i2c@1c00000 spi@1d00000 display-controller panel@0 hdmi-encoder 说明: 显示控制器节点(display-controller)通常位于 soc 节点下,其子节点描述具体的显示输出接口(如 LVDS 面板、HDMI 编码器)。

3.2 核心属性详解

Device Tree 定义了一系列标准属性,用于描述设备的硬件资源。对于 DRM 显示驱动,以下四个属性最为关键:compatible、reg、interrupts、clocks 和 gpios。

3.2.1 compatible 属性——驱动匹配的钥匙

compatible 属性是一个字符串列表,用于将设备节点与内核驱动进行匹配。内核通过查找驱动中 of_match_table 定义的 compatible 字符串,找到对应的 probe 函数。

属性格式 示例 匹配规则
"vendor,device" "ti,am335x-lcdc" 精确匹配,优先使用
"vendor,device-gen" "fsl,imx6q-ldb" 带版本号的兼容匹配
"generic-driver" "simple-framebuffer" 通用驱动,作为 fallback
// 内核驱动中的匹配表示例
static const struct of_device_id display_of_match[] = {
    { .compatible = "vendor,display-controller-v2", .data = &v2_config },
    { .compatible = "vendor,display-controller",    .data = &v1_config },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, display_of_match);

static int display_probe(struct platform_device *pdev)
{
    const struct of_device_id *match;
    match = of_match_device(display_of_match, &pdev->dev);
    // 根据 match->data 选择硬件配置
}
常见错误: compatible 字符串中的逗号前后不能有空格,且 vendor 前缀必须与内核驱动中的定义完全一致。大小写敏感,"Vendor,Device" 与 "vendor,device" 不匹配。

3.2.2 reg 属性——地址空间描述

reg 属性描述设备的寄存器地址范围,由一组 (address, length) 对组成。其解析依赖于父节点的 #address-cells 和 #size-cells 属性。

// reg 属性示例
display_controller: display-controller@1e000000 {
    compatible = "vendor,display-controller";
    reg = <0x1e000000 0x1000    // 寄存器基地址 0x1e000000,长度 4KB
           0x1e010000 0x100>;   // 第二个地址区域:控制寄存器
};

// 父节点定义地址格式
soc {
    #address-cells = <1>;  // 地址用 1 个 32 位单元表示
    #size-cells = <1>;     // 长度用 1 个 32 位单元表示
};

在驱动中,通过 platform_get_resource() 或 of_iomap() 获取 reg 属性对应的内存资源:

static int display_probe(struct platform_device *pdev)
{
    struct resource *res;
    void __iomem *base;

    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    base = devm_ioremap_resource(&pdev->dev, res);
    if (IS_ERR(base))
        return PTR_ERR(base);

    // 第二个地址区域
    res = platform_get_resource(pdev, IORESOURCE_MEM, 1);
    // ...
}

3.2.3 interrupts 属性——中断资源绑定

interrupts 属性描述设备使用的中断号、触发类型等信息。其格式依赖于中断控制器父节点的 #interrupt-cells 属性。

中断控制器类型 #interrupt-cells interrupts 格式 示例
ARM GIC 3 <中断类型 中断号 触发类型> <0 42 4>
简单中断控制器 1 <中断号> <15>
级联中断控制器 2 <父中断号 子中断号> <3 8>
// 中断属性完整示例
display_controller: display-controller@1e000000 {
    compatible = "vendor,display-controller";
    reg = <0x1e000000 0x1000>;
    interrupts = <0 42 4>;  // GIC SPI 中断 42,高电平触发
    interrupt-parent = <&gic>;  // 指定中断控制器
};

// 驱动中获取中断号
static int display_probe(struct platform_device *pdev)
{
    int irq;
    irq = platform_get_irq(pdev, 0);
    if (irq < 0)
        return irq;

    ret = devm_request_irq(&pdev->dev, irq, display_irq_handler,
                           IRQF_TRIGGER_HIGH, "display", dev);
}

3.2.4 时钟与 GPIO 绑定

显示驱动通常需要多个时钟源(像素时钟、总线时钟)和 GPIO 控制(复位、背光使能、电源控制)。Device Tree 使用 clocks 和 gpios 属性来描述这些资源。

时钟绑定
// 时钟绑定示例
display_controller: display-controller@1e000000 {
    compatible = "vendor,display-controller";
    reg = <0x1e000000 0x1000>;
    clocks = <&clk_gate DISPLAY_CLK>,   // 主时钟
             <&clk_gate PIXEL_CLK>;      // 像素时钟
    clock-names = "core", "pixel";        // 时钟名称,驱动通过名称获取

    assigned-clocks = <&clk_gate PIXEL_CLK>;
    assigned-clock-rates = <148500000>;   // 设置像素时钟频率为 148.5MHz
};

// 驱动中获取时钟
static int display_probe(struct platform_device *pdev)
{
    struct clk *core_clk, *pixel_clk;

    core_clk = devm_clk_get(&pdev->dev, "core");
    pixel_clk = devm_clk_get(&pdev->dev, "pixel");

    clk_prepare_enable(core_clk);
    clk_set_rate(pixel_clk, 148500000);
}
GPIO 绑定
// GPIO 绑定示例
display_controller: display-controller@1e000000 {
    compatible = "vendor,display-controller";
    reg = <0x1e000000 0x1000>;

    enable-gpios = <&gpio1 15 GPIO_ACTIVE_HIGH>;   // 电源使能 GPIO
    reset-gpios = <&gpio2 5 GPIO_ACTIVE_LOW>;       // 复位 GPIO,低电平有效
    backlight-gpios = <&gpio3 10 GPIO_ACTIVE_HIGH>; // 背光控制 GPIO
};

// 驱动中操作 GPIO
static int display_probe(struct platform_device *pdev)
{
    struct gpio_desc *enable_gpio, *reset_gpio;

    enable_gpio = devm_gpiod_get(&pdev->dev, "enable", GPIOD_OUT_HIGH);
    reset_gpio = devm_gpiod_get(&pdev->dev, "reset", GPIOD_OUT_HIGH);

    // 复位时序:拉低 10ms 后释放
    gpiod_set_value(reset_gpio, 0);
    msleep(10);
    gpiod_set_value(reset_gpio, 1);
}
显示驱动 Device Tree 资源绑定流程 display-controller 节点 compatible reg interrupts clocks / gpios of_match_table 驱动匹配 platform_get_resource ioremap platform_get_irq request_irq devm_clk_get / gpiod_get 时钟使能 / GPIO 控制 DRM/KMS 显示驱动初始化成功

3.3 显示驱动中的 Device Tree 实践

在实际的 DRM 显示驱动开发中,Device Tree 的编写需要遵循内核社区定义的绑定规范。以下是一个完整的显示控制器 Device Tree 节点示例,涵盖了本节所学的所有核心属性:

// 完整的显示控制器 Device Tree 节点
&display_controller {
    compatible = "vendor,display-controller-v2", "vendor,display-controller";
    reg = <0x1e000000 0x4000>;
    interrupts = <0 42 4>;
    interrupt-parent = <&gic>;

    // 时钟资源
    clocks = <&clk_gate DISPLAY_CLK>,
             <&clk_gate PIXEL_CLK>,
             <&clk_gate LVDS_CLK>;
    clock-names = "core", "pixel", "lvds";

    // 时钟频率配置
    assigned-clocks = <&clk_gate PIXEL_CLK>,
                      <&clk_gate LVDS_CLK>;
    assigned-clock-rates = <148500000>,
                           <74250000>;

    // GPIO 控制
    enable-gpios = <&gpio1 15 GPIO_ACTIVE_HIGH>;
    reset-gpios = <&gpio2 5 GPIO_ACTIVE_LOW>;
    backlight-gpios = <&gpio3 10 GPIO_ACTIVE_HIGH>;

    // 显示输出接口子节点
    ports {
        #address-cells = <1>;
        #size-cells = <0>;

        port@0 {
            reg = <0>;
            display_out_rgb: endpoint {
                remote-endpoint = <&panel_in>;
                bus-width = <24>;
                data-order = <0x0012>;
            };
        };

        port@1 {
            reg = <1>;
            display_out_lvds: endpoint {
                remote-endpoint = <&lvds_bridge_in>;
            };
        };
    };

    status = "okay";
};
实践要点:
  • compatible 属性建议提供多个兼容字符串,从具体到通用排列,便于驱动向后兼容。
  • reg 属性中的地址范围应精确对应硬件手册中的寄存器区域,避免映射过大内存空间。
  • clock-names 必须与驱动中 devm_clk_get() 使用的名称完全一致。
  • GPIO 的 GPIO_ACTIVE_HIGH/GPIO_ACTIVE_LOW 标志决定了驱动中 gpiod_set_value() 的逻辑电平。

3.4 本章小结

Device Tree 是嵌入式 Linux 显示驱动开发的基石。通过本章的学习,您应该掌握:

知识点 关键内容 DRM 驱动中的应用
Device Tree 语法 节点、属性、标签、包含文件 定义显示控制器及其子节点
compatible 属性 驱动匹配字符串,支持多级兼容 of_match_table 匹配 probe 函数
reg 属性 寄存器地址和长度,支持多段区域 ioremap 映射显示控制器寄存器
interrupts 属性 中断号、类型,依赖 interrupt-parent 注册 VBLANK 和错误中断处理
clocks 属性 时钟源和频率配置 设置像素时钟、总线时钟频率
gpios 属性 GPIO 控制,支持有效电平标志 面板电源、复位、背光控制
进阶提示: 在下一章中,我们将深入探讨如何在内核驱动中解析这些 Device Tree 属性,并利用它们完成 DRM/KMS 显示控制器的初始化。建议读者在阅读下一章之前,先尝试编写一个简单的 Device Tree 节点,并使用 dtc 工具编译验证其语法正确性。