2. 设备树基础:语法、节点与属性详解

设备树(Device Tree,DT)是嵌入式 Linux 系统中描述硬件拓扑结构的核心数据格式。DRM 显示驱动通过设备树获取显示控制器、接口、面板等硬件的地址、中断、时钟、复位等资源信息。本节从语法、节点、属性、匹配机制和关键属性五个维度展开,为后续显示链路调试打下基础。

学习目标:掌握设备树源文件(.dts)的语法规则;理解节点与属性的层级关系;熟悉 compatible 匹配机制;能够正确解析 reg 和 interrupts 属性在显示驱动中的应用。

2.1 设备树语法基础

设备树源文件(.dts)采用树形结构描述硬件,每个硬件设备对应一个节点,节点包含若干属性。语法规则如下:

  • 节点定义:node-name@unit-address,其中 unit-address 通常为寄存器基地址。
  • 属性格式:property-name = <value>; 或 property-name = "string";
  • 标签(label):用于引用节点,如 display: display-controller@13800000。
  • 包含文件:#include "soc.dtsi" 引入 SoC 级设备树包含文件。
// 一个典型的显示控制器节点示例
display: display-controller@13800000 {
    compatible = "vendor,display-controller", "simple-bus";
    reg = <0x0 0x13800000 0x0 0x1000>;
    interrupts = <0 42 4>;
    clocks = <&clk DISPLAY_CLK>;
    status = "okay";

    port {
        #address-cells = <1>;
        #size-cells = <0>;
        display_out: endpoint@0 {
            reg = <0>;
            remote-endpoint = <&hdmi_in>;
        };
    };
};
提示:设备树中的 #address-cells 和 #size-cells 决定了 reg 属性中地址和长度的字长(32 位或 64 位)。对于 64 位地址空间,通常设置为 <2> 和 <2>。
设备树节点层级结构 / (根节点) soc@0 display-controller@13800000 port endpoint@0 属性示例 compatible = "vendor,display-ctrl"; reg = <0x13800000 0x1000>; interrupts = <0 42 4>;

2.2 节点与属性详解

设备树节点是硬件设备的抽象表示,属性则描述设备的资源配置。下表列出显示驱动中常见的节点类型和关键属性:

节点类型 典型节点名 关键属性 说明
显示控制器 display-controller@13800000 compatible, reg, interrupts, clocks, ports 核心显示硬件单元,负责图像合成与输出
显示接口 hdmi@13810000 compatible, reg, interrupts, ddc-i2c-bus HDMI/DP/LVDS/MIPI DSI 等物理接口
显示面板 panel@0 compatible, enable-gpios, backlight, display-timings LCD/OLED 面板,定义时序和电源控制
时钟控制器 clock-controller@10000000 compatible, reg, #clock-cells 提供显示相关时钟源
端口/端点 port, endpoint@0 reg, remote-endpoint 描述显示链路的数据流连接关系
注意:节点名中的 unit-address(如 @13800000)必须与 reg 属性中的首地址一致,否则设备树编译器(dtc)会报地址不匹配警告。

2.3 compatible 匹配机制

compatible 属性是设备树与内核驱动绑定的关键。内核通过设备驱动模型中 of_match_table 进行匹配,匹配顺序遵循“最具体优先”原则。

匹配流程如下:

  1. 内核解析设备树,提取每个节点的 compatible 字符串列表。
  2. 遍历已注册的平台驱动,比较驱动的 of_match_table 与节点的 compatible。
  3. 按字符串列表顺序匹配,第一个匹配成功的驱动获得绑定。
  4. 匹配成功后调用驱动的 probe 函数,传入 struct platform_device。
// 设备树节点中的 compatible
compatible = "vendor,display-controller-v2", "vendor,display-controller", "simple-bus";

// 内核驱动中的 of_match_table
static const struct of_device_id display_of_match[] = {
    { .compatible = "vendor,display-controller-v2", .data = &v2_data },
    { .compatible = "vendor,display-controller",   .data = &v1_data },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, display_of_match);

static struct platform_driver display_driver = {
    .probe  = display_probe,
    .remove = display_remove,
    .driver = {
        .name = "vendor-display",
        .of_match_table = display_of_match,
    },
};
最佳实践:compatible 字符串应遵循“厂商,型号”格式,版本号放在最后。内核优先匹配列表中的第一个字符串,因此将最精确的版本放在最前面。
compatible 匹配流程 设备树节点 compatible = "v2","v1","bus" 匹配 of_match_table? 匹配成功 调用 probe() 匹配失败 继续遍历下一驱动 匹配顺序说明: 内核按 compatible 字符串列表顺序匹配,先匹配 "vendor,display-controller-v2", 失败后再尝试 "vendor,display-controller",最后尝试 "simple-bus"。

2.4 reg 属性详解

reg 属性描述设备占用的地址空间,格式为 <address length> 的序列。地址和长度的字长由父节点的 #address-cells 和 #size-cells 决定。

