1. 概述
在驱动开发中,当我们需要实时响应外部硬件事件时,你会选择哪种方式?比如:
- 用户按下了某个物理按键。
- 传感器发出了数据就绪信号。
- 硬件模块完成了某项传输任务。
除了简单但低效的轮询(Polling),我们还有另一种更强大、更高效的武器——中断(Interrupt)。本文将以GPIO按键为例,带你从零开始,一步一步构建一个完整的中断驱动程序,掌握从设备树描述到驱动代码实现的全流程。
中断本质上是一种异步触发机制。它允许CPU暂时中断当前正在执行的任务,优先去处理一个更高优先级的突发硬件事件。整个过程涉及到“保存现场”和“恢复现场”,确保处理完中断后,CPU能无缝衔接回原来的工作。
中断处理流程:

在Linux内核驱动开发中,我们需要熟悉以下几个关键的中断概念:
| 概念 |
全称 |
含义 |
| IRQ Number |
Interrupt Request Number |
中断号,内核识别中断的唯一ID,由硬件连接或设备树映射生成。 |
| ISR |
Interrupt Service Routine |
中断服务程序,中断发生时被执行的驱动注册函数。 |
| 顶半部 / 底半部 |
Top/Bottom Half |
顶半部(ISR)负责快速响应硬件,底半部(Tasklet/Workqueue)负责处理耗时逻辑。 |
| 触发方式 |
Trigger Type |
上升沿(Rising)、下降沿(Falling)、高/低电平(Level)。 |
⚠️ 注意
在中断上下文(Interrupt Context)中,绝对不能执行休眠、调度或耗时的操作(比如 msleep、mutex_lock 等)。记住,快进快出是中断顶半部设计的铁律!
2. 设备树
要在驱动中使用中断,我们首先需要在设备树中清晰地描述硬件连接状态。以下是一个GPIO按键的设备树节点示例:
key_input_node {
compatible = "abc,key-input";
/* 指定 GPIO 引脚,GPIO_ACTIVE_LOW 表示按下时物理电平为低 */
gpios = <&gpio2 4 GPIO_ACTIVE_LOW>;
/* 描述中断信息,interrupt-parent 指定中断控制器 */
interrupt-parent = <&gpio2>;
/* 4 是 GPIO 引脚号,IRQ_TYPE_EDGE_FALLING 表示下降沿触发 */
interrupts = <4 IRQ_TYPE_EDGE_FALLING>;
};
说明:
gpios 属性是驱动获取GPIO对象的关键。而 interrupts 属性则用于显式声明中断。实际上,对于GPIO中断,内核提供了 gpiod_to_irq() 接口,因此设备树中可以省略 interrupts 属性,直接通过 gpios 属性进行映射,这种方式更为常用和简洁。
3. 驱动代码
我们将遵循标准的Platform驱动框架,并使用 devm_ 系列函数来管理资源。这样做的好处是,在驱动卸载时,内核会自动帮我们释放相关资源,避免了手动管理的繁琐和可能的内存泄漏。
驱动初始化流程:

