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私有外设中断)、中断号、触发类型与优先级标志。
#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。务必检查每个中断控制器节点是否完整声明了这两个属性。
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的标准步骤:
- 获取GPIO描述符:使用
devm_gpiod_get(dev, con_id, flags)或devm_gpiod_get_index(dev, con_id, index, flags)。 - 设置方向:
gpiod_direction_input(gpiod)或gpiod_direction_output(gpiod, value)。 - 读写电平:
gpiod_get_value(gpiod)和gpiod_set_value(gpiod, value)。 - 释放:使用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 通用属性混淆。
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 属性解析。
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 加载。
evtest 工具验证按键事件是否正常上报。这是理解中断与GPIO协同工作的最佳入门实验。