Linux 中断驱动程序--按键中断驱动

一、为什么需要中断驱动?

在嵌入式 Linux 开发中,按键是最常见的输入设备之一。如果使用轮询(Polling)方式检测按键状态,CPU 需要不停地读取 GPIO 电平,这会极大地浪费系统资源,导致 CPU 无法处理其他任务。

中断(Interrupt)机制则是解决这一问题的完美方案:

  • 被动响应:CPU 仅在按键按下(或抬起)时才收到通知并进行处理。
  • 高实时性:硬件电平变化触发中断,系统能立即响应。
  • 低功耗:CPU 大部分时间处于空闲或处理其他任务的状态,无需忙等。

二、驱动核心结构

1.代码结构分析

  • 内核驱动 :负责 GPIO 初始化、中断申请、数据读取逻辑。
  • 应用测试 :负责打开设备节点并调用 read() 函数获取按键状态。
  • 设备树节点:描述硬件资源(GPIO 编号、中断触发方式)。

2. 工作流程图

  1. 初始化:驱动加载,解析设备树,申请 GPIO 和中断。
  2. 阻塞等待:用户程序调用 read(),驱动将其加入等待队列并休眠。
  3. 中断触发:按键按下,硬件产生中断,执行中断处理函数。
  4. 唤醒进程:中断处理函数唤醒等待队列中的进程,read() 返回按键值。

三、代码实现详解

1.中断处理函数 (key_irq_handler)
cs 复制代码
static irqreturn_t key_irq_handler(int irq, void * dev) {
    int arg = *(int *)dev; // 获取传入的参数
    if(100 != arg) return IRQ_NONE; // 简单的参数校验
    
    condition = 1; // 核心逻辑:设置条件为真
    wake_up_interruptible(&wq); // 唤醒在 wq 队列上睡眠的进程
    printk("irq = %d dev = %d\n", irq, arg);
    return IRQ_HANDLED;
}
  • 中断上下文原则 :ISR 必须快速执行 。它只做最必要的事情------设置标志位和唤醒进程,避免任何可能引起休眠的操作(如 copy_to_user)。
  • 唤醒机制wake_up_interruptible 是与 wait_event_interruptible 完美配对的函数,它能将处于可中断睡眠状态的进程标记为可运行,由调度器决定何时恢复其执行。
2.. 文件操作:读取函数 (read)

这是驱动与应用交互的接口。

cs 复制代码
static ssize_t read(struct file * file, char __user * buf, size_t size, loff_t * loff) {
    int ret = 0;
    int status = 0;
    
    condition = 0; // 1. 重置条件标志
    wait_event_interruptible(wq, condition); // 2. 进程在此处睡眠,直到 condition != 0
    
    status = 1; // 3. 被唤醒后执行
    ret = copy_to_user(buf, &status, sizeof(status)); // 4. 将数据传回用户空间
    return sizeof(status);
}
  • 阻塞式设计:wait_event_interruptible 宏实现了优雅的阻塞逻辑。当条件不满足时,进程主动让出 CPU;一旦被唤醒,它会自动重新检查条件并继续执行。
    1. 用户/内核空间隔离:必须使用 copy_to_user 将内核数据安全地拷贝到用户空间缓冲区,直接赋值会导致内核崩溃(Oops)。
3.平台驱动探针 (probe)
cs 复制代码
static int probe(struct platform_device * pdev) {
    // 1. 查找设备树节点
    pdts = of_find_node_by_path("/ptkey");
    
    // 2. 获取 GPIO 和 中断号
    key_gpio = of_get_named_gpio(pdts, "ptkey-gpio", 0);
    key_irq = irq_of_parse_and_map(pdts, 0);
    
    // 3. 申请中断
    request_irq(key_irq, key_irq_handler, IRQF_TRIGGER_FALLING, "key0_irq", &arg);
    
    // 4. 初始化等待队列
    init_waitqueue_head(&wq);
    
    return 0;
}
  • 设备树驱动模型:通过 of_find_node_by_path 和 of_get_named_gpio 等 API,驱动代码与具体的硬件地址解耦,使得同一份驱动可以适配不同的硬件平台。
  • 资源生命周期管理:在 probe 中申请的资源(GPIO、IRQ),必须在对应的 remove 函数中正确释放,否则会造成资源泄漏。