3.1 核心实现
#include <linux/module.h>
#include <linux/platform_device.h>
/* 中断相关接口 */
#include <linux/interrupt.h>
/* GPIO descriptor(gpiod_)接口 */
#include <linux/gpio/consumer.h>
/* 中断服务函数 (ISR) - 顶半部
* 注意:这里不能执行耗时操作!
*/
static irqreturn_t key_irq_handler(int irq, void *dev_id)
{
/* 在实际项目中,可以根据需要在这里唤醒底半部 */
pr_info("Key Pressed! IRQ Number: %d\n", irq);
/* 告诉内核中断已被正确处理 */
return IRQ_HANDLED;
}
static int key_input_probe(struct platform_device *pdev)
{
int irq;
int ret;
struct gpio_desc *key_gpio;
struct device *dev = &pdev->dev;
pr_info("Key Input Driver Probing...\n");
/* 1. 获取 GPIO 对象 (根据 DTS 中的 compatible 和 gpios 匹配) */
key_gpio = devm_gpiod_get(dev, NULL, GPIOD_IN);
if (IS_ERR(key_gpio)) {
dev_err(dev, "Failed to get GPIO\n");
return PTR_ERR(key_gpio);
}
/* 2. 将 GPIO 转换为对应的 IRQ 号 */
irq = gpiod_to_irq(key_gpio);
if (irq < 0) {
dev_err(dev, "Failed to map GPIO to IRQ\n");
return irq;
}
pr_info("Mapped GPIO to IRQ: %d\n", irq);
/* 3. 申请中断
* 参数说明:
* - dev: 设备对象
* - irq: 中断号
* - handler: 中断服务函数
* - flags: 触发标志
* - name: /proc/interrupts 节点中显示的名字
* - dev_id: 传递给 ISR 的私有数据 (通常传 device 结构体,这里演示用 NULL)
*/
ret = devm_request_irq(dev, irq, key_irq_handler,
IRQF_TRIGGER_FALLING, "abc_key_irq", NULL);
if (ret) {
dev_err(dev, "Failed to request IRQ: %d\n", ret);
return ret;
}
return 0;
}
3.2 驱动注册
static const struct of_device_id key_input_of_match[] = {
{ .compatible = "abc,key-input" },
{ }
};
MODULE_DEVICE_TABLE(of, key_input_of_match);
static struct platform_driver key_input_driver = {
.probe = key_input_probe,
.driver = {
.name = "key_input",
.of_match_table = key_input_of_match,
},
};
module_platform_driver(key_input_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("dump linux");
MODULE_DESCRIPTION("Simple GPIO Interrupt Driver");
4. 测试与验证
驱动代码编写完成后,下一步就是加载测试,验证中断是否成功注册并正常工作。
4.1 加载驱动
sudo insmod key_input.ko
如果 dmesg 中看到 Key Input Driver Probing... 和 Mapped GPIO to IRQ: ... 等成功信息,说明驱动加载成功。
4.2 查看系统中断表
/proc/interrupts 是调试中断最有效的工具之一,它统计了内核中所有已注册中断的信息。
cat /proc/interrupts | grep abc_key_irq
输出示例:
# 149:软件中断号,0:在某个 CPU 上的触发次数,Edge:触发方式
149: 0 0 0 0 0 0 0 0 GICv3 4 Edge abc_key_irq
4.3 触发测试
- 执行
cat /proc/interrupts | grep abc_key_irq 记录下当前触发次数。
- 按下对应的GPIO按键。
- 查看
dmesg,你应该能看到 Key Pressed! IRQ Number: ... 的输出。
- 再次执行
cat /proc/interrupts | grep abc_key_irq,对比观察中断触发次数是否增加了。
5. 调试
在实际开发中,你可能会遇到一些问题。下面这张表汇总了一些常见现象和排查思路:
| 现象 |
可能原因 |
排查手段 |
Probe失败,gpiod_to_irq() < 0 |
GPIO控制器不支持中断或设备树配置错误。 |
检查芯片手册GPIO相关说明;检查设备树interrupt-parent属性。 |
| 驱动注册成功,但按键按下无反应 |
1. 没有触发电平变化。 2. 引脚复用未正确配置。 |
1. 测量物理引脚电平状态。 2. 检查设备树pinctrl设置。 |
| 按一次按键,中断触发很多次 |
按键抖动。 |
硬件上并联电容或软件去抖(例如在中断处理中增加去抖逻辑)。 |
| 系统卡死 |
中断顶半部中使用了导致阻塞的函数。 |
严禁在中断顶半部中睡眠,检查ISR中是否调用了msleep, mutex_lock等可能导致调度的函数。 |
6. 总结
中断是驱动开发中的核心机制,相比于轮询,它能极大提升系统的实时性和响应效率。几乎所有的外设交互都离不开中断的参与,因此,掌握中断驱动开发是内核驱动开发者的必修课。
本文通过一个GPIO按键的实例,详细展示了从设备树配置、驱动代码编写到测试调试的全过程。关键点在于理解“快进快出”的顶半部设计原则,以及何时、如何将耗时任务合理地调度到底半部(如Tasklet或Workqueue)中去执行。希望这篇实践指南能帮助你在Linux内核驱动的道路上走得更稳、更远。如果你想深入学习更多网络/系统相关的内核知识,欢迎来云栈社区交流探讨。