跳转到内容
新建笔记

Linux 驱动中断:控制器、IRQ 映射与线程化处理

驱动申请中断时使用的是 Linux IRQ 号,不是从芯片手册或 DTS 中随手取出的某个整数。硬件中断号、设备树中断说明符与 Linux IRQ 的转换由中断控制器及 irq_domain 完成;多级控制器还会形成级联或层级关系。

原笔记先介绍 GIC,又引用 F1C100S 的 PIO 节点。这两者的中断编码不同,必须分别理解。下面以 Linux 6.8 的 binding 和驱动 API 为例。

类型基本 INTID 范围用途
SGI,Software Generated Interrupt0~15软件发起,常用于处理器间通信;可以指定目标处理器集合
PPI,Private Peripheral Interrupt16~31每个处理器各自的中断,例如局部定时器;相同 INTID 在不同核上表示各自的来源
SPI,Shared Peripheral Interrupt32~1019系统外设产生,可路由到配置的目标处理器;“shared”不表示必须同时广播给所有 CPU

这里限定为 GICv2 的基本编号;1020~1023 是特殊/保留编号,不能把 1020 算成普通 SPI。其他 GIC 版本的扩展中断空间需要另读其规范。

GICv2 通过 GICD_SGIR 发起 SGI,GICD_ITARGETSRn 管理相关 SPI 目标。GICv3 的 SGI 发起和 CPU 接口采用不同的系统寄存器机制,不能照搬寄存器访问代码。

GIC 分发器、CPU 接口与处理器中断入口的原始示意

图中 SPI 的 32~1019 范围是正确的;目标选择位置误写成 GICD_ISENABLERn,后者是使能寄存器,应区分 GICD_ITARGETSRn。图中的 ARMv8 DAIF/HCR 标注也不是 F1C100S ARM9 的控制寄存器说明。

2. 一个 cell 的含义由控制器决定

跳转到“2. 一个 cell 的含义由控制器决定”

中断消费者常见属性是 interrupts(复数)和 interrupt-parent。控制器节点的 interrupt-controller; 表明它提供中断服务,#interrupt-cells 规定每个说明符占几个 32 位 cell。三个 cell 并不普遍等于“中断域、编号、触发方式”。

Linux 的 GIC binding 使用 <类型 相对编号 flags>:类型 0 为 SPI、1 为 PPI;SPI 相对编号从 0 起,对应实际 INTID 加 32,PPI 对应加 16。因此原例 <GIC_SPI 74 IRQ_TYPE_LEVEL_HIGH> 的基本 INTID 是 106,不是 74。三项经头文件展开为 <0 74 4>。Linux GIC binding

flags 也受 binding 约束:常见值 1 表示上升沿、4 表示高电平,但不能把每个 GPIO 支持的下降沿/低电平组合直接套到所有 GIC SPI 上。GICv2 的 PPI 还可能在更高位编码 CPU 掩码。

interrupt-parent 描述的是中断连接关系,未在当前节点写明时,Linux 解析会沿父级查找相应控制器关系;这不保证中断控制器就是设备树结构上的直接父节点。较复杂连接也可由 interrupts-extended、中断映射等 binding 表达。

compatible 表达设备的编程模型,用于选择匹配驱动;它不是中断域编号,也不是 GIC 专用属性。控制器首先根据自己的 binding 解析说明符,再把控制器内的硬件号映射到 Linux IRQ。

3. F1C100S:INTC 上一级,PIO 再分出引脚

跳转到“3. F1C100S:INTC 上一级,PIO 再分出引脚”

Linux 6.8 的 F1C100S SoC 源文件中,根节点的中断父节点指向 intc。这个控制器使用 allwinner,suniv-f1c100s-ic,#interrupt-cells = <1>,不是 GIC。PIO 节点向它提供的三项是:

interrupts = <38>, <39>, <40>;

