mykey2_node {
compatible = "mykey2,key2";
key2-gpio = <&gpx1 1 0>;
interrupt-parent = <&gpx1>;
interrupts = <1 3>;
};
驱动层
A.中断号:每个中断都有一个中断号,通过中断号即可区分不同的中断
B.从设备树获取中断号
unsigned int irq_of_parse_and_map(struct device_node *node, int index);
C.在 Linux 内核中要想使用某个中断是需要申请的,request_irq 函数用于申请中断
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,const char *name, void *dev)
D.中断处理函数:使用 request_irq 函数申请中断的时候需要设置中断处理函数
irqreturn_t (*irq_handler_t) (int, void *)
E.释放中断:void free_irq(unsigned int irq, void *dev_id);
unsigned int irq_of_parse_and_map(struct device_node *node, int index);
/*
功能:获得设备树中的中断号并进行映射
参数:node:设备节点
index:序号
返回值:成功:中断号 失败:错误码
*/
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,const char *name, void *dev)
/*
参数:
irq:所申请的中断号
handler:该中断号对应的中断处理函数
flags:中断触发方式或处理方式
触发方式:IRQF_TRIGGER_NONE //无触发
IRQF_TRIGGER_RISING //上升沿触发
IRQF_TRIGGER_FALLING //下降沿触发
IRQF_TRIGGER_HIGH //高电平触发
IRQF_TRIGGER_LOW //低电平触发
处理方式:
IRQF_DISABLED //用于快速中断,处理中屏蔽所有中断
IRQF_SHARED //共享中断
name:中断名 /proc/interrupts
dev:传递给中断例程的参数,共享中断时用于区分那个设备,一般为对应设备的结构体地址,无共享中断时写NULL
返回值:成功:0 失败:错误码
*/
typedef irqreturn_t (*irq_handler_t)(int, void *);
/*
参数:
int:中断号
void*:对应的申请中断时的dev_id
返回值:
typedef enum irqreturn irqreturn_t; //中断返回值类型
enum irqreturn {
IRQ_NONE = (0 << 0),
IRQ_HANDLED = (1 << 0),
IRQ_WAKE_THREAD = (1 << 1),
};
返回IRQ_HANDLED表示处理完了,返回IRQ_NONE在共享中断表示不处理
*/
void free_irq(unsigned int irq, void *dev_id);
/*
功能:释放中断号
参数:
irq:设备号
dev_id:共享中断时用于区分那个设备一般强转成设备号,无共享中断时写NULL
*/
驱动层按键中断代码实现
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "fs4412_key.h"
int major = 11;
int minor = 0;
int fs4412key2_num = 1;
struct fs4412key2_dev
{
struct cdev mydev;
//gpio引脚
int gpio;
//终端号
int irqno;
//接收按键产生的数据
struct keyvalue data;
//是否有新的数据产生标志位
int newflag;
//中断属于异常上下文所以不可以使用阻塞引起睡眠 应该用自旋锁 实现读阻塞
spinlock_t lock;
wait_queue_head_t rq;
};
struct fs4412key2_dev *pgmydev = NULL;
int fs4412key2_open(struct inode *pnode,struct file *pfile)
{
pfile->private_data =(void *) (container_of(pnode->i_cdev,struct fs4412key2_dev,mydev));
return 0;
}
int fs4412key2_close(struct inode *pnode,struct file *pfile)
{
return 0;
}
ssize_t fs4412key2_read(struct file *pfile,char __user *puser,size_t count,loff_t *p_pos)
{
struct fs4412key2_dev *pmydev = (struct fs4412key2_dev *)pfile->private_data;
int size = 0;
int ret = 0;
//要读取数据的大小要大于按键结构体的大小
if(count < sizeof(struct keyvalue))
{
printk("expect read size is invalid\n");
return -1;
}
spin_lock(&pmydev->lock);
//没数据可读时
if(!pmydev->newflag)
{
if(pfile->f_flags & O_NONBLOCK)
{//非阻塞
//解锁
spin_unlock(&pmydev->lock);
printk("O_NONBLOCK No Data Read\n");
return -1;
}
else
{//阻塞
//解锁
spin_unlock(&pmydev->lock);
//判断是否来数据 不等于就睡眠 等于一就接受数据
ret = wait_event_interruptible(pmydev->rq,pmydev->newflag == 1);
if(ret)
{
printk("Wake up by signal\n");
return -ERESTARTSYS;
}
spin_lock(&pmydev->lock);
}
}
//调整接收数据的大小
if(count > sizeof(struct keyvalue))
{
size = sizeof(struct keyvalue);
}
else
{
size = count;
}
//将数据拷贝到用户空间
ret = copy_to_user(puser,&pmydev->data,size);
if(ret)
{
spin_unlock(&pmydev->lock);
printk("copy_to_user failed\n");
return -1;
}
//拷贝成功将数据表示为变为0
pmydev->newflag = 0;
spin_unlock(&pmydev->lock);
return size;
}
unsigned int fs4412key2_poll(struct file *pfile,poll_table *ptb)
{
struct fs4412key2_dev *pmydev = (struct fs4412key2_dev *)pfile->private_data;
unsigned int mask = 0;
poll_wait(pfile,&pmydev->rq,ptb);
spin_lock(&pmydev->lock);
//有新的数据产生就可以读
if(pmydev->newflag)
{
mask |= POLLIN | POLLRDNORM;
}
spin_unlock(&pmydev->lock);
return mask;
}
struct file_operations myops = {
.owner = THIS_MODULE,
.open = fs4412key2_open,
.release = fs4412key2_close,
.read = fs4412key2_read,
.poll = fs4412key2_poll,
};
//中断处理函数
irqreturn_t key2_irq_handle(int no,void *arg)
{
struct fs4412key2_dev *pmydev = (struct fs4412key2_dev *)arg;
int status1 = 0;
int status2 = 0;
int status = 0;
//读取两次按键gpio管脚 中间间隔1毫秒 按键消抖
status1 = gpio_get_value(pmydev->gpio);
mdelay(1);
status2 = gpio_get_value(pmydev->gpio);
//不相等中断空
if(status1 != status2)
{
return IRQ_NONE;
}
//按键等于0
status = status1;
spin_lock(&pmydev->lock);
if(status == pmydev->data.status)
{
spin_unlock(&pmydev->lock);
return IRQ_NONE;
}
pmydev->data.code = KEY2;
pmydev->data.status = status;
//按键按下有新的数据产生
pmydev->newflag = 1;
spin_unlock(&pmydev->lock);
//唤醒阻塞 把等待队列里的任务唤醒
wake_up(&pmydev->rq);
return IRQ_HANDLED;
}
int __init fs4412key2_init(void)
{
int ret = 0;
dev_t devno = MKDEV(major,minor);
//设备树节点指针
struct device_node *pnode = NULL;
//找出key2的设备节点
pnode = of_find_node_by_path("/mykey2_node");
if(NULL == pnode)
{
printk("find node failed\n");
return -1;
}
pgmydev = (struct fs4412key2_dev *)kmalloc(sizeof(struct fs4412key2_dev),GFP_KERNEL);
if(NULL == pgmydev)
{
printk("kmallc for struct fs4412key2_dev failed\n");
return -1;
}
//从设备树获取gpio编号 可能gpio号包含多个但咱们只有一个所以是0
pgmydev->gpio = of_get_named_gpio(pnode,"key2-gpio",0);
//从设备树获取中断号
pgmydev->irqno = irq_of_parse_and_map(pnode,0);
/*申请设备号*/
ret = register_chrdev_region(devno,fs4412key2_num,"fs4412key2");
if(ret)
{
ret = alloc_chrdev_region(&devno,minor,fs4412key2_num,"fs4412key2");
if(ret)
{
kfree(pgmydev);
pgmydev = NULL;
printk("get devno failed\n");
return -1;
}
major = MAJOR(devno);//容易遗漏,注意
}
/*给struct cdev对象指定操作函数集*/
cdev_init(&pgmydev->mydev,&myops);
/*将struct cdev对象添加到内核对应的数据结构里*/
pgmydev->mydev.owner = THIS_MODULE;
cdev_add(&pgmydev->mydev,devno,fs4412key2_num);
//等待队列头做初始化
init_waitqueue_head(&pgmydev->rq);
//自旋锁初始化
spin_lock_init(&pgmydev->lock);
//中断申请
ret = request_irq(pgmydev->irqno,key2_irq_handle,IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING,"fs4412key2",pgmydev);
//申请失败
if(ret)
{
printk("request_irq failed\n");
cdev_del(&pgmydev->mydev);
//释放动态分配内存
kfree(pgmydev);
//指针指向空
pgmydev = NULL;
//
unregister_chrdev_region(devno,fs4412key2_num);
return -1;
}
return 0;
}
void __exit fs4412key2_exit(void)
{
dev_t devno = MKDEV(major,minor);
//释放中断
free_irq(pgmydev->irqno,pgmydev);
cdev_del(&pgmydev->mydev);
unregister_chrdev_region(devno,fs4412key2_num);
kfree(pgmydev);
pgmydev = NULL;
}
MODULE_LICENSE("GPL");
module_init(fs4412key2_init);
module_exit(fs4412key2_exit);
按键中断应用层代码实现
#include
#include
#include
#include
#include
#include
#include "fs4412_key.h"
int main(int argc,char *argv[])
{
int fd = -1;
struct keyvalue keydata = {0};
int ret = 0;
if(argc < 2)
{
printf("The argument is too few\n");
return 1;
}
fd = open(argv[1],O_RDONLY);
if(fd < 0)
{
printf("open %s failed\n",argv[1]);
return 3;
}
while((ret = read(fd,&keydata,sizeof(keydata))) == sizeof(keydata))
{
if(keydata.status == KEY_DOWN)
{
printf("Key2 is down!\n");
}
else
{
printf("Key2 is up!\n");
}
}
close(fd);
fd = -1;
return 0;
}
Linux 内核将中断分为上半部和下半部的主要目的就是实现中断处理函数的快进快出,那些对时间敏感、执行速度快的操作可以放到中断处理函数中,也就是上半部。剩下的所有工作都可以放到下半部去执行,比如在上半部将数据拷贝到内存中,关于数据的具体处理就可以放到下半部去执行。
1.如果要处理的内容不希望被其他中断打断,那么可以放到上半部。
2.如果要处理的任务对时间敏感,可以放到上半部。
3.如果要处理的任务与硬件有关,可以放到上半部
上半部处理很简单,直接编写中断处理函数就行了,关键是下半部该怎么做呢?Linux 内核提供了多种下半部机制。
1.软中断
2.tasklet
3.工作队列
任务机制
workqueue ----- 内核线程 能睡眠 运行时间无限制
异常机制 ------- 不能睡眠 下半部执行时间不宜太长( < 1s)
软中断 ---- 接口不方便
tasklet ----- 无具体延后时间要求时
定时器 -----有具体延后时间要求时
驱动层
A.定义 taselet
struct tasklet_struct testtasklet;
B.tasklet 处理函数
void testtasklet_func(unsigned long data)
C.中断处理函数中调度 tasklet
tasklet_schedule(&testtasklet);
D. 驱动入口函数中初始化 tasklet
tasklet_init(&testtasklet, testtasklet_func, data);
//初始化tasklet
void tasklet_init(struct tasklet_struct *t,void (*func)(unsigned long), unsigned long data)
//调度tasklet
void tasklet_schedule(struct tasklet_struct *t)
//参数:t:tasklet的结构体
驱动层代码实现
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "fs4412_key.h"
int major = 11;
int minor = 0;
int fs4412key2_num = 1;
struct fs4412key2_dev
{
struct cdev mydev;
int gpio;
int irqno;
struct keyvalue data;
int newflag;
spinlock_t lock;
wait_queue_head_t rq;
//定义tasklet_struct类型的对象
struct tasklet_struct tsk;
};
struct fs4412key2_dev *pgmydev = NULL;
int fs4412key2_open(struct inode *pnode,struct file *pfile)
{
pfile->private_data =(void *) (container_of(pnode->i_cdev,struct fs4412key2_dev,mydev));
return 0;
}
int fs4412key2_close(struct inode *pnode,struct file *pfile)
{
return 0;
}
ssize_t fs4412key2_read(struct file *pfile,char __user *puser,size_t count,loff_t *p_pos)
{
struct fs4412key2_dev *pmydev = (struct fs4412key2_dev *)pfile->private_data;
int size = 0;
int ret = 0;
if(count < sizeof(struct keyvalue))
{
printk("expect read size is invalid\n");
return -1;
}
spin_lock(&pmydev->lock);
if(!pmydev->newflag)
{
if(pfile->f_flags & O_NONBLOCK)
{//非阻塞
spin_unlock(&pmydev->lock);
printk("O_NONBLOCK No Data Read\n");
return -1;
}
else
{//阻塞
spin_unlock(&pmydev->lock);
ret = wait_event_interruptible(pmydev->rq,pmydev->newflag == 1);
if(ret)
{
printk("Wake up by signal\n");
return -ERESTARTSYS;
}
spin_lock(&pmydev->lock);
}
}
if(count > sizeof(struct keyvalue))
{
size = sizeof(struct keyvalue);
}
else
{
size = count;
}
ret = copy_to_user(puser,&pmydev->data,size);
if(ret)
{
spin_unlock(&pmydev->lock);
printk("copy_to_user failed\n");
return -1;
}
pmydev->newflag = 0;
spin_unlock(&pmydev->lock);
return size;
}
unsigned int fs4412key2_poll(struct file *pfile,poll_table *ptb)
{
struct fs4412key2_dev *pmydev = (struct fs4412key2_dev *)pfile->private_data;
unsigned int mask = 0;
poll_wait(pfile,&pmydev->rq,ptb);
spin_lock(&pmydev->lock);
if(pmydev->newflag)
{
mask |= POLLIN | POLLRDNORM;
}
spin_unlock(&pmydev->lock);
return mask;
}
struct file_operations myops = {
.owner = THIS_MODULE,
.open = fs4412key2_open,
.release = fs4412key2_close,
.read = fs4412key2_read,
.poll = fs4412key2_poll,
};
//上半部结束时调度中断处理下半部
irqreturn_t key2_irq_handle(int no,void *arg)
{
struct fs4412key2_dev *pmydev = (struct fs4412key2_dev *)arg;
tasklet_schedule(&pmydev->tsk);
return IRQ_HANDLED;
}
//中断下半部处理函数
void bottom_irq_func(unsigned long arg)
{
struct fs4412key2_dev *pmydev = (struct fs4412key2_dev *)arg;
int status1 = 0;
int status2 = 0;
int status = 0;
status1 = gpio_get_value(pmydev->gpio);
mdelay(1);
status2 = gpio_get_value(pmydev->gpio);
if(status1 != status2)
{
return;
}
status = status1;
spin_lock(&pmydev->lock);
if(status == pmydev->data.status)
{
spin_unlock(&pmydev->lock);
return;
}
pmydev->data.code = KEY2;
pmydev->data.status = status;
pmydev->newflag = 1;
spin_unlock(&pmydev->lock);
wake_up(&pmydev->rq);
return;
}
int __init fs4412key2_init(void)
{
int ret = 0;
dev_t devno = MKDEV(major,minor);
struct device_node *pnode = NULL;
pnode = of_find_node_by_path("/mykey2_node");
if(NULL == pnode)
{
printk("find node failed\n");
return -1;
}
pgmydev = (struct fs4412key2_dev *)kmalloc(sizeof(struct fs4412key2_dev),GFP_KERNEL);
if(NULL == pgmydev)
{
printk("kmallc for struct fs4412key2_dev failed\n");
return -1;
}
pgmydev->gpio = of_get_named_gpio(pnode,"key2-gpio",0);
pgmydev->irqno = irq_of_parse_and_map(pnode,0);
/*申请设备号*/
ret = register_chrdev_region(devno,fs4412key2_num,"fs4412key2");
if(ret)
{
ret = alloc_chrdev_region(&devno,minor,fs4412key2_num,"fs4412key2");
if(ret)
{
kfree(pgmydev);
pgmydev = NULL;
printk("get devno failed\n");
return -1;
}
major = MAJOR(devno);//容易遗漏,注意
}
/*给struct cdev对象指定操作函数集*/
cdev_init(&pgmydev->mydev,&myops);
/*将struct cdev对象添加到内核对应的数据结构里*/
pgmydev->mydev.owner = THIS_MODULE;
cdev_add(&pgmydev->mydev,devno,fs4412key2_num);
init_waitqueue_head(&pgmydev->rq);
spin_lock_init(&pgmydev->lock);
//初始化tasklet
tasklet_init(&pgmydev->tsk,bottom_irq_func,(unsigned long)pgmydev);
ret = request_irq(pgmydev->irqno,key2_irq_handle,IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING,"fs4412key2",pgmydev);
if(ret)
{
printk("request_irq failed\n");
cdev_del(&pgmydev->mydev);
kfree(pgmydev);
pgmydev = NULL;
unregister_chrdev_region(devno,fs4412key2_num);
return -1;
}
return 0;
}
void __exit fs4412key2_exit(void)
{
dev_t devno = MKDEV(major,minor);
free_irq(pgmydev->irqno,pgmydev);
cdev_del(&pgmydev->mydev);
unregister_chrdev_region(devno,fs4412key2_num);
kfree(pgmydev);
pgmydev = NULL;
}
MODULE_LICENSE("GPL");
module_init(fs4412key2_init);
module_exit(fs4412key2_exit);
工作队列是另外一种下半部执行方式,工作队列在进程上下文执行,工作队列将要推后的工作交给一个内核线程去执行,因为工作队列工作在进程上下文,因此工作队列允许睡眠或重新调度。因此如果你要推后的工作可以睡眠那么就可以选择工作队列,否则的话就只能选择软中断或 tasklet。
驱动层API
A.定义一个 work_struct 结构体变量
struct work_struct testwork;
B.驱动入口函数中初始化 work
INIT_WORK(&testwork, testwork_func_t);
C.定义work 处理函数
void testwork_func_t(struct work_struct *work);
D. 中断处理函数中调度work 处理函数
schedule_work(&testwork);
//定义work_struct 结构体变量
struct work_struct work_queue;
//初始化工作队列
INIT_WORK(struct work_struct * pwork, _func);
//参数:pwork:工作队列
//func:工作队列的底半部处理函数
//定义工作队列底半部处理函数
void work_queue_func(struct work_struct *work);
//工作队列的调度函数
bool schedule_work(struct work_struct *work);
驱动层代码实现
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "fs4412_key.h"
int major = 11;
int minor = 0;
int fs4412key2_num = 1;
struct fs4412key2_dev
{
struct cdev mydev;
int gpio;
int irqno;
struct keyvalue data;
int newflag;
spinlock_t lock;
wait_queue_head_t rq;
//struct tasklet_struct tsk;工作队列对象
struct work_struct wk;
};
struct fs4412key2_dev *pgmydev = NULL;
int fs4412key2_open(struct inode *pnode,struct file *pfile)
{
pfile->private_data =(void *) (container_of(pnode->i_cdev,struct fs4412key2_dev,mydev));
return 0;
}
int fs4412key2_close(struct inode *pnode,struct file *pfile)
{
return 0;
}
ssize_t fs4412key2_read(struct file *pfile,char __user *puser,size_t count,loff_t *p_pos)
{
struct fs4412key2_dev *pmydev = (struct fs4412key2_dev *)pfile->private_data;
int size = 0;
int ret = 0;
if(count < sizeof(struct keyvalue))
{
printk("expect read size is invalid\n");
return -1;
}
spin_lock(&pmydev->lock);
if(!pmydev->newflag)
{
if(pfile->f_flags & O_NONBLOCK)
{//非阻塞
spin_unlock(&pmydev->lock);
printk("O_NONBLOCK No Data Read\n");
return -1;
}
else
{//阻塞
spin_unlock(&pmydev->lock);
ret = wait_event_interruptible(pmydev->rq,pmydev->newflag == 1);
if(ret)
{
printk("Wake up by signal\n");
return -ERESTARTSYS;
}
spin_lock(&pmydev->lock);
}
}
if(count > sizeof(struct keyvalue))
{
size = sizeof(struct keyvalue);
}
else
{
size = count;
}
ret = copy_to_user(puser,&pmydev->data,size);
if(ret)
{
spin_unlock(&pmydev->lock);
printk("copy_to_user failed\n");
return -1;
}
pmydev->newflag = 0;
spin_unlock(&pmydev->lock);
return size;
}
unsigned int fs4412key2_poll(struct file *pfile,poll_table *ptb)
{
struct fs4412key2_dev *pmydev = (struct fs4412key2_dev *)pfile->private_data;
unsigned int mask = 0;
poll_wait(pfile,&pmydev->rq,ptb);
spin_lock(&pmydev->lock);
if(pmydev->newflag)
{
mask |= POLLIN | POLLRDNORM;
}
spin_unlock(&pmydev->lock);
return mask;
}
struct file_operations myops = {
.owner = THIS_MODULE,
.open = fs4412key2_open,
.release = fs4412key2_close,
.read = fs4412key2_read,
.poll = fs4412key2_poll,
};
//上半部
irqreturn_t key2_irq_handle(int no,void *arg)
{
struct fs4412key2_dev *pmydev = (struct fs4412key2_dev *)arg;
//tasklet_schedule(&pmydev->tsk);
//任务调度下半部
schedule_work(&pmydev->wk);
return IRQ_HANDLED;
}
//void bottom_irq_func(unsigned long arg)
//下半部
void bottom_irq_func(struct work_struct *pwk)
{
//struct fs4412key2_dev *pmydev = (struct fs4412key2_dev *)arg;
//已知结构体地址获取其成员地址
struct fs4412key2_dev *pmydev = container_of(pwk,struct fs4412key2_dev,wk);
int status1 = 0;
int status2 = 0;
int status = 0;
status1 = gpio_get_value(pmydev->gpio);
mdelay(1);
status2 = gpio_get_value(pmydev->gpio);
if(status1 != status2)
{
return;
}
status = status1;
spin_lock(&pmydev->lock);
if(status == pmydev->data.status)
{
spin_unlock(&pmydev->lock);
return;
}
pmydev->data.code = KEY2;
pmydev->data.status = status;
pmydev->newflag = 1;
spin_unlock(&pmydev->lock);
wake_up(&pmydev->rq);
return;
}
int __init fs4412key2_init(void)
{
int ret = 0;
dev_t devno = MKDEV(major,minor);
struct device_node *pnode = NULL;
pnode = of_find_node_by_path("/mykey2_node");
if(NULL == pnode)
{
printk("find node failed\n");
return -1;
}
pgmydev = (struct fs4412key2_dev *)kmalloc(sizeof(struct fs4412key2_dev),GFP_KERNEL);
if(NULL == pgmydev)
{
printk("kmallc for struct fs4412key2_dev failed\n");
return -1;
}
pgmydev->gpio = of_get_named_gpio(pnode,"key2-gpio",0);
pgmydev->irqno = irq_of_parse_and_map(pnode,0);
/*申请设备号*/
ret = register_chrdev_region(devno,fs4412key2_num,"fs4412key2");
if(ret)
{
ret = alloc_chrdev_region(&devno,minor,fs4412key2_num,"fs4412key2");
if(ret)
{
kfree(pgmydev);
pgmydev = NULL;
printk("get devno failed\n");
return -1;
}
major = MAJOR(devno);//容易遗漏,注意
}
/*给struct cdev对象指定操作函数集*/
cdev_init(&pgmydev->mydev,&myops);
/*将struct cdev对象添加到内核对应的数据结构里*/
pgmydev->mydev.owner = THIS_MODULE;
cdev_add(&pgmydev->mydev,devno,fs4412key2_num);
init_waitqueue_head(&pgmydev->rq);
spin_lock_init(&pgmydev->lock);
//tasklet_init(&pgmydev->tsk,bottom_irq_func,(unsigned long)pgmydev);
//初始化工作队列
INIT_WORK(&pgmydev->wk,bottom_irq_func);
ret = request_irq(pgmydev->irqno,key2_irq_handle,IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING,"fs4412key2",pgmydev);
if(ret)
{
printk("request_irq failed\n");
cdev_del(&pgmydev->mydev);
kfree(pgmydev);
pgmydev = NULL;
unregister_chrdev_region(devno,fs4412key2_num);
return -1;
}
return 0;
}
void __exit fs4412key2_exit(void)
{
dev_t devno = MKDEV(major,minor);
free_irq(pgmydev->irqno,pgmydev);
cdev_del(&pgmydev->mydev);
unregister_chrdev_region(devno,fs4412key2_num);
kfree(pgmydev);
pgmydev = NULL;
}
MODULE_LICENSE("GPL");
module_init(fs4412key2_init);
module_exit(fs4412key2_exit);