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 知识结构图:中断申请与释放全景

下面这张图把整个流程串起来了。我建议你保存下来,写驱动时对照着看。

中断申请与释放全景图 request_irq 参数:irq/handler/flags/name/dev flags:IRQF_SHARED / 触发方式 中断处理函数 返回值:IRQ_NONE / IRQ_HANDLED 共享中断:检查 dev_id 非共享:直接处理 free_irq 参数:irq + dev_id ⚠ 注意事项:先 free_irq 再释放资源 / 共享中断必须传 dev_id / 触发方式与硬件一致 核心流程:申请 → 注册处理函数 → 处理中断 → 释放 共享中断必须用电平触发,处理函数需返回 IRQ_NONE 或 IRQ_HANDLED

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 上翻车——明明设备树配的是上升沿,我硬编码了个下降沿,结果按键按下去没反应。后来养成了习惯:能用设备树就别硬编码,能共享就别独占。