4. 设备树中断与GPIO:interrupt-parent、interrupts、gpios属性配置

中断和GPIO,可以说是嵌入式开发中最常用的两个外设资源了。我这些年调过的板子,十有八九的坑都出在这两个地方。设备树里怎么描述它们?说白了,就是让内核知道:哪个设备能产生中断,中断信号连到了哪个中断控制器,以及GPIO引脚怎么用。

咱们今天就把 interrupt-parent、interrupts 和 gpios 这三个属性彻底讲透。嗯,你想想看,搞懂了它们,设备树里一半的配置你都能拿下了。

4.1 中断的“三件套”:谁产生、连到哪、怎么用

一个完整的中断描述,在设备树里需要三个信息:

  • 谁产生中断 —— 外设节点本身
  • 中断信号连到谁 —— interrupt-parent 指向中断控制器
  • 中断的具体参数 —— interrupts 属性描述中断号、触发方式等

举个例子,一个UART设备要使用中断:

uart0: serial@10000000 {
    compatible = "ns16550a";
    reg = <0x10000000 0x1000>;
    interrupt-parent = <&gic>;
    interrupts = <0 33 4>;
};

这里 interrupt-parent = <&gic> 表示中断信号送到GIC(通用中断控制器)。interrupts 三个数字分别代表:中断类型(0=SPI)、中断号(33)、触发方式(4=高电平触发)。

核心要点:如果一个系统里只有一个中断控制器,interrupt-parent 可以在根节点或父节点统一指定,子节点会继承。但我在项目中遇到过,有人忘了给子节点显式指定,结果换了中断控制器后,所有外设的中断都乱了——因为继承的是旧的中断控制器。

4.2 interrupt-parent:谁是你的“上级”

interrupt-parent 是一个phandle(指针句柄),指向中断控制器的节点。它的作用就是告诉内核:我这个设备的中断请求线,物理上连到了哪个中断控制器。

来看一个多级中断控制器的例子:

/ {
    interrupt-parent = <&gic>;

    gic: interrupt-controller@10400000 {
        compatible = "arm,gic-400";
        reg = <0x10400000 0x10000>;
        interrupt-controller;
        #interrupt-cells = <3>;
    };

    gpio0: gpio@11000000 {
        compatible = "snps,dw-apb-gpio";
        reg = <0x11000000 0x1000>;
        interrupt-parent = <&gic>;
        interrupts = <0 42 4>;
        #gpio-cells = <2>;
        gpio-controller;
        interrupt-controller;
        #interrupt-cells = <2>;
    };

    button@0 {
        compatible = "gpio-keys";
        interrupt-parent = <&gpio0>;
        interrupts = <5 2>;  /* GPIO5, 下降沿触发 */
    };
};

你看,button@0 的 interrupt-parent 指向了 gpio0,而不是根节点的 gic。为什么?因为按键的中断信号先到了GPIO控制器,再由GPIO控制器转发给GIC。这就是中断的层级关系。

我的习惯:在写设备树时,我通常会在根节点统一设置 interrupt-parent,然后在每个外设节点里再检查一遍。如果某个外设的中断控制器和根节点不同,就显式覆盖。这样既省代码,又不容易漏。

4.3 interrupts:中断的“身份证”

interrupts 属性描述中断的具体参数。它的格式由中断控制器的 #interrupt-cells 决定。常见的有:

中断控制器类型 #interrupt-cells 格式说明
ARM GIC 3 <中断类型 中断号 触发方式>
RISC-V PLIC 1 <中断号>
GPIO控制器 2 <引脚号 触发方式>
简单中断控制器 1 <中断号>

以ARM GIC为例,三个字段的含义:

  • 中断类型:0=SPI(共享外设中断),1=PPI(私有外设中断)
  • 中断号:从32开始算(0-31是内部中断)
  • 触发方式:1=上升沿,2=下降沿,4=高电平,8=低电平

我曾经在调试一个网卡驱动时,发现中断一直不触发。查了半天,原来是触发方式写错了——网卡是电平触发,我写成了边沿触发。内核虽然能识别,但硬件就是不配合。嗯,这种低级错误,犯过一次就记住了。

4.4 gpios:GPIO的“接线图”

GPIO在设备树里用 gpios 属性描述。格式是:<&gpio控制器 引脚号 标志位>。

来看一个LED的例子:

leds {
    compatible = "gpio-leds";
    led0 {
        label = "power";
        gpios = <&gpio0 12 GPIO_ACTIVE_HIGH>;
        default-state = "on";
    };
    led1 {
        label = "status";
        gpios = <&gpio0 13 GPIO_ACTIVE_LOW>;
        linux,default-trigger = "heartbeat";
    };
};

这里 gpios = <&gpio0 12 GPIO_ACTIVE_HIGH> 表示:使用 gpio0 控制器的第12号引脚,高电平有效。GPIO_ACTIVE_LOW 则相反,低电平点亮。

注意:GPIO的引脚号是从0开始算的,不是芯片手册上的物理引脚号。我见过有人直接把芯片手册的引脚号写进去,结果驱动操作的完全是另一个引脚。一定要看GPIO控制器的 ngpios 属性和驱动文档,确认引脚号的映射关系。

