4. 设备树中断与GPIO:中断控制器描述、中断映射、GPIO控制器描述、GPIO属性与使用

在嵌入式Linux系统中,中断(IRQ)与GPIO(通用输入输出)是驱动与硬件交互最核心的两个机制。设备树通过标准化的绑定语法,将中断控制器、中断映射关系、GPIO控制器及其引脚属性描述为节点与属性,驱动据此完成资源申请与硬件操作。本章将深入解析中断与GPIO在设备树中的描述规范、映射原理及驱动适配方法。

核心要点:中断控制器通过 interrupt-controller 属性标记,#interrupt-cells 定义中断描述符的单元数;GPIO控制器通过 gpio-controller 标记,#gpio-cells 定义引脚描述符的单元数。中断映射由 interrupt-parent 和 interrupts 属性完成,GPIO使用则通过 gpios 或 [name]-gpios 属性引用。

4.1 中断控制器描述

中断控制器是系统中负责接收外设中断信号并向CPU核提交中断请求的硬件单元。在设备树中,中断控制器节点必须包含以下关键属性:

属性 类型 说明
interrupt-controller 空属性 标记该节点为中断控制器
#interrupt-cells u32 指定中断描述符所需的单元数(通常为1、2或3)
compatible 字符串 匹配中断控制器驱动
reg 地址/长度对 中断控制器的寄存器基地址与范围

以下是一个典型的ARM GIC(通用中断控制器)设备树节点示例:

gic: interrupt-controller@f1000000 {
    compatible = "arm,cortex-a15-gic";
    #interrupt-cells = <3>;
    interrupt-controller;
    reg = <0x0 0xf1000000 0x0 0x10000>,
          <0x0 0xf1001000 0x0 0x2000>;
};

对于 #interrupt-cells = <3> 的GIC,中断描述符的三个单元分别表示:中断类型(0=SPI共享外设中断,1=PPI私有外设中断)、中断号、触发类型与优先级标志。

经验提示:ARM GICv2/v3 使用 #interrupt-cells = <3>,而许多SoC内部的中断控制器(如TI OMAP的INTC)使用 #interrupt-cells = <2>,第一个单元为中断号,第二个为触发类型。务必查阅芯片手册确认中断描述符格式。

4.2 中断映射:从外设到中断控制器

外设节点通过 interrupt-parent 和 interrupts 属性将硬件中断信号映射到中断控制器。映射过程涉及中断域的转换,尤其在多级中断控制器级联时更为复杂。

4.2.1 基本中断映射

最简单的映射方式:外设节点直接引用父级中断控制器,并给出中断描述符。

uart0: serial@10000000 {
    compatible = "ns16550a";
    reg = <0x0 0x10000000 0x0 0x1000>;
    interrupt-parent = <&gic>;
    interrupts = <0 33 4>;  /* SPI 33, 高电平触发 */
};

4.2.2 中断映射与中断域

当系统中存在多个中断控制器(例如GPIO控制器也作为中断控制器)时,外设的中断号可能不是全局的,而是相对于其父中断控制器的本地中断号。设备树使用 interrupt-map 和 interrupt-map-mask 属性实现中断域之间的地址转换。

pcie@20000000 {
    compatible = "pci-host-ecam-generic";
    reg = <0x0 0x20000000 0x0 0x10000000>;
    #address-cells = <3>;
    #size-cells = <2>;
    interrupt-parent = <&gic>;
    interrupts = <0 48 4>;  /* 主中断 */
    #interrupt-cells = <1>;
    interrupt-map-mask = <0 0 0 7>;
    interrupt-map = <0 0 0 1 &pcie_intc 1>,
                    <0 0 0 2 &pcie_intc 2>,
                    <0 0 0 3 &pcie_intc 3>,
                    <0 0 0 4 &pcie_intc 4>;
    
    pcie_intc: interrupt-controller {
        interrupt-controller;
        #interrupt-cells = <1>;
    };
};

