2. 中断申请与释放:request_irq/free_irq 函数详解
中断处理是驱动开发里最核心的环节之一。说白了,外设要通知 CPU 有事干了,就得靠中断线。但这条线怎么申请、怎么释放、怎么和别人共享,就是今天要聊的内容。
我个人习惯把中断申请比作「租电话线」——你得先告诉交换机(内核)你要用几号线、谁来接电话、遇到占线怎么办。搞错了,轻则驱动加载失败,重则系统死机。嗯,咱们一步步拆开看。
2.1 request_irq:把中断线租下来
函数原型长这样:
int request_irq(unsigned int irq,
irq_handler_t handler,
unsigned long flags,
const char *name,
void *dev);
五个参数,一个都不能少。我一个个说。
| 参数 | 含义 | 注意点 |
|---|---|---|
irq |
中断号 | 硬件决定,可通过 platform_get_irq 获取 |
handler |
中断处理函数指针 | 类型 irq_handler_t,返回值 irqreturn_t |
flags |
中断标志位 | 决定触发方式、共享策略等 |
name |
设备名称 | 用于 /proc/interrupts 显示 |
dev |
设备标识 | 共享中断时用来区分设备,必须唯一 |
关键点:dev 参数在共享中断场景下是「身份证」。我曾经在项目里忘了传这个参数,结果 free_irq 时内核找不到对应设备,直接 panic。血的教训。
2.2 中断标志位:flags 里藏着什么?
flags 参数是个位掩码,常用的就这么几个:
- IRQF_SHARED —— 允许多个设备共享同一根中断线。硬件上必须支持电平触发。
- IRQF_TRIGGER_RISING —— 上升沿触发。
- IRQF_TRIGGER_FALLING —— 下降沿触发。
- IRQF_TRIGGER_HIGH —— 高电平触发。
- IRQF_TRIGGER_LOW —— 低电平触发。
- IRQF_ONESHOT —— 中断处理期间自动屏蔽该中断线,处理完再恢复。常用于 threaded IRQ。
你想想看,如果两个设备共享中断线,一个用边沿触发,一个用电平触发,那不乱套了?所以共享中断必须统一用电平触发。这是硬件设计时就定好的规矩。
我的习惯:写驱动时优先用设备树里配好的触发方式,别自己硬编码。设备树里 interrupts = <GIC_SPI 42 IRQ_TYPE_EDGE_RISING>; 这种写法,内核解析后会自动填好 flags。
2.3 中断处理函数原型:到底长啥样?
处理函数的类型是 irq_handler_t,定义在 <linux/interrupt.h>:
typedef irqreturn_t (*irq_handler_t)(int irq, void *dev_id);
两个参数:
irq—— 触发的中断号。同一个处理函数可以注册多个中断号,靠这个区分。dev_id—— 就是 request_irq 时传的dev参数。共享中断时用它判断是不是自己的设备。
返回值只有两种:
IRQ_NONE—— 不是我的中断,请内核继续找下一个处理函数。IRQ_HANDLED—— 我处理了,内核可以收工了。
注意:共享中断里,每个处理函数都必须检查 dev_id 是否匹配。如果返回 IRQ_NONE 但实际是你的设备,内核会报 spurious interrupt 警告。我曾经被这个坑过,排查了半天才发现是 dev_id 传错了。
2.4 free_irq:用完记得还
函数原型:
void free_irq(unsigned int irq, void *dev_id);
参数和 request_irq 一样。注意:
- 非共享中断:
dev_id可以为 NULL。 - 共享中断:
dev_id必须和申请时一致,否则内核不知道要释放哪个。
调用时机通常在模块卸载函数里。顺序很重要——先 free_irq,再注销设备、释放内存。否则中断来了,处理函数访问已释放的内存,直接 oops。
2.5 知识结构图:中断申请与释放全景
下面这张图把整个流程串起来了。我建议你保存下来,写驱动时对照着看。
2.6 完整示例:一个按键中断驱动
光说不练假把式。下面是个真实项目里简化出来的按键中断驱动:
#include <linux/module.h>
#include <linux/interrupt.h>
#include <linux/gpio.h>
#include <linux/of_gpio.h>
static int irq_num;
static int gpio_key = 123; // 假设 GPIO 123
static irqreturn_t key_handler(int irq, void *dev_id)
{
int *pressed = (int *)dev_id;
*pressed = !gpio_get_value(gpio_key);
pr_info("Key %s\n", *pressed ? "pressed" : "released");
return IRQ_HANDLED;
}
static int __init key_drv_init(void)
{
int ret;
static int key_state = 0;
irq_num = gpio_to_irq(gpio_key);
if (irq_num < 0)
return irq_num;
ret = request_irq(irq_num, key_handler,
IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING,
"my_key", &key_state);
if (ret) {
pr_err("request_irq failed: %d\n", ret);
return ret;
}
pr_info("Key IRQ %d registered\n", irq_num);
return 0;
}
static void __exit key_drv_exit(void)
{
free_irq(irq_num, &key_state);
pr_info("Key IRQ freed\n");
}
module_init(key_drv_init);
module_exit(key_drv_exit);
MODULE_LICENSE("GPL");
避坑指南:
- 申请中断前先确认 GPIO 已申请并配置为输入。
- dev_id 传的是
&key_state的地址,不是 NULL。共享中断时这个指针就是你的「身份证」。 - 释放时 dev_id 必须和申请时一致,否则内核会报
Trying to free already-free IRQ。
2.7 常见错误与排查思路
| 错误现象 | 可能原因 | 排查方法 |
|---|---|---|
| request_irq 返回 -EBUSY | 中断号已被占用,或 flags 不匹配 | 检查 /proc/interrupts 看谁在用 |
| 中断触发但处理函数没执行 | 中断被屏蔽,或 flags 触发方式不对 | 检查 /proc/irq/xxx/spurious |
| 共享中断时频繁误触发 | dev_id 没检查,或硬件没拉低电平 | 示波器看波形,确认电平状态 |
| 卸载模块时系统崩溃 | free_irq 后还有中断飞来 | 先屏蔽中断再释放,或检查 dev_id 是否一致 |
嗯,中断申请这块其实不难,但细节特别多。我刚开始写驱动时,经常在 flags 上翻车——明明设备树配的是上升沿,我硬编码了个下降沿,结果按键按下去没反应。后来养成了习惯:能用设备树就别硬编码,能共享就别独占。