它们分别对应 PIO 的 D、E、F 三组外部中断汇总输入。这里每项只有一个 cell,不需要额外加上 GIC 的 SPI 偏移 32。F1C100S SoC 描述

同一 PIO 节点的 reg = <0x01c20800 0x400> 表示从该地址开始的 1024 字节寄存器资源;0x400 是长度,不是原注释所说的 TWI 地址差。源节点还声明 apb、hosc、losc 三个时钟名称,分别连接其对应时钟提供者;不能因为只使用某个引脚就省略控制器 binding 所需的时钟。

与此同时,PIO 自己又是下游消费者的 GPIO 与中断控制器:#gpio-cells = <3>、#interrupt-cells = <3>。PIO 的中断消费者使用 <bank pin flags>,例如 PE12 的上升沿说明符是 <4 12 1>,其中 E 按 A=0 起算为 4。其内部 IRQ bank 编号可与这个 GPIO bank 编号不同,控制器驱动会完成转换。Allwinner PIO binding

这也说明:节点自己向上游发出的 interrupts,由上游的 #interrupt-cells 解释;节点自己的 #interrupt-cells 规定的是下游消费者的格式。不能用 PIO 自己的数字 3 去解释其 <38>, <39>, <40>。

4. 从固件资源取得 Linux IRQ

跳转到“4. 从固件资源取得 Linux IRQ”
场景推荐入口不应采用的替代写法
platform 设备的普通中断资源platform_get_irq(pdev, index)直接将 DTS 某个 cell 传给 request_irq
已申请的 GPIO 输入gpiod_to_irq(desc)用 bank * 32 + pin 猜 Linux IRQ
需要线程处理的设备request_threaded_irq / devm_request_threaded_irq在硬中断里执行可能睡眠的 GPIO、I²C 或互斥锁操作

这些取得 IRQ 的接口可能返回负错误码,包括依赖尚未准备好的 -EPROBE_DEFER;不能转换成无符号数后继续申请。dev_err_probe 有助于保留并报告正确的错误。

申请 IRQ 后处理函数可能立即运行,所以实例数据、锁、硬件状态和中断源清理策略必须先就绪。共享 IRQ 的硬处理函数要确认是否确由本设备触发;无关时返回 IRQ_NONE,属于本设备且需唤醒线程时返回 IRQ_WAKE_THREAD。共享线还必须有唯一且非空的 dev_id,通常是实例结构体地址。

IRQF_ONESHOT 使线程处理期间保持相应中断屏蔽,不负责替设备清除其内部中断状态。电平中断若未按手册消除源条件,恢复后仍会再次触发。通用 IRQ API

5. 完整 GPIO 线程中断示例

跳转到“5. 完整 GPIO 线程中断示例”

下面的 vlog_irq.c 申请一根独占的物理上升沿输入,统计线程被调用的次数,并在对应设备目录提供只读 serviced 属性。它没有按键消抖,也不是高速脉冲计数器:屏蔽期间的多个边沿可能合并,计数不保证等于物理脉冲数。

