4、中断共享:多个设备共享同一中断号时的驱动设计策略
嵌入式系统里,中断线是稀缺资源。尤其汽车电子,一个 SoC 可能挂了几十个外设,但中断控制器能给你的中断号就那么几十个。怎么办?共享中断。
说白了,就是多个设备共用一根中断线、同一个中断号。听起来简单,做起来坑不少。我最早接触这个是在一个车载信息娱乐项目里,触摸屏和加速度传感器共用了同一个 GPIO 中断。当时年轻,没仔细看芯片手册,结果一碰屏幕系统就死机……嗯,从那以后我对共享中断就特别上心。
4.1 共享中断的硬件基础
不是所有中断控制器都支持共享。你得先确认硬件支不支持这个玩法。
| 中断控制器类型 | 是否支持共享 | 典型场景 |
|---|---|---|
| GPIO 中断(边沿触发) | 不支持 | 按键、传感器 |
| GPIO 中断(电平触发) | 支持 | 多设备共享一根 GPIO 线 |
| GIC(通用中断控制器) | 支持 | ARM SoC 标准外设 |
| 级联中断控制器 | 视具体实现 | 复杂多核系统 |
关键点:边沿触发不能共享。为什么?因为边沿触发只产生一个脉冲,如果两个设备同时触发,中断控制器只能捕获一个边沿,另一个就丢了。电平触发就没这个问题——只要电平还在,中断就一直挂着,直到所有设备都处理完。
4.2 Linux 共享中断的注册机制
Linux 内核里,共享中断通过 request_irq() 的最后一个参数 irqflags 来控制。关键标志是 IRQF_SHARED。
// 共享中断注册示例
int dev1_irq;
int ret;
// 设备1注册共享中断
ret = request_irq(irq_num, dev1_interrupt_handler,
IRQF_SHARED | IRQF_TRIGGER_HIGH,
"dev1", &dev1_data);
if (ret) {
pr_err("dev1: failed to request shared irq %d\n", irq_num);
return ret;
}
// 设备2注册同一个中断号
ret = request_irq(irq_num, dev2_interrupt_handler,
IRQF_SHARED | IRQF_TRIGGER_HIGH,
"dev2", &dev2_data);
if (ret) {
pr_err("dev2: failed to request shared irq %d\n", irq_num);
free_irq(irq_num, &dev1_data);
return ret;
}
这里有个细节:dev_id 参数(最后一个)在共享中断里必须唯一。内核靠它来区分不同的设备。我习惯用设备结构体指针作为 dev_id,既唯一又方便在中断处理里拿到设备上下文。
4.3 中断处理函数的编写要点
共享中断的处理函数和普通中断不一样。你不能假设每次触发都是你的设备。必须做两件事:
- 检查设备是否真的产生了中断——读设备的中断状态寄存器
- 如果不是你的设备,立即返回 IRQ_NONE
static irqreturn_t dev1_interrupt_handler(int irq, void *dev_id)
{
struct dev1_data *data = (struct dev1_data *)dev_id;
u32 status;
// 第一步:读设备中断状态
status = readl(data->base + INT_STATUS_REG);
if (!(status & DEV1_INT_MASK)) {
// 不是我的中断,返回 IRQ_NONE
return IRQ_NONE;
}
// 第二步:清除中断标志
writel(DEV1_INT_MASK, data->base + INT_CLEAR_REG);
// 第三步:处理实际中断逻辑
// ... 你的业务代码 ...
// 第四步:返回 IRQ_HANDLED
return IRQ_HANDLED;
}
4.4 共享中断的驱动设计策略
我总结了几条实战策略,都是踩过坑换来的:
策略一:中断处理尽量轻量
共享中断的触发频率可能是所有设备的总和。你的处理函数必须快。把耗时操作扔进 tasklet 或 workqueue。
static irqreturn_t dev_shared_handler(int irq, void *dev_id)
{
struct my_dev *dev = (struct my_dev *)dev_id;
if (!check_my_interrupt(dev))
return IRQ_NONE;
// 只做必要的事:调度下半部
tasklet_schedule(&dev->tasklet);
return IRQ_HANDLED;
}
策略二:中断标志清除时机
电平触发的中断,必须在处理完所有设备后再清除中断标志。如果先清标志再处理,另一个设备的中断可能被误清。
策略三:设备优先级排序
内核遍历共享中断的处理函数链表时,按注册顺序调用。把实时性要求高的设备放在前面注册。
| 设备类型 | 注册顺序 | 原因 |
|---|---|---|
| CAN 控制器 | 第1 | 实时性要求最高 |
| GPIO 按键 | 第2 | 用户交互不能卡顿 |
| 温度传感器 | 第3 | 慢速设备,可以等 |
4.5 共享中断的调试与排查
共享中断出问题,最常见的现象是:系统莫名其妙卡死,或者某个设备完全没反应。
我的排查步骤:
- 看 /proc/interrupts——确认每个设备的中断计数是否正常增长
- 检查中断触发方式——是不是用了边沿触发?赶紧改电平触发
- 确认 dev_id 唯一性——两个设备用了同一个指针?内核会报错
- 加调试打印——在中断处理函数入口打印设备名,看谁在抢中断
# 查看中断统计
cat /proc/interrupts | grep shared_irq_num
# 输出示例:
# 42: 12345 67890 GIC 42 dev1, dev2
# ^^ ^^^^ ^^^^
# 中断号 dev1计数 dev2计数
如果 dev1 的计数一直不涨,说明它的中断被 dev2 吃掉了。这时候检查 dev2 的处理函数是不是忘了返回 IRQ_NONE。
4.6 共享中断的架构图
下面这张图展示了共享中断的完整流程。从硬件触发到内核分发,再到各个设备处理函数。
4.7 共享中断的注意事项
- 不要用 spin_lock_irqsave 锁共享资源——中断处理函数里如果自旋锁持有时间过长,其他设备的中断会被阻塞。用 spin_lock 或 raw_spin_lock 替代。
- 注意中断嵌套——共享中断里,一个设备的中断处理可能被另一个设备的中断打断。如果你的设备有共享资源,记得加锁。
- 卸载驱动时顺序释放——先注册的要后释放。内核在 free_irq 时会检查 dev_id 是否匹配,顺序错了会报错。
共享中断说难不难,说简单也不简单。核心就三点:电平触发、检查设备状态、快速返回。把这三点记牢了,大部分共享中断的问题都能搞定。剩下的,就是多读芯片手册,多看 /proc/interrupts,多写测试代码。嗯,实践出真知。