3. 设备树常用属性:compatible、reg、interrupts、status、clocks、gpios 等标准属性解析

设备树里的属性,说白了就是描述硬件“长什么样、怎么用”的标签。每个外设节点都靠这些属性告诉内核:我是谁、我接在哪、我怎么被控制。我刚开始学设备树时,总觉得属性太多记不住,后来发现只要抓住几个核心的,其他都是变体。

这一章,我把最常用的六个属性掰开揉碎讲清楚。你跟着走一遍,以后看任何 dts 文件都不会发怵。

3.1 compatible —— 你是谁?

compatible 是设备树里最重要的属性,没有之一。内核通过它来匹配驱动。格式很简单:

compatible = "厂商,型号", "通用型号";

前面是精确匹配,后面是降级兼容。举个例子:

uart0: serial@10000000 {
    compatible = "fsl,imx6q-uart", "fsl,imx21-uart";
    reg = <0x10000000 0x4000>;
    interrupts = <0 58 4>;
};

内核先找 fsl,imx6q-uart 的驱动,找不到就试 fsl,imx21-uart。我遇到过有人把顺序写反了,结果驱动匹配到了不兼容的老版本,串口能注册但收发数据全是乱的。

核心原则:越精确的型号放越前面,越通用的放越后面。不要只写一个字符串,至少留一个降级选项。

我的习惯:写 compatible 时,我会先查内核里 Documentation/devicetree/bindings/ 目录下的绑定文档。那里有官方推荐的字符串格式,照着写最保险。

3.2 reg —— 你在哪?

reg 描述设备的地址空间。它由一组“地址 + 长度”对组成,格式取决于父节点的 #address-cells 和 #size-cells。

#address-cells #size-cells reg 含义
1 1 32 位地址 + 32 位长度
2 1 64 位地址 + 32 位长度
1 0 仅地址,无长度(常用于 GPIO 等)

看个实际例子:

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

    ethernet@20200000 {
        reg = <0x20200000 0x1000>;
        // 基址 0x20200000,长度 4KB
    };

    gpio0: gpio@20900000 {
        reg = <0x20900000 0x1000>;
    };
};

我曾经调试过一个网卡驱动,死活读不到 MAC 地址。查了半天,发现 reg 里的长度写成了 0x100,只映射了 256 字节,而 MAC 寄存器刚好在偏移 0x200 的位置。嗯,这种低级错误最坑人。

注意:reg 的地址值必须和节点名中的 @ 后面的地址一致。不一致时,内核会报 warning,但不会拒绝加载——这种隐晦问题最容易漏掉。

3.3 interrupts —— 你怎么通知我?

interrupts 描述设备的中断信息。它依赖父节点的 #interrupt-cells 来解析。最常见的是 ARM GIC(通用中断控制器),它用 3 个 cell:

interrupts = <中断类型 中断号 触发方式>;

中断类型:0 表示 SPI(共享外设中断),1 表示 PPI(私有外设中断)。
触发方式:1 是上升沿,2 是下降沿,4 是高电平,8 是低电平。

举个例子:

i2c@30000000 {
    compatible = "snps,designware-i2c";
    reg = <0x30000000 0x1000>;
    interrupts = <0 45 4>;
    // SPI 中断,中断号 45,高电平触发
};

我踩过一个坑:某款传感器芯片要求下降沿触发,但设备树里写成了上升沿。结果中断疯狂触发,CPU 占用率直接飙到 100%。后来用示波器一看,信号波形是下降沿,跟设备树配置完全反了。

避坑指南:我曾经在移植 BSP 时,发现中断号总是偏移了 32。后来才明白,有些 SoC 的 SPI 中断号是从 0 开始编号的,而 GIC 规范里 SPI 是从 32 开始。检查芯片手册里的中断映射表,比猜靠谱得多。

3.4 status —— 你开了吗?

status 控制设备是否启用。就这么几个值:

值 含义
okay 设备启用,驱动正常加载
disabled 设备禁用,驱动不加载
reserved 保留给固件使用,OS 不能碰
fail 设备检测失败,但节点保留

实际项目中,status 最常用的场景是:SoC 的 dtsi 文件里把所有外设默认 status = "disabled",板级 dts 里按需打开。

// imx6q.dtsi(SoC 级)
uart1: serial@2020000 {
    status = "disabled";
};

// my-board.dts(板级)
&uart1 {
    status = "okay";
    pinctrl-0 = <&pinctrl_uart1>;
};