vlog_irq.c
// SPDX-License-Identifier: GPL-2.0-only
#include <linux/atomic.h>
#include <linux/gpio/consumer.h>
#include <linux/interrupt.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/platform_device.h>
#include <linux/slab.h>
#include <linux/sysfs.h>
struct vlog_irq_state {
atomic64_t serviced;
};
static irqreturn_t vlog_irq_thread(int irq, void *data)
{
struct vlog_irq_state *s = data;
atomic64_inc(&s->serviced);
return IRQ_HANDLED;
}
static ssize_t serviced_show(struct device *dev, struct device_attribute *attr,
char *buf)
{
struct vlog_irq_state *s = dev_get_drvdata(dev);
return sysfs_emit(buf, "%lld\n", (long long)atomic64_read(&s->serviced));
}
static DEVICE_ATTR_RO(serviced);
static struct attribute *vlog_attrs[] = { &dev_attr_serviced.attr, NULL };
ATTRIBUTE_GROUPS(vlog);
static int vlog_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct vlog_irq_state *s;
struct gpio_desc *input;
int irq, ret;
s = devm_kzalloc(dev, sizeof(*s), GFP_KERNEL);
if (!s)
return -ENOMEM;
atomic64_set(&s->serviced, 0);
platform_set_drvdata(pdev, s);
input = devm_gpiod_get(dev, "event", GPIOD_IN);
if (IS_ERR(input))
return dev_err_probe(dev, PTR_ERR(input), "cannot acquire event GPIO\n");
irq = gpiod_to_irq(input);
if (irq < 0)
return dev_err_probe(dev, irq, "GPIO has no usable IRQ\n");
/* Exclusive physical rising-edge input; no IRQF_SHARED here. */
ret = devm_request_threaded_irq(dev, irq, NULL, vlog_irq_thread,
IRQF_ONESHOT | IRQF_TRIGGER_RISING,
dev_name(dev), s);
if (ret)
return dev_err_probe(dev, ret, "cannot request IRQ\n");
return devm_device_add_groups(dev, vlog_groups);
}
static const struct of_device_id vlog_of_match[] = {
{ .compatible = "example,vlog-irq" },
{ }
};
MODULE_DEVICE_TABLE(of, vlog_of_match);
static struct platform_driver vlog_driver = {
.probe = vlog_probe,
.driver = {
.name = "vlog_irq",
.of_match_table = vlog_of_match,
},
};
module_platform_driver(vlog_driver);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Counts serviced threaded interrupts from an exclusive GPIO");

这里硬处理函数传 NULL,由内核默认处理函数唤醒线程,适用于这个不共享的简单 GPIO 输入例子。边沿控制器自身的确认由 GPIO/IRQ 控制器驱动负责;若改成带独立状态寄存器的外设,必须增加该设备自己的状态判定与清除流程。

与其配套的 F1C100S 板级片段使用 event-gpios,由 gpiod_to_irq 完成转换,不需要同时再写一份 interrupts:

#include <dt-bindings/gpio/gpio.h>
/* A separate, unshared physical rising-edge input on PE12. */
/ {
event-demo {
compatible = "example,vlog-irq";
event-gpios = <&pio 4 12 GPIO_ACTIVE_HIGH>;
status = "okay";
};
};

example,vlog-irq 是教学用的私有匹配字符串;使用前还需根据原理图确认 PE12 电气条件、引脚复用和占用情况。GPIO_ACTIVE_HIGH 是逻辑极性;示例的 IRQF_TRIGGER_RISING 明确选择物理上升沿,不能以为切换 active-low 就自动反转中断边沿。

同目录 Makefile 填写 obj-m += vlog_irq.o,按 外部模块构建方法 使用目标内核编译。此例的资源申请次序是“实例 → GPIO → IRQ → sysfs 属性”;设备解绑时 managed 资源逆序撤销,先停止属性访问,再释放 IRQ 并等待处理完成,随后释放 GPIO 和实例。

若增加工作队列、定时器、DMA 或设备本身可继续产生的中断,驱动还必须在释放数据前停止这些生产者并同步完成;devm_ 不会替任何自建异步任务推断生命周期。

6. 板上排查与验证范围

跳转到“6. 板上排查与验证范围”

排查时从运行时设备树和绑定状态开始,再观察 /proc/interrupts、设备日志、引脚电平、触发极性与清中断条件。计数完全不增加可能是路由、复用或申请失败;连续暴增也可能是电平源未清除,并不说明 CPU 太快。不要把示例中的逻辑 IRQ 固定成旧代码的 IRQ_EINT... 常量。

本例在 Ubuntu 6.8.0-146-generic 的 x86-64 构建资料下通过编译和 MODPOST;F1C100S 的相关 DT 属性做了编译和编码检查。未加载本模块、未注入真实中断,也未进行板上频率、去抖或丢边沿测量。