文章目录
-
- 前言
- 一、中断
-
- [1.1 中断的概念](#1.1 中断的概念)
- [1.2 中断相关函数接口](#1.2 中断相关函数接口)
-
- [1.2.1 中断相关](#1.2.1 中断相关)
- [1.2.2 中断号相关](#1.2.2 中断号相关)
- [1.2.3 查看中断](#1.2.3 查看中断)
- [1.3 操作步骤](#1.3 操作步骤)
-
- [1.3.0 配置中断前](#1.3.0 配置中断前)
- [1.3.1 适配设备树](#1.3.1 适配设备树)
- [1.3.2 从设备树中获得中断号](#1.3.2 从设备树中获得中断号)
- [1.3.3 申请中断](#1.3.3 申请中断)
- [1.3.4 编写中断处理函数](#1.3.4 编写中断处理函数)
- [1.3.5 修改应用层函数app](#1.3.5 修改应用层函数app)
- 二、等待队列
-
- [2.1 等待队列的概念](#2.1 等待队列的概念)
- [2.2 等待队列的相关函数接口](#2.2 等待队列的相关函数接口)
- [2.3 实现步骤](#2.3 实现步骤)
-
- [2.3.1 定义等待队列](#2.3.1 定义等待队列)
- [2.3.2 初始化等待队列](#2.3.2 初始化等待队列)
- [2.3.3 注册中断](#2.3.3 注册中断)
- [2.3.4 中断服务函数](#2.3.4 中断服务函数)
- [2.3.5 等待事情触发](#2.3.5 等待事情触发)
- [2.3.6 中断到来](#2.3.6 中断到来)
- 三、中断子系统
-
- [3.1 中断子系统相关概念](#3.1 中断子系统相关概念)
- [3.2 中断下半部的实现方式](#3.2 中断下半部的实现方式)
-
- [3.2.1 软中断](#3.2.1 软中断)
- [3.2.2 tasklet](#3.2.2 tasklet)
- [3.2.3 工作队列](#3.2.3 工作队列)
- [3.3 总结](#3.3 总结)
-
- [3.3.1 顶半部(Top Half)](#3.3.1 顶半部(Top Half))
- [3.3.2 软中断(Softirq)](#3.3.2 软中断(Softirq))
- [3.3.3 tasklet](#3.3.3 tasklet)
- [3.3.4 workqueue(工作队列)](#3.3.4 workqueue(工作队列))
- [3.4 Linux三种内核上下文](#3.4 Linux三种内核上下文)
-
- [3.4.1 硬件中断上下文(Hardirq 顶半部)](#3.4.1 硬件中断上下文(Hardirq 顶半部))
- [3.4.2 软中断上下文(Softirq / Tasklet)](#3.4.2 软中断上下文(Softirq / Tasklet))
- [3.4.3 内核线程上下文](#3.4.3 内核线程上下文)
- [3.5 相关函数](#3.5 相关函数)
-
- [3.5.1 tasklet](#3.5.1 tasklet)
- [3.5.2 工作队列](#3.5.2 工作队列)
- [3.6 按键源码示例](#3.6 按键源码示例)

前言
在上篇文章【Linux驱动开发】pincrtl子系统与gpio子系统中,我们学习了Linux驱动开发的三大思想,以及pincrtl子系统与gpio子系统。文章中以led为示例,在最后让大家通过pincrtl子系统和gpio子系统来实现按键驱动,若使用打印来表示按键是否按下,会发现在交互界面会一直打印文字,这就是轮询 的非阻塞的按键 。在这片文章中会介绍中断概念 、等待队列 以及中断子系统 ,先通过中断、等待队列 来实现阻塞的按键驱动 ,再介绍中断子系统以及通过中断子系统来实现按键驱动。
一、中断
1.1 中断的概念
中断是指CPU在执行程序的过程中,出现了某些突发事件急待处理, CPU必须暂停当前程序的执行,转去处理突发事件,处理完毕后又返回原程序被中断的位置继续执行。中断可以分为: 内部中断 和外部中断 ,内部中断来源于CPU内部,外部中断来源自CPU外部。可以通过指令/proc/interrupts查看中断相关信息(用来处理异步事件,随机事件,非顺序逻辑的情况)。
1.2 中断相关函数接口
1.2.1 中断相关
| 函数名 | 功能 |
|---|---|
| request_irq | 注册中断处理程序,绑定中断号与处理函数 |
| free_irq | 释放已注册的中断资源 |
| disable_irq | 禁止中断 |
| disable_irq_nosync | 禁止中断(立即返回不等待中断处理结束) |
| enable_irq | 使能中断 |
| local_irq_save | 禁用当前CPU所有中断,保存中断状态, 适用多cpu |
| loca_irq_disable | 禁止当前CPU所有中断 适用多cpu |
| local_irq_restore | 恢复保存的中断状态 适用多cpu |
| devm_request_irq | 自动管理资源的中断注册函数 |
1.2.2 中断号相关
| 函数名 | 功能 | 备注 |
|---|---|---|
| of_irq_get | 获得设备树中第一个中断号 | |
| gpio_to_irq | 通过GPIO编号获得中断号映射。通过已经获得的gpio号,在获得中断号 | 返回值是获得中断号 |
| request_irq | 申请中断,绑定处理函数,最后一个传递中断号,传递IRQF_SHARED可使用 | linux/interrupt.h |
| free_irq | 释放终端资源 | 参数要和申请时一致,否则可能出错 |
| enable_irq | 使能这个中断 | 和资源竞争有关 |
| disable_irq | 暂时性关闭这个 这个中断 | free_irq前,建议调用 |
注:加粗函数为较常用的函数接口。
1.2.3 查看中断
通过指令cat /proc/interrupts我们可以在交互界面查看中断:

1.3 操作步骤
在/home/linux/imx6ull/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/arch/arm/boot/dts中

interrupts = <GIC_SPI 66 IRQ_TYPE_LEVEL_HIGH>::在设备树中已经将GPIO1族的中断号与GIC关联上了;
interrupt-controller; :可以作为其他 父中断控制器。
interrupts:需要描述引脚号,上升触发,下降触发,双边沿触发(IRQ_TYPE_EDGE_RISING, IRQ_TYPE_EDGE_FALLING , IRQ_TYPE_EDGE_BOTH);
因此使用中断的第一步就是设备树。
1.3.0 配置中断前
在配置中断前,需要创建注册好设备,并获取节点,获得并申请gpio:
c
int ret = 0;
// 设备号的申请(10),自动创建设备节点,杂项设备,属于字符设备的一种
ret = misc_register(&misc_dev);
if (ret < 0)
{
pr_err("misc_register error\n");
return ret;
}
// 从设备树中,找到对应的节点。
pnode = of_find_node_by_path("/putekey");
if (!pnode)
{
pr_err("of_find_node_by_path error\n");
ret = -ENODEV;
goto del_misc;
}
key_gpio = of_get_named_gpio(pnode, "gpio-key", 0);
if (key_gpio < 0)
{
pr_err("of_get_named_gpio error\n");
ret = -ENODEV;
goto del_pnode;
}
ret = gpio_request(key_gpio, "key");
if (ret)
{
pr_err("gpio_request error\n");
goto del_pnode;
}
gpio_direction_input(key_gpio);
gpio_set_value(key_gpio, 1);
1.3.1 适配设备树

在原有的按键设备树基础上,添加进这两行关于中断的代码。
1.3.2 从设备树中获得中断号
使用指令of_irq_get(pnode, 0)从节点中获取中断号:

1.3.3 申请中断
调用request_irq();,申请中断,填入中断处理函数:

1.3.4 编写中断处理函数
在这个函数中,写的就是中断到来后需要做的事。
注意:需要注意返回值;
c
enum irqreturn {
IRQ_NONE , //转给下一个注册的中断处理函数, 表示这个不是本驱动要处理的中断, 最终都没有驱动处理, 内核报 "nobody cared"。 因为中断处理的时候, 有可能中断号共享的状态。
IRQ_HANDLED , //大部分, 用这个。 表示中断正常处理了。 内核不再继续传递,
IRQ_WAKE_THREAD , //唤醒 threaded irq 线程去处理, 需要进一步处理
};
1.3.5 修改应用层函数app
将main函数while(1){}中的输出删去,因为在中断处理函数中,加入了输出。
但是在时候,我们会发现呈现的效果依旧是非阻塞的按键。因此,要实现阻塞按键的效果,我们可以通过等待队列来实现,在下一章节将会解释。
二、等待队列
2.1 等待队列的概念
等待队列是内核用于实现阻塞 / 睡眠机制的数据结构。当进程需要等待某个条件成立(等待硬件就绪、等待数据、等待信号)时,进程放弃 CPU,进入休眠,挂入等待队列;等条件满足后,由别的代码(中断 / 其他进程)唤醒队列上的进程,进程重新进入可运行状态。
2.2 等待队列的相关函数接口
| 函数名 | 功能 |
|---|---|
| init_waitqueue_head | 初始化等待队列头 初始化 |
| DECLARE_WAIT_QUEUE_HEAD | 定义 等待队列头,定义+初始化 |
| DECLARE_WAITQUEUE | 定义等待队列元素 |
| add_wait_queue | 添加等待队列 |
| remove_wait_queue | 移除等待队列 |
| wait_event | 等待事件(不可打断),ctrl+c也不行 |
| wait_event_interruptible | 等待事件(可以被打断) |
| wait_event_timeout | 等待事件(设定超时时间) |
| wait_event_interruptible_timeout | 等待事件(设定超时时间,可以被信号打断) |
| wake_up | 唤醒事件 |
| wake_up_interruptible | 唤醒事件(可以被信号打断) |
2.3 实现步骤
在完成设备创建、GPIO获得申请的基础上继续;
2.3.1 定义等待队列
通过DECLARE_WAIT_QUEUE_HEAD指令定义一个等待队列,并设置一个 int flag =0;(相当于条件变量):

2.3.2 初始化等待队列
使用函数init_waitqueue_head初始化等待队列:

2.3.3 注册中断
注册中断处理,可以唤醒等待的事件:

2.3.4 中断服务函数
在中断服务函数中调用等待队列唤醒函数,在中断触发后,等待队列将会被唤醒:

2.3.5 等待事情触发
先flag=0, 在调用 wait_event_interruptible,一般是在需要阻塞的地方。 按键的话,就是read函数中:

2.3.6 中断到来
中断到来,在中断处理函数中调用 wake_up_interruptible,调用函数前flag=1 。唤醒阻塞事件。
注意这个flag 的标志为,置0 ,置1的时机。
这样就实现了阻塞的按键驱动。
三、中断子系统
3.1 中断子系统相关概念
中断会打断内核进程正常调度和运行,所以系统对高吞吐量的追求势必要求中断服务尽量短小精悍,快进快出,所以Linux中断处理程序架构分成了上半部与下半部:

这是为了在中断执行时间尽量短和中断处理需完成的工作尽量大之间找到一个平衡点,两个半部: 顶半部(Top Half) 和底半部(BottomHalf) 。
上半部 :也称为中断顶半部 ,用于完成尽量少的比较紧急的功能,通常被设置为不可被新中断打断。
下半部 :也称为中断底半部 ,用来完成中断事件绝大多数任务,可以被新的中断打断 ,这也是底半部和顶半部的最大不同,因为顶半部往往被设计成不可中断。底半部相对来说并不是非常紧急,而且相对比较耗时的操作。
上半部的使用场景:
- 要处理的内容不希望被其他中断打断
- 要处理的任务对时间敏感
- 要处理的任务与硬件有关
下半部的使用场景:
- 完成并不是非常紧急的耗时的任务
- 可以被打断
3.2 中断下半部的实现方式
有softirq,tasklet,workque这三种方式实现。
3.2.1 软中断
资源比较有限,用于系统级的任务:
c
#include/linux/interrupt.h
/* 系统中,使用软中断 */
enum
{
HI_SOFTIRQ=0, // 高优先级软中断
TIMER_SOFTIRQ, // 定时器软中断
NET_TX_SOFTIRQ, // 网络发送软中断
NET_RX_SOFTIRQ, // 网络接收软中断
BLOCK_SOFTIRQ, // 块设备软中断
BLOCK_IOPOLL_SOFTIRQ,// 块设备IO轮询软中断
TASKLET_SOFTIRQ, // Tasklet软中断(tasklet基于该软中断实现)
SCHED_SOFTIRQ, // 调度软中断
HRTIMER_SOFTIRQ, // 高精度定时器软中断
RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */ // RCU软中断,RCU建议放在最后
NR_SOFTIRQS // 软中断类型总数
};
软中断 是一种传统的底半部处理机制 ,它的执行时机通常是顶半部返回 的时候;
软中断 和tasklet运行于中断上下文 ,不允许睡眠。
3.2.2 tasklet
tasklet是利用软中断来实现的,软中断调度tasklet;
tasklet不允许睡眠。
3.2.3 工作队列
工作队列运行于进程上下文 ,允许睡眠。
3.3 总结
| 机制 | 运行环境 | 能否休眠 | 调度时机 | 并行性 | 用途 / 场景 |
|---|---|---|---|---|---|
| 顶半部 | 中断上下文 | ❌ 不能 | 立即执行 | 独占CPU | 必须紧急处理的硬件操作 |
| 软中断 | 中断上下文 | ❌ 不能 | 中断返回前 | SMP 多核并行 | 网络、块设备、高精度定时器 |
| tasklet | 中断上下文 | ❌ 不能 | 中断返回前 | 同类型不能并行 | 驱动最常用:轻量底半部 |
| workqueue | 进程上下文 | ✅ 能 | 内核线程调度 | 完全并行 | 能休眠、耗时操作 |
3.3.1 顶半部(Top Half)
- 就是 handler()处理函数中,硬中断上下文
- 特点: 最快、最急、不能休眠
- 不能做:msleep、copy_to_user、mutex、kmalloc
- 只能做:
- 清中断
- 读寄存器
- 唤醒等待队列
- 调度底半部
3.3.2 软中断(Softirq)
- 内核最底层底半部,属于底半部,工作在软中断上下文。执行的时机,顶半部返回的时候(ISR返回的时候)
- 驱动开发者基本不用!,资源有限
- 特点:
- 不能休眠
- 多核可并行
- 性能极高
- 内核自己用:
- 网络收发包
- 块设备IO
- 定时器
- 写的驱动的99.9% 都不需要自定义软中断
3.3.3 tasklet
基于软中断封装,专门给驱动用!
-
最简单、最安全、最常用
-
不能休眠
-
同一个 tasklet 不会在多核上同时跑。属于底半部,工作在软中断上下文,由软中断调度。
-
代码超简单:
cDECLARE_TASKLET(my_tasklet, tasklet_func, data); -
在顶半部中:
ctasklet_schedule(&my_tasklet);
适用场景:
- 按键处理
- GPIO 中断
- 传感器数据读取
- 不需要休眠的快速处理
3.3.4 workqueue(工作队列)
唯一能休眠的底半部!
-
运行在内核线程,工作在底半部,内核线程上下文
-
可以休眠:msleep、mutex、kmalloc、copy_to_user
-
用法简单:
cschedule_work(&work);
适用场景:
- 需要延时
- 需要分配内存
- 需要大量计算
- 需要 I2C/SPI 等总线操作
3.4 Linux三种内核上下文
3.4.1 硬件中断上下文(Hardirq 顶半部)
CPU收到外设硬件电信号,立刻打断当前所有任务,执行request_irq注册的ISR中断服务函数,运行环境为硬件中断上下文。
- 归属:狭义中断上下文,优先级最高
- 载体:独立中断栈,无绑定
task_struct进程 - 对应场景:中断上半部 ISR
3.4.2 软中断上下文(Softirq / Tasklet)
硬中断退出前触发、或空闲时ksoftirqd内核线程调度执行,属于广义中断上下文;tasklet是软中断的封装子集。
- 归属:广义中断上下文,优先级低于硬中断、高于普通进程
- 载体:无专属进程,临时借用 CPU 执行环境
- 对应场景:中断底半部 tasklet、原生 softirq(网卡 / 块设备)
3.4.3 内核线程上下文
进程上下文,Workqueue 归属此类;
内核线程(kthread、events 工作线程)属于纯内核态进程上下文;用户进程系统调用(open/read/write)也属于进程上下文,二者同规则。工作队列 workqueue 完全运行在此环境。
- 归属:进程上下文,调度器统一调度
- 载体:拥有完整
task_struct、内核栈、调度属性 - 对应场景:workqueue、自定义 kthread、驱动文件操作接口
3.5 相关函数
3.5.1 tasklet
tasklet使用比较简单,它的执行上下文是软中断,执行时机通常是顶半部返回的时候
| 函数名 | 功能 |
|---|---|
| DECLARE_TASKLET | 定义TASKLET |
| tasklet_schedule | 使系统在适当的时候进行调度tasklet |
使用步骤:
- 定义tasklet 的回调函数
- DECLARE_TASKLET 通过这个宏定义tasklet,传入回调函数
- 在中断的处理函数handler(ISR),中调用tasklet_schedule。
- tasklet的回调函数,一般认为在handler函数返回后,会被调度。
打印,申请内存,等在内核中,认为都是比较耗时的操作。在顶半部,一般都是不可重入的。
3.5.2 工作队列
工作队列的使用方法与tasklet类似,但是工作队列的执行上下文是内核线程,因此可以调度和睡眠。
| 函数名 | 功能 |
|---|---|
| INIT_WORK | 初始化工作队列 |
| schedule_work | 使系统在适当的时候进行调度队列 |
使用步骤
- 定义一个工作队列
- 定义工作队列的回调
- 使用 INIT_WORK 初始化,传入工作队列的回调函数
- 在handler的回调函数中,调用schedule_work () ;
简单的说:
顶半部: 特别急,不能打断,不能睡眠。
tasklet: 相对急,不能睡眠,可以被高优先级中断,打断
workqueue: 相对佛系,不急的那种。可以休眠,也可以被其他的打断。
3.6 按键源码示例
c
/**
* @file key_drv.c
* @author suaplus
* @brief
gpio1_18
低电平,按键按下
1.这个一个key 的测试的驱动
2.加入自动创建设备节点,不用在mknod
3. 使用misc 方式,编写驱动
4. 加入dts的支持
5. pin+gpio
6. 加入阻塞功能
7. 加入tasklet+workquene(工作队列)
* @version 0.1
* @date 2026-10-09
*
* @copyright Copyright (c) 2026
*
*/
#include "asm-generic/errno-base.h"
#include "asm-generic/gpio.h"
#include "asm/gpio.h"
#include "asm/uaccess.h"
#include "linux/export.h"
#include "linux/fs.h"
#include "linux/interrupt.h"
#include "linux/kdev_t.h"
#include "linux/kernel.h"
#include "linux/of_irq.h"
#include "linux/printk.h"
#include "linux/string.h"
#include "linux/workqueue.h"
#include <linux/init.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <asm/io.h>
#include <linux/device.h>
#include <linux/miscdevice.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_device.h>
#include <linux/of_gpio.h>
#include <linux/wait.h>
#include <linux/sched.h>
#include <linux/workqueue.h>
#define DEV_NAME "key0" // /dev/key0
extern void msleep(unsigned int msecs);
#define BITS_OFFSET 18
static int myopen(struct inode *node, struct file *f);
static int myrelease(struct inode *node, struct file *f);
static ssize_t myread(struct file *f, char __user *data, size_t size, loff_t *off);
static ssize_t mywrite(struct file *f, const char __user *data, size_t size, loff_t *off);
static ssize_t show(struct device *dev, struct device_attribute *attr, char *buf);
static ssize_t store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count);
static void task_handle(unsigned long arg);
void work_handle(struct work_struct *work);
static irqreturn_t myhandler(int, void *);
static struct device_node *pnode = NULL;
static struct device_attribute attr = __ATTR(key_state, 0664, show, store);
static int key_gpio = 0;
static wait_queue_head_t wait_que;
static int wait_que_cond = 0;
static int irq_no = 0;
//定义tasklet
DECLARE_TASKLET(mytasklet, task_handle, 0);
static struct work_struct mywork;
typedef enum
{
MYkey_OFF,
MYkey_ON
} MYkey_STATE;
MYkey_STATE mykey_state;
static struct file_operations fops = {
.owner = THIS_MODULE,
.open = myopen,
.release = myrelease,
.write = mywrite,
.read = myread,
};
static struct miscdevice misc_dev = {
.minor = MISC_DYNAMIC_MINOR, // 动态分配次设备号
.name = DEV_NAME, // 在/dev目录下设备名,-> open
.fops = &fops,
};
// sys 目录下 key状态文件被读出调用
// cat /sys/class/key/key0/key_state
// key_OFF key_ON
ssize_t show(struct device *dev, struct device_attribute *attr, char *buf)
{
if (gpio_get_value(key_gpio))
{
// key up
// key_status = 0;
strcpy(buf, "key_up");
}
else
{
// key press
// key_status = 1;
strcpy(buf, "key_down");
}
return strlen(buf);
}
// sys 目录下 key状态文件被写入调用
// echo "1\n" > /sys/class/key/key0/key_state
// echo "0\n" > /sys/class/key/key0/key_state
ssize_t store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count)
{
pr_info("store call...\n");
return count;
}
static int myopen(struct inode *node, struct file *f)
{
pr_info("myopen call...\n");
return 0;
}
static int myrelease(struct inode *node, struct file *f)
{
pr_info("close call...\n");
return 0;
}
//
void work_handle(struct work_struct *work)
{
//可以打断,可以休眠
wait_que_cond = 1;
wake_up(&wait_que);
msleep(10);
return;
}
//底半部处理
static void task_handle(unsigned long arg)
{
//不会打断,不可休眠
// wait_que_cond = 1;
// wake_up(&wait_que);
// msleep(10);
//线程化处理,可以直接在硬件中断中调用
schedule_work(&mywork);
return;
}
//顶半部处理,不能做耗时处理,不可以被高优先级抢断
irqreturn_t myhandler(int irq_no, void *arg)
{
//需要调度tasklet
tasklet_schedule(&mytasklet);
return IRQ_HANDLED;
}
static ssize_t myread(struct file *f, char __user *data, size_t size, loff_t *off)
{
int ret = 0;
u32 key_status = 0;
wait_que_cond = 0;
wait_event_interruptible(wait_que, wait_que_cond);
if (gpio_get_value(key_gpio))
{
// key up
key_status = 0;
}
else
{
// key press
key_status = 1;
}
ret = copy_to_user(data, &key_status, sizeof(key_status));
if (ret)
{
pr_err(" myread copy_to_user error\n");
return -EFAULT;
}
return sizeof(key_status);
}
// 1 -> key on 0 -> key_off
static ssize_t mywrite(struct file *f, const char __user *data, size_t size, loff_t *off)
{
pr_info("mywrite call...\n");
return size;
}
static int __init mykey_init(void)
{
int ret = 0;
// 设备号的申请(10),自动创建设备节点,杂项设备,属于字符设备的一种
ret = misc_register(&misc_dev);
if (ret < 0)
{
pr_err("misc_register error\n");
return ret;
}
// 从设备树中,找到对应的节点。
pnode = of_find_node_by_path("/putekey");
if (!pnode)
{
pr_err("of_find_node_by_path error\n");
ret = -ENODEV;
goto del_misc;
}
//获取gpio号
key_gpio = of_get_named_gpio(pnode, "gpio-key", 0);
if (key_gpio < 0)
{
pr_err("of_get_named_gpio error\n");
ret = -ENODEV;
goto del_pnode;
}
//申请使用gpio
ret = gpio_request(key_gpio, "key");
if (ret)
{
pr_err("gpio_request error\n");
goto del_pnode;
}
INIT_WORK(&mywork, work_handle);
gpio_direction_input(key_gpio);
gpio_set_value(key_gpio, 1);
// 初始化 等待队列
init_waitqueue_head(&wait_que);
// 从设备树中,取出 中断号
irq_no = of_irq_get(pnode, 0);
if (irq_no < 0)
{
pr_err("of_irq_get error\n");
ret = irq_no;
goto del_gpio;
}
ret = request_irq(irq_no, myhandler, IRQF_TRIGGER_RISING, "key_irq", NULL);
if (ret < 0)
{
pr_err("request_irq error\n");
// return ret;
goto del_gpio;
}
ret = device_create_file(misc_dev.this_device, &attr);
if (ret < 0)
{
pr_err("device_create_file error\n");
// return ret;
goto del_irq;
}
pr_info("key drv ok\n");
return 0;
del_irq:
free_irq(irq_no, NULL);
del_gpio:
gpio_free(key_gpio);
del_pnode:
of_node_put(pnode);
del_misc:
misc_deregister(&misc_dev);
return ret;
}
static void __exit key_exit(void)
{
device_remove_file(misc_dev.this_device, &attr);
free_irq(irq_no, NULL);
gpio_free(key_gpio);
of_node_put(pnode);
misc_deregister(&misc_dev);
return;
}
module_exit(key_exit);
module_init(mykey_init);
MODULE_LICENSE("GPL");