上述示例中,PCIe设备的INTA~INTD通过 interrupt-map 映射到PCIe桥内部的中断控制器 pcie_intc,再由该控制器将中断路由到GIC。

常见错误:在多级中断映射时,忘记设置 #interrupt-cells 或 interrupt-controller 属性会导致内核无法解析中断域,驱动申请中断失败返回 -EINVAL。务必检查每个中断控制器节点是否完整声明了这两个属性。
设备树中断映射层级结构 CPU 核 GIC 中断控制器 UART (interrupts) GPIO 控制器 PCIe 桥 PCIe INTC interrupt-parent 指向父中断控制器,interrupts 提供本地中断描述符;interrupt-map 实现跨域转换

4.3 GPIO控制器描述

GPIO控制器在设备树中同样需要标准化的声明方式,以便驱动能够枚举引脚、配置方向与电气特性。

属性 类型 说明
gpio-controller 空属性 标记该节点为GPIO控制器
#gpio-cells u32 指定GPIO描述符所需的单元数(通常为2)
ngpios u32 该控制器管理的GPIO引脚总数(可选,默认由驱动决定)
gpio-ranges u32数组 将GPIO引脚映射到pinctrl子系统,格式:<控制器基址, pinctrl基址, 数量>

典型GPIO控制器节点示例:

gpio0: gpio@ff000000 {
    compatible = "snps,dw-apb-gpio";
    reg = <0x0 0xff000000 0x0 0x1000>;
    gpio-controller;
    #gpio-cells = <2>;
    ngpios = <32>;
    gpio-ranges = <&pinctrl 0 0 32>;
};

其中 #gpio-cells = <2> 的两个单元通常表示:引脚号(0~31)和GPIO标志位(如 GPIO_ACTIVE_HIGH 或 GPIO_ACTIVE_LOW)。

4.4 GPIO属性与使用

外设节点通过 gpios 或命名后缀 [name]-gpios 属性引用GPIO引脚。驱动在probe阶段使用 devm_gpiod_get 系列API获取GPIO描述符,并操作引脚。

4.4.1 设备树中的GPIO引用

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

button@0 {
    compatible = "gpio-keys";
    interrupt-parent = <&gpio0>;
    interrupts = <15 IRQ_TYPE_EDGE_FALLING>;
    gpios = <&gpio0 15 GPIO_ACTIVE_LOW>;
    label = "user-button";
};

上述示例展示了两种GPIO使用场景:LED作为输出,按键作为输入且同时使用中断功能。注意按键节点同时声明了 gpios(用于读取电平)和 interrupts(用于边沿触发),两者可以共存。

4.4.2 GPIO标志位定义

内核头文件 include/dt-bindings/gpio/gpio.h 定义了常用的GPIO标志位宏:

宏名称 值 说明
GPIO_ACTIVE_HIGH 0 高电平有效(默认)
GPIO_ACTIVE_LOW 1 低电平有效
GPIO_OPEN_DRAIN 2 开漏输出
GPIO_OPEN_SOURCE 4 开源输出
GPIO_PULL_UP 8 内部上拉
GPIO_PULL_DOWN 16 内部下拉

4.4.3 驱动侧GPIO使用流程

驱动中获取并操作GPIO的标准步骤:

  1. 获取GPIO描述符:使用 devm_gpiod_get(dev, con_id, flags) 或 devm_gpiod_get_index(dev, con_id, index, flags)。
  2. 设置方向:gpiod_direction_input(gpiod) 或 gpiod_direction_output(gpiod, value)。
  3. 读写电平:gpiod_get_value(gpiod) 和 gpiod_set_value(gpiod, value)。
  4. 释放:使用devm管理时自动释放,否则调用 gpiod_put(gpiod)。
#include <linux/gpio/consumer.h>

struct gpio_desc *led_gpio;

