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完整路径:
从图中可以看出,外设可以通过两种方式连接到中断控制器:直接连接(通过 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配置,说白了就是搞清楚“谁连谁、怎么连、参数是什么”。只要把这三个问题回答清楚,设备树这部分就稳了。我在项目中反复验证过,按照这个思路来,基本不会出大问题。