4.5 中断与GPIO的“联姻”:gpio-keys和gpio-leds

实际项目中,中断和GPIO经常一起出现。最典型的就是按键和LED:

gpio-keys {
    compatible = "gpio-keys";
    #address-cells = <1>;
    #size-cells = <0>;
    autorepeat;

    button@0 {
        label = "Power";
        linux,code = <KEY_POWER>;
        gpios = <&gpio0 5 GPIO_ACTIVE_LOW>;
        interrupts = <5 IRQ_TYPE_EDGE_FALLING>;
        interrupt-parent = <&gpio0>;
    };
};

这里同时用了 gpios 和 interrupts。gpios 用于读取按键的当前状态,interrupts 用于在按键按下时产生中断。两者配合,既能轮询又能中断触发。

我个人习惯是:能用中断就别用轮询。轮询浪费CPU,中断只在事件发生时响应。但要注意,有些GPIO控制器不支持所有引脚的中断,需要查手册确认。

4.6 避坑指南:我踩过的那些坑

做设备树配置这么多年,我总结了几条血泪教训:

  • 中断号偏移:ARM GIC的中断号从32开始,但有些SoC厂商的文档直接写物理中断号(从0开始)。写设备树时一定要确认是否需要加32。我曾经因为这个,调了整整两天。
  • GPIO复用:同一个引脚可能被多个功能复用。配置了GPIO后,要确保对应的引脚复用寄存器也设置正确。否则,你操作GPIO,硬件却在执行别的功能。
  • 中断控制器级联:如果系统有多个中断控制器(比如GIC+GPIO控制器),一定要检查每个外设的 interrupt-parent 是否正确。漏写或写错,中断就收不到。
  • 标志位冲突:GPIO_ACTIVE_HIGH 和 GPIO_ACTIVE_LOW 搞反了,LED会常亮或常灭。按键的按下/释放逻辑也会反。

我的调试方法:遇到中断或GPIO问题,我第一件事就是看 /proc/interrupts 和 /sys/kernel/debug/gpio。前者看中断有没有注册,后者看GPIO的电平和方向。这两个文件能帮你快速定位80%的问题。

4.7 知识体系图:中断与GPIO的完整链路

下面这张图展示了从外设到CPU的中断和GPIO完整路径:

设备树中断与GPIO链路图 设备树中断与GPIO链路图 外设节点 UART / I2C / SPI GPIO控制器 gpios 属性 中断控制器 GIC / PLIC CPU核心 处理中断 gpios interrupts interrupt-parent IRQ线 关键属性说明: • interrupt-parent:指向中断控制器节点,定义中断信号的接收方 • interrupts:中断参数,格式由中断控制器的 #interrupt-cells 决定 • gpios:GPIO引脚描述,格式为 <控制器 引脚号 标志位>

从图中可以看出,外设可以通过两种方式连接到中断控制器:直接连接(通过 interrupt-parent)或通过GPIO控制器间接连接。GPIO控制器本身也是一个中断控制器,它把多个GPIO引脚的中断汇聚后,再发给上级中断控制器。

4.8 实战:一个完整的配置示例

最后,咱们看一个完整的设备树片段,把今天讲的知识点串起来:

/dts-v1/;
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/interrupt-controller/irq.h>

/ {
    model = "MyBoard";
    compatible = "mycompany,myboard";
    interrupt-parent = <&gic>;

    gic: interrupt-controller@10400000 {
        compatible = "arm,gic-400";
        reg = <0x10400000 0x10000>;
        interrupt-controller;
        #interrupt-cells = <3>;
    };

    gpio0: gpio@11000000 {
        compatible = "snps,dw-apb-gpio";
        reg = <0x11000000 0x1000>;
        interrupt-parent = <&gic>;
        interrupts = <0 42 4>;
        #gpio-cells = <2>;
        gpio-controller;
        interrupt-controller;
        #interrupt-cells = <2>;
        ngpios = <32>;
    };

    uart0: serial@10000000 {
        compatible = "ns16550a";
        reg = <0x10000000 0x1000>;
        interrupt-parent = <&gic>;
        interrupts = <0 33 4>;
        clock-frequency = <1843200>;
    };

    leds {
        compatible = "gpio-leds";
        led0 {
            label = "power";
            gpios = <&gpio0 12 GPIO_ACTIVE_HIGH>;
            default-state = "on";
        };
    };

    gpio-keys {
        compatible = "gpio-keys";
        autorepeat;
        button@0 {
            label = "Power";
            linux,code = <KEY_POWER>;
            gpios = <&gpio0 5 GPIO_ACTIVE_LOW>;
            interrupt-parent = <&gpio0>;
            interrupts = <5 IRQ_TYPE_EDGE_FALLING>;
        };
    };
};

这个例子涵盖了今天讲的所有知识点:中断控制器的声明、外设的中断配置、GPIO控制器的声明、以及GPIO和中断的联合使用。你可以把它当作一个模板,在实际项目中直接修改使用。

嗯,设备树的中断和GPIO配置,说白了就是搞清楚“谁连谁、怎么连、参数是什么”。只要把这三个问题回答清楚,设备树这部分就稳了。我在项目中反复验证过,按照这个思路来,基本不会出大问题。