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 的完整中断生命周期,以及关键检查点:
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 中的触发方式配置。