#address-cells #size-cells reg 格式示例 地址空间
1 1 reg = <0x13800000 0x1000>; 32 位地址,32 位长度
2 2 reg = <0x0 0x13800000 0x0 0x1000>; 64 位地址,64 位长度
1 0 reg = <0x42>; 仅地址,无长度(如 I2C 地址)

在 DRM 驱动中,reg 属性通常映射为 struct resource,通过 platform_get_resource() 获取:

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

    // 获取第 0 个 reg 段
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    if (!res)
        return -ENXIO;

    // 将物理地址映射到内核虚拟地址空间
    base = devm_ioremap_resource(&pdev->dev, res);
    if (IS_ERR(base))
        return PTR_ERR(base);

    // 后续通过 base 访问显示控制器寄存器
    dev_info(&pdev->dev, "display controller at 0x%llx mapped\n",
             (unsigned long long)res->start);
    return 0;
}
关键点:显示控制器通常需要多个 reg 段(如控制寄存器、帧缓冲器、gamma 表等),通过索引 0、1、2 分别获取。务必核对芯片手册中的地址范围与设备树中的 reg 值一致。

2.5 interrupts 属性详解

interrupts 属性描述设备的中断资源,格式依赖于中断控制器的 #interrupt-cells。ARM GIC(Generic Interrupt Controller)是最常见的中断控制器,其 #interrupt-cells = <3>。

中断域 #interrupt-cells interrupts 格式 说明
ARM GIC 3 <0 42 4> 0=SPI, 42=中断号, 4=触发类型
RISC-V PLIC 1 <42> 仅中断号
简单中断控制器 2 <42 1> 中断号 + 触发类型

ARM GIC 中断域的三个字段含义:

  • 第 1 字段:中断类型,0=SPI(共享外设中断),1=PPI(私有外设中断)。
  • 第 2 字段:中断号,SPI 范围为 32~1019,PPI 范围为 16~31。
  • 第 3 字段:触发类型,1=上升沿触发,2=下降沿触发,4=高电平触发,8=低电平触发。
// 设备树中的中断定义
display-controller@13800000 {
    compatible = "vendor,display-controller";
    reg = <0x0 0x13800000 0x0 0x1000>;
    interrupts = <0 42 4>;  // 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_SHARED, "display", dev);
    if (ret)
        return ret;

    dev_info(&pdev->dev, "registered irq %d\n", irq);
    return 0;
}
常见错误:中断号在设备树中通常使用硬件中断号,而非 Linux IRQ 号。GIC 的 SPI 中断号从 32 开始计数,设备树中的 42 对应 GIC 的 SPI 42,在 Linux 中映射为 IRQ 74(42+32)。驱动中应始终使用 platform_get_irq() 获取已映射的 IRQ 号。
中断资源映射关系 设备树节点 interrupts = <0 42 4> GIC 中断控制器 SPI 42 → hwirq 42 Linux IRQ 域 IRQ 74 (42 + 32) 中断映射流程: 1. 设备树指定 interrupts = <0 42 4> → 类型 SPI,硬件中断号 42,高电平触发。 2. GIC 驱动将 hwirq 42 映射到 Linux IRQ 号(通常 SPI 基数为 32,故 IRQ=74)。 3. 驱动通过 platform_get_irq() 获取 IRQ 74,注册中断处理函数。

2.6 显示链路中的设备树实践

在 DRM 显示驱动中,设备树不仅描述单个设备,还通过 ports 和 endpoint 描述显示链路的数据流连接。以下是一个完整的显示链路设备树片段:

// 显示控制器 -> HDMI 接口 -> HDMI 连接器
display-controller@13800000 {
    compatible = "vendor,display-controller";
    reg = <0x0 0x13800000 0x0 0x1000>;
    interrupts = <0 42 4>;
    ports {
        #address-cells = <1>;
        #size-cells = <0>;
        port@0 {
            reg = <0>;
            dc_out_hdmi: endpoint {
                remote-endpoint = <&hdmi_in_dc>;
            };
        };
    };
};

hdmi@13810000 {
    compatible = "vendor,hdmi-tx";
    reg = <0x0 0x13810000 0x0 0x1000>;
    interrupts = <0 43 4>;
    ports {
        #address-cells = <1>;
        #size-cells = <0>;
        port@0 {
            reg = <0>;
            hdmi_in_dc: endpoint {
                remote-endpoint = <&dc_out_hdmi>;
            };
        };
        port@1 {
            reg = <1>;
            hdmi_out_con: endpoint {
                remote-endpoint = <&con_in_hdmi>;
            };
        };
    };
};

hdmi-connector {
    compatible = "hdmi-connector";
    type = "a";
    port {
        con_in_hdmi: endpoint {
            remote-endpoint = <&hdmi_out_con>;
        };
    };
};
调试技巧:使用 dtc -I dtb -O dts 反编译设备树二进制文件,检查 remote-endpoint 引用是否正确。使用 ls /sys/firmware/devicetree/base/ 查看内核实际解析的设备树结构。