led_gpio = devm_gpiod_get(&pdev->dev, "led", GPIOD_OUT_LOW);
if (IS_ERR(led_gpio)) {
    dev_err(&pdev->dev, "Failed to get LED GPIO\n");
    return PTR_ERR(led_gpio);
}

gpiod_set_value(led_gpio, 1);  /* 点亮LED */
命名约定:设备树中使用 led-gpios 属性时,驱动侧 devm_gpiod_get(dev, "led", ...) 会自动去掉后缀 -gpios 并匹配。这是内核GPIO子系统推荐的命名方式,避免与 gpios 通用属性混淆。
GPIO控制器与外设交互模型 GPIO 控制器 gpio-controller; #gpio-cells = <2>; ngpios = <32>; LED 设备 gpios = <&gpio0 12 0>; 按键设备 gpios = <&gpio0 15 1>; 传感器 xyz-gpios = <&gpio0 20 0>; GPIO 使用要点 • 输出:gpiod_set_value() • 输入:gpiod_get_value() • 中断:interrupts 属性

4.5 中断与GPIO的协同使用

许多外设同时需要中断和GPIO功能,例如按键既需要GPIO读取电平状态,也需要中断检测按下事件。设备树支持在同一节点中同时声明 interrupts 和 gpios,但需注意两者可能引用不同的控制器。

touch@38 {
    compatible = "edt,edt-ft5406";
    reg = <0x38>;
    interrupt-parent = <&gpio1>;
    interrupts = <8 IRQ_TYPE_EDGE_FALLING>;
    reset-gpios = <&gpio2 5 GPIO_ACTIVE_LOW>;
    irq-gpios = <&gpio1 8 GPIO_ACTIVE_LOW>;
};

上述触摸屏控制器节点中,中断引脚和复位引脚分别使用不同的GPIO控制器。驱动中通过 devm_gpiod_get(dev, "reset", ...) 获取复位引脚,通过 devm_gpiod_get(dev, "irq", ...) 获取中断引脚,而中断注册则使用 platform_get_irq(pdev, 0) 从 interrupts 属性解析。

避坑指南:当GPIO控制器同时也是中断控制器时(如 gpio1 节点同时声明了 gpio-controller 和 interrupt-controller),外设节点引用该GPIO作为中断父节点时,中断号是GPIO的引脚号,而非系统全局中断号。驱动中 platform_get_irq 返回的是经过映射后的虚拟中断号,可直接用于 request_irq。

4.6 实战:设备树覆盖中的中断与GPIO

在设备树覆盖(Device Tree Overlay)场景中,中断与GPIO的引用需要特别注意符号解析与依赖顺序。以下是一个覆盖示例,为扩展板添加一个GPIO按键和LED:

// my-extension.dts
/dts-v1/;
/plugin/;

&{/} {
    my_led: my-led {
        compatible = "gpio-leds";
        led-ext {
            label = "ext:green";
            gpios = <&gpio0 23 GPIO_ACTIVE_HIGH>;
        };
    };
};

&{/} {
    my_button: my-button {
        compatible = "gpio-keys";
        #address-cells = <1>;
        #size-cells = <0>;
        button-ext {
            label = "ext-button";
            gpios = <&gpio0 24 GPIO_ACTIVE_LOW>;
            interrupts = <24 IRQ_TYPE_EDGE_FALLING>;
            interrupt-parent = <&gpio0>;
        };
    };
};

编译覆盖文件时,需确保 gpio0 符号在基板设备树中已定义且可被覆盖引用。使用 dtc -@ -I dts -O dtb -o my-extension.dtbo my-extension.dts 生成覆盖二进制,再通过 configfs 或 u-boot overlays 加载。

课程实践建议:在QEMU或真实开发板上,尝试编写一个简单的覆盖文件,添加一个GPIO按键并注册为输入设备。使用 evtest 工具验证按键事件是否正常上报。这是理解中断与GPIO协同工作的最佳入门实验。