4.模块初始化与退出
cs 复制代码
static int __init key_driver_init(void) {
    platform_driver_register(&pdrv); // 注册平台驱动
}
static void __exit key_driver_exit(void) {
    platform_driver_unregister(&pdrv); // 注销平台驱动
}
  • 模块入口/出口:__init 和 __exit 宏告诉内核这些函数只在模块加载/卸载时使用,之后其占用的内存可以被回收,优化了内核内存。
  • 平台总线注册:通过 platform_driver_register 将驱动注册到 Linux 的 Platform 总线上,使其能够被设备树中的匹配节点发现并调用 probe。

四、应用测试:key_app.c

1.主函数逻辑
cs 复制代码
int main(int argc, const char *argv[]) {
    int fd = open("/dev/key", O_RDWR); // 打开设备节点
    int status = 0;
    
    while(1) {
        int ret = read(fd, &status, sizeof status); // 调用驱动的 read 函数
        printf("ret = %d status = %d\n", ret, status);
    }
    
    close(fd);
    return 0;
}
  • 标准文件操作:应用程序完全将设备视为一个普通文件,通过 open、read、close 这些 POSIX 标准接口进行交互,体现了 Linux "一切皆文件" 的设计哲学。
  • 同步阻塞行为:read 调用在此处是同步且阻塞的。程序会一直停在此行,直到内核驱动有数据返回(即按键被按下),这简化了应用层的逻辑,无需自行管理轮询或异步通知。

五、 关键技术点总结

  1. 非轮询机制:相比传统的 while 循环读取 GPIO 电平,这种 中断 + 等待队列 的方式更加高效,不占用 CPU 资源。
  2. 设备树解耦:驱动代码不包含具体的硬件地址,而是通过设备树节点(/ptkey)动态获取 GPIO 和 IRQ,提高了驱动的通用性。
  3. 阻塞 I/O:wait_event_interruptible 和 wake_up_interruptible 是成对使用的宏/函数,是实现阻塞型驱动的标准范式。
  4. 参数传递:代码中使用 &arg 作为中断共享参数,虽然本例中仅用于演示,但在多设备驱动中,通常会传递指向设备私有数据结构(struct key_dev)的指针。
相关推荐
安科士andxe9 小时前
深入解析|安科士1.25G CWDM SFP光模块核心技术,破解中长距离传输痛点
服务器·网络·5g
小白同学_C12 小时前
Lab4-Lab: traps && MIT6.1810操作系统工程【持续更新】 _
linux·c/c++·操作系统os
今天只学一颗糖12 小时前
1、《深入理解计算机系统》--计算机系统介绍
linux·笔记·学习·系统架构
2601_9491465312 小时前
Shell语音通知接口使用指南:运维自动化中的语音告警集成方案
运维·自动化
儒雅的晴天12 小时前
大模型幻觉问题
运维·服务器
Gofarlic_OMS13 小时前
科学计算领域MATLAB许可证管理工具对比推荐
运维·开发语言·算法·matlab·自动化
通信大师13 小时前
深度解析PCC策略计费控制:核心网产品与应用价值
运维·服务器·网络·5g
dixiuapp13 小时前
智能工单系统如何选,实现自动化与预测性维护
运维·自动化
不做无法实现的梦~13 小时前
ros2实现路径规划---nav2部分
linux·stm32·嵌入式硬件·机器人·自动驾驶
Elastic 中国社区官方博客13 小时前
如何防御你的 RAG 系统免受上下文投毒攻击
大数据·运维·人工智能·elasticsearch·搜索引擎·ai·全文检索