3. 中断注册与注销

中断处理是驱动响应硬件事件的“神经反射”。注册与注销中断处理函数是每个中断驱动必须完成的基础操作。Linux 内核提供 request_irq() 和 free_irq() 两个核心接口,配合 flags 标志位实现中断触发方式、共享策略、唤醒行为等精细控制。本节将深入剖析这两个接口的语义、参数细节与常见陷阱,并通过实战案例演示 flags 的配置方法。

3.1 request_irq:中断处理函数的注册入口

request_irq() 是驱动向内核注册中断处理函数的标准接口。其函数原型如下:

int request_irq(unsigned int irq,
                irq_handler_t handler,
                unsigned long flags,
                const char *name,
                void *dev);

各参数含义与注意事项:

参数 类型 说明 常见陷阱
irq unsigned int 中断号,由硬件平台或设备树指定 使用 platform_get_irq() 获取,不要硬编码
handler irq_handler_t 中断处理函数指针,返回 irqreturn_t 必须检查返回值:IRQ_HANDLED 或 IRQ_NONE
flags unsigned long 中断标志位,控制触发方式、共享策略等 共享中断必须设置 IRQF_SHARED
name const char * 中断名称,用于 /proc/interrupts 显示 建议使用驱动名+设备实例,便于调试
dev void * 设备标识,共享中断时用于区分设备 必须唯一,通常传设备结构体指针
调试提示: 注册失败时,首先检查 /proc/interrupts 确认中断号是否已被占用;其次确认 flags 是否与硬件实际触发方式匹配(电平/边沿)。

3.2 free_irq:安全注销中断处理函数

驱动卸载或设备移除时,必须调用 free_irq() 释放中断资源。其原型为:

void free_irq(unsigned int irq, void *dev);

关键行为:

  • 同步等待: free_irq() 会阻塞,直到所有正在执行的中断处理函数返回,确保后续不会再有该中断的调用。
  • dev 参数匹配: 必须与 request_irq() 传入的 dev 完全一致,否则内核无法正确找到要注销的处理函数。
  • 共享中断注销: 仅当最后一个使用该中断号的设备注销时,内核才会真正释放中断线。
常见故障: 在中断处理函数内部调用 free_irq() 会导致死锁!因为 free_irq() 会等待当前处理函数返回,形成自我等待。正确的做法是在进程上下文(如 remove 回调)中注销中断。

3.3 中断标志位 flags 配置实战

flags 参数是中断行为配置的核心,由多个宏按位或组合而成。以下是驱动开发中最常用的标志位:

标志位 值 作用 适用场景
IRQF_TRIGGER_RISING 0x00000001 上升沿触发 GPIO 按键、边沿敏感外设
IRQF_TRIGGER_FALLING 0x00000002 下降沿触发 GPIO 按键、边沿敏感外设
IRQF_TRIGGER_HIGH 0x00000004 高电平触发 电平敏感外设、中断控制器级联
IRQF_TRIGGER_LOW 0x00000008 低电平触发 电平敏感外设、唤醒源
IRQF_SHARED 0x00000080 允许共享中断线 多个设备共用同一中断号
IRQF_ONESHOT 0x00002000 中断处理期间屏蔽本中断 线程化中断处理、需要防止嵌套的场景
IRQF_NO_SUSPEND 0x00004000 系统休眠时不关闭中断 唤醒源、实时时钟中断

实战配置示例: 一个 GPIO 按键驱动,使用下降沿触发、共享中断、且作为系统唤醒源:

#define GPIO_IRQ_NR  123
#define DEV_NAME     "my_gpio_key"

static struct my_device {
    int irq;
    // ... 其他设备数据
} my_dev;

static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
    struct my_device *dev = (struct my_device *)dev_id;
    // 处理中断事件
    pr_info("Interrupt received on device %s\n", DEV_NAME);
    return IRQ_HANDLED;
}

static int my_probe(struct platform_device *pdev)
{
    int ret;
    unsigned long flags = IRQF_TRIGGER_FALLING |
                          IRQF_SHARED |
                          IRQF_NO_SUSPEND;

    my_dev.irq = platform_get_irq(pdev, 0);
    if (my_dev.irq < 0)
        return my_dev.irq;

    ret = request_irq(my_dev.irq, my_irq_handler,
                      flags, DEV_NAME, &my_dev);
    if (ret) {
        dev_err(&pdev->dev, "Failed to request IRQ %d\n", my_dev.irq);
        return ret;
    }
    return 0;
}

static int my_remove(struct platform_device *pdev)
{
    free_irq(my_dev.irq, &my_dev);
    return 0;
}

3.4 中断注册与注销流程架构

下图展示了从驱动 probe 到 remove 的完整中断生命周期,以及关键检查点:

中断注册与注销流程架构图 中断注册与注销生命周期 probe() 获取中断号 request_irq() 注册 检查 /proc/interrupts 中断处理运行中 handler 执行 / 线程化 remove() 触发注销 free_irq() 注销 确认 dev 参数匹配 注册失败:返回负值 检查 flags 与硬件匹配 关键检查清单: 1. 中断号来源:优先使用 platform_get_irq(),避免硬编码 2. flags 组合:触发方式 + 共享标志 + 电源管理标志 3. 注销时机:在 remove() 中调用,禁止在 handler 内调用

3.5 共享中断的注册与识别机制

当多个设备共享同一中断线时,内核会依次调用所有注册的处理函数,直到某个函数返回 IRQ_HANDLED。驱动必须通过 dev 参数判断中断是否属于自己的设备:

static irqreturn_t shared_irq_handler(int irq, void *dev_id)
{
    struct my_device *dev = (struct my_device *)dev_id;

    // 读取硬件状态寄存器,判断是否本设备产生中断
    if (!(readl(dev->reg_base + STATUS_REG) & INT_PENDING))
        return IRQ_NONE;  // 不是本设备的中断

    // 处理中断
    writel(INT_CLEAR, dev->reg_base + CLEAR_REG);
    return IRQ_HANDLED;
}
共享中断调试要点:
  • 所有共享中断的驱动必须设置 IRQF_SHARED 标志,否则注册会失败。
  • 处理函数必须检查硬件状态,不能假设每次调用都是自己的中断。
  • 使用 cat /proc/interrupts 查看各设备的中断计数,辅助定位共享冲突。

3.6 中断标志位配置故障定位表

以下表格总结了 flags 配置不当导致的典型故障现象及排查方向:

故障现象 可能原因 排查方向
中断从未触发 触发方式与硬件不匹配(如边沿 vs 电平) 检查硬件手册,确认实际触发极性
中断频繁触发导致系统卡死 电平触发未设置 IRQF_ONESHOT,导致嵌套 添加 IRQF_ONESHOT 或改用线程化中断
共享中断注册失败 部分驱动未设置 IRQF_SHARED 检查所有共享该中断号的驱动代码
系统休眠后无法唤醒 唤醒中断未设置 IRQF_NO_SUSPEND 为唤醒源中断添加 IRQF_NO_SUSPEND
注销时系统崩溃 dev 参数不匹配或重复注销 确保 free_irq 的 dev 与注册时一致

3.7 实战:完整的中断注册与注销模板

以下代码展示了一个符合内核编码规范的中断驱动模板,包含错误处理、资源释放和调试输出:

#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/interrupt.h>
#include <linux/io.h>

struct irq_drv_data {
    int irq;
    void __iomem *regs;
    char name[32];
};

static irqreturn_t irq_drv_handler(int irq, void *dev_id)
{
    struct irq_drv_data *data = dev_id;
    u32 status;

    status = readl(data->regs + 0x00);
    if (!(status & 0x1))
        return IRQ_NONE;

    writel(0x1, data->regs + 0x04);  // 清除中断
    pr_info("%s: interrupt handled\n", data->name);
    return IRQ_HANDLED;
}

static int irq_drv_probe(struct platform_device *pdev)
{
    struct irq_drv_data *data;
    struct resource *res;
    int ret;

    data = devm_kzalloc(&pdev->dev, sizeof(*data), GFP_KERNEL);
    if (!data)
        return -ENOMEM;

    data->irq = platform_get_irq(pdev, 0);
    if (data->irq < 0)
        return data->irq;

    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    data->regs = devm_ioremap_resource(&pdev->dev, res);
    if (IS_ERR(data->regs))
        return PTR_ERR(data->regs);

    snprintf(data->name, sizeof(data->name), "irq_drv.%d", pdev->id);

    ret = request_irq(data->irq, irq_drv_handler,
                      IRQF_TRIGGER_HIGH | IRQF_SHARED,
                      data->name, data);
    if (ret) {
        dev_err(&pdev->dev, "request_irq failed: %d\n", ret);
        return ret;
    }

    platform_set_drvdata(pdev, data);
    dev_info(&pdev->dev, "registered IRQ %d\n", data->irq);
    return 0;
}

static int irq_drv_remove(struct platform_device *pdev)
{
    struct irq_drv_data *data = platform_get_drvdata(pdev);

    free_irq(data->irq, data);
    dev_info(&pdev->dev, "freed IRQ %d\n", data->irq);
    return 0;
}

static struct platform_driver irq_drv_driver = {
    .probe  = irq_drv_probe,
    .remove = irq_drv_remove,
    .driver = {
        .name = "irq_drv_example",
    },
};

module_platform_driver(irq_drv_driver);
MODULE_LICENSE("GPL");
调试验证: 加载驱动后执行 cat /proc/interrupts | grep irq_drv,确认中断号已注册且计数正常。触发硬件中断后观察计数是否增加,若未增加则检查 flags 中的触发方式配置。