我见过有人把 okay 拼写成 ok,内核直接忽略,设备不工作。这种问题查驱动日志根本看不出来,得打开 CONFIG_OF_DEBUG 才能看到解析警告。

记住:status 只写 okay 或 disabled,别搞花活。其他值在内核里的支持程度参差不齐。

3.5 clocks —— 你用什么时钟?

clocks 和 clock-names 配合使用,指定设备需要的时钟源。格式:

clocks = <&clk_node1>, <&clk_node2>;
clock-names = "clk1", "clk2";

驱动通过 clock-names 来获取对应的时钟句柄,不用关心顺序。看个例子:

usdhc@2190000 {
    compatible = "fsl,imx6q-usdhc";
    reg = <0x02190000 0x4000>;
    interrupts = <0 22 4>;
    clocks = <&clks IMX6Q_CLK_USDHC1>,
             <&clks IMX6Q_CLK_USDHC2>;
    clock-names = "ipg", "ahb";
};

驱动里这样用:

clk_ipg = devm_clk_get(&pdev->dev, "ipg");
clk_ahb = devm_clk_get(&pdev->dev, "ahb");

我调试过一个 SD 卡驱动,发现时钟频率总是不对。查了半天,原来是 clock-names 里写的是 "ipg",但驱动里要的是 "per"。名字对不上,devm_clk_get 返回 NULL,驱动直接用了默认频率。

注意:clocks 属性的 cell 个数由时钟控制器的 #clock-cells 决定。如果 #clock-cells = <1>,每个时钟需要两个值:时钟控制器句柄 + 时钟索引。少写一个 cell,整个时钟树都会乱掉。

3.6 gpios —— 你怎么控制引脚?

gpios 描述设备使用的 GPIO 引脚。格式:

gpios = <&gpio_controller 引脚号 标志>;

标志位常用值:

  • 0 —— 高电平有效,默认输出
  • GPIO_ACTIVE_LOW (1) —— 低电平有效
  • GPIO_OPEN_DRAIN (16) —— 开漏输出
  • GPIO_PULL_UP (32) —— 内部上拉

实际例子:

leds {
    compatible = "gpio-leds";
    led-red {
        gpios = <&gpio1 15 GPIO_ACTIVE_LOW>;
        label = "red-led";
    };
};

这里 GPIO_ACTIVE_LOW 表示写 1 时灯灭,写 0 时灯亮。我遇到过硬件工程师把 LED 正极接 GPIO、负极接 VCC,结果设备树里写成了高电平有效,灯永远不亮。最后飞线才搞定。

我的经验:调试 GPIO 问题时,先看原理图确认引脚电平极性,再查设备树的标志位。很多“驱动不工作”的问题,其实是 GPIO 有效电平写反了。用 /sys/kernel/debug/gpio 可以实时查看引脚状态,非常方便。

3.7 属性之间的关系图

下面这张图总结了六个核心属性各自描述了什么,以及它们之间的协作关系:

设备树核心属性关系图 设备节点 compatible reg interrupts status clocks gpios 各属性作用说明: • compatible → 驱动匹配,告诉内核“我是谁” • reg → 地址空间,告诉内核“我在哪” • interrupts → 中断通道,告诉内核“怎么通知我” • status → 启用状态,告诉内核“我开没开” • clocks / gpios → 资源依赖,告诉内核“我需要什么”

3.8 综合示例:一个完整的 I2C 设备节点

把上面六个属性串起来,看一个完整的例子:

i2c@30000000 {
    compatible = "snps,designware-i2c";
    reg = <0x30000000 0x1000>;
    interrupts = <0 45 4>;
    clocks = <&clks I2C_CLK>;
    clock-names = "i2c";
    status = "okay";

    temperature_sensor: sensor@48 {
        compatible = "ti,tmp102";
        reg = <0x48>;
        interrupts = <0 60 4>;
        gpios = <&gpio1 10 GPIO_ACTIVE_LOW>;
    };
};

这个节点里:

  • I2C 控制器本身用 compatible 匹配驱动,reg 定义寄存器基址,interrupts 定义中断,clocks 定义时钟,status 启用它。
  • 子节点(温度传感器)用 compatible 匹配驱动,reg 定义 I2C 地址,interrupts 定义报警中断,gpios 定义就绪引脚。

你看,不管多复杂的设备树,核心就是这几个属性来回组合。掌握了它们,读任何 dts 文件都不会觉得陌生。

总结一句话:设备树属性就是硬件的“身份证 + 地址簿 + 电话线 + 开关 + 资源清单”。把这六个搞明白,设备树就算入门了。