3. 设备树与平台设备

在 Linux 设备树驱动适配工程中,设备树负责“描述硬件”,平台设备驱动负责“驱动硬件”,而两者之间的桥梁就是标准属性与匹配机制。理解 compatible、reg、interrupts、clocks、gpios 等属性的语义,以及 platform_driver 如何与设备树节点完成匹配,是后续所有适配工作的基础。本节从属性含义、匹配流程、典型场景和常见误区四个层面展开。

一、核心原理:设备树节点如何变成平台设备

内核启动时,会解析 DTB 中的每一个设备节点。对于位于总线(如 simple-bus、platform-bus)下、且 compatible 属性存在的节点,内核会将其转换为 platform_device,并把节点中的标准属性翻译成资源:reg 变成 IORESOURCE_MEM,interrupts 变成 IORESOURCE_IRQ,clocks、gpios 等则保留在节点中由驱动通过 of_* 接口按需获取。驱动侧注册 platform_driver 后,内核用 of_device_id 表中的 compatible 字符串与设备节点逐一比对,匹配成功即调用驱动的 probe 函数。

设备树不是“配置脚本”,而是硬件描述。驱动不应硬编码寄存器地址、中断号、GPIO 编号,而应通过 of_* 接口从节点中读取,这样才能做到“一份驱动适配多块板”。

二、标准属性语义与驱动读取接口

下面这张图展示了设备树节点中的标准属性如何被内核翻译,并最终被平台驱动通过对应 API 读取。图中左侧是节点属性,中间是内核翻译结果,右侧是驱动常用接口。

设备树标准属性 → 内核资源 → 驱动读取接口 设备树节点属性 compatible reg interrupts clocks / clock-names gpios / reset-gpios status / pinctrl-* 内核翻译结果 of_device_id 匹配键 IORESOURCE_MEM IORESOURCE_IRQ struct clk 句柄 struct gpio_desc pinctrl 状态 驱动读取接口 of_match_device() platform_get_resource() platform_get_irq() devm_clk_get() devm_gpiod_get() pinctrl_select_state()

各属性的语义与读取方式如下表所示:

属性 含义 驱动读取接口 常见错误
compatible 匹配键,格式为 "厂商,型号" of_device_id 表 / of_match_device() 字符串拼写不一致、缺少厂商前缀
reg 寄存器地址与长度 platform_get_resource() + devm_ioremap_resource() 地址与父节点 ranges 不匹配
interrupts 中断号与触发类型 platform_get_irq() / devm_request_irq() 中断父节点未指定或类型写错
clocks 时钟句柄列表 devm_clk_get() + clk_prepare_enable() clock-names 与驱动请求名不一致
gpios GPIO 句柄与标志 devm_gpiod_get() / gpiod_set_value() 未配置 pinctrl 导致引脚复用错误

三、匹配机制与 probe 流程

平台设备与驱动的匹配由内核总线层完成,核心是 of_device_id 表中的 compatible 与设备节点 compatible 的比对。匹配成功后进入 probe,典型流程如下:

  1. 驱动注册时提供 of_device_id 匹配表,并设置 .compatible 字段。
  2. 内核遍历平台设备,调用 of_match_device() 比对 compatible。
  3. 匹配成功,调用驱动的 probe(),传入 platform_device。
  4. 在 probe 中依次获取 reg、interrupts、clocks、gpios 等资源。
  5. 初始化硬件、注册字符设备或子系统接口,返回 0 表示成功。
static const struct of_device_id demo_of_match[] = {
    { .compatible = "vendor,demo-uart" },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, demo_of_match);

static int demo_probe(struct platform_device *pdev)
{
    struct resource *res;
    void __iomem *base;
    int irq;
    struct clk *clk;

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

    irq = platform_get_irq(pdev, 0);
    if (irq < 0)
        return irq;

    clk = devm_clk_get(&pdev->dev, "baud");
    if (IS_ERR(clk))
        return PTR_ERR(clk);
    clk_prepare_enable(clk);

    return 0;
}

static struct platform_driver demo_driver = {
    .probe = demo_probe,
    .driver = {
        .name = "demo-uart",
        .of_match_table = demo_of_match,
    },
};
module_platform_driver(demo_driver);

四、典型场景:一块板适配多颗芯片

假设同一份驱动要支持 A 厂和 B 厂两颗 UART 控制器,寄存器布局相同但时钟名不同。设备树中分别写:

/* A 厂节点 */
uart0: serial@10000000 {
    compatible = "vendorA,uart-v1";
    reg = <0x10000000 0x1000>;
    interrupts = <GIC_SPI 32 IRQ_TYPE_LEVEL_HIGH>;
    clocks = <&clk_uart0>;
    clock-names = "baud";
};

/* B 厂节点 */
uart1: serial@10001000 {
    compatible = "vendorB,uart-v2";
    reg = <0x10001000 0x1000>;
    interrupts = <GIC_SPI 33 IRQ_TYPE_LEVEL_HIGH>;
    clocks = <&clk_uart1>;
    clock-names = "baud";
};

驱动只需在 of_device_id 表中同时列出两个 compatible,probe 中统一用 devm_clk_get(&pdev->dev, "baud") 获取时钟,即可实现一份驱动适配两颗芯片。若 B 厂时钟名不同,可在驱动中根据 of_device_get_match_data() 返回的私有数据区分处理,而不是在 probe 中硬编码判断。

推荐使用 devm_* 系列接口管理资源,内核会在设备卸载时自动释放,避免手动 goto 清理带来的遗漏。

五、误区与复盘

  • compatible 写成通用字符串:如只写 "uart",会与其它驱动冲突,必须使用 "厂商,型号" 格式。
  • reg 地址与 ranges 不匹配:子节点地址需经过父节点 ranges 转换,否则 ioremap 会失败。
  • interrupts 缺少 interrupt-parent:若父节点未继承,需在节点中显式指定中断控制器。
  • clocks 与 clock-names 顺序不一致:驱动按名字获取时,名字必须与 clock-names 完全一致。
  • gpios 未配 pinctrl:GPIO 能申请成功但引脚功能未切换,表现为电平无变化。
  • probe 返回错误码被忽略:应使用 PTR_ERR() 和 IS_ERR() 判断,返回负值让内核延迟重试。
调试匹配失败时,优先查看 /sys/firmware/devicetree/base/ 下节点是否存在,再用 dmesg | grep -i "demo" 观察 probe 是否被调用。若节点存在但 probe 未执行,多半是 compatible 不匹配或驱动未编译进内核。

掌握标准属性与匹配机制后,后续的 pinctrl、regulator、DMA 等属性适配都可以沿用同一套“节点描述—内核翻译—驱动读取”的思路,做到举一反三。