1、中断申请和释放
申请函数:int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev)
成功返回 0
参数含义
irq | 中断号 | |
handler | 中断处理函数 | |
flags | 中断标志 | |
name | 中断名字,设置以后可以在/proc/interrupts 文件中看到对应的中断名字 | |
dev | 如果将 flags 设置为 IRQF_SHARED 的话, dev 用来区分不同的中断,一般情况下将 dev 设置为设备结构体, dev 会传递给中断处理函数 irq_handler_t 的第二个参数 | |
释放:void free_irq(unsigned int irq, void *dev);
中断处理函数格式:irqreturn_t (*irq_handler_t) (int, void *)
第一个参数是要中断处理函数要相应的中断号。第二个参数是一个指向 void 的指针,也就
是个通用指针,需要与 request_irq 函数的 dev 参数保持一致。用于区分共享中断的不同设备,
dev 也可以指向设备数据结构。中断处理函数的返回值为 irqreturn_t 类型,例子:
static irqreturn_t rtc_interrupt(int irq, void *dev_id)
{
}
激活当前CPU中断:
local_irq_enable();
禁止当前CPU中断:
local_irq_disable();
激活指定中断线:
void enable_irq(unsigned int irq);
禁止指定中断线:
void disable_irq(unsigned int irq);
禁止指定中断线:
void disable_irq_nosync(unsigned int irq);
注:此函数调用irq_chip中disable禁止指定中断线,所以不会保证中断线上执行的中断服务程序已经退出。
2、中断上下半部
这里重点关注:Tasklet
2.1下半部定义和初始化
DECLARE_TASKLET(name, func, data)
其中 name 为要定义的 tasklet 名字,这个名字就是一个 tasklet_struct 类型的时候变量, func
就是 tasklet 的处理函数, data 是传递给 func 函数的参数。
2.2下半部函数调用
在上半部,也就是中断处理函数中调用 tasklet_schedule 函数就能使 tasklet 在合适的时间运
行, tasklet_schedule 函数原型如下:
void tasklet_schedule(struct tasklet_struct *t)
函数参数和返回值含义如下:
t:要调度的 tasklet,也就是 DECLARE_TASKLET 宏里面的 name。
3、设备树里的中断信息
3.1实例分析:I.MX6ULL 的中断控制器节点,节点内容如下所示
1 intc: interrupt-controller@00a01000 {
2 compatible = "arm,cortex-a7-gic";
3 #interrupt-cells = <3>;
4 interrupt-controller;
5 reg = <0x00a01000 0x1000>,
6 <0x00a02000 0x100>;
7 };
第 2 行, compatible 属性值为“arm,cortex-a7-gic”在 Linux 内核源码中搜索“arm,cortex-a7-
gic”即可找到 GIC 中断控制器驱动文件。
第 3 行, #interrupt-cells 和#address-cells、 #size-cells 一样。表示此中断控制器下设备的 cells
大小,对于设备而言,会使用 interrupts 属性描述中断信息, #interrupt-cells 描述了 interrupts 属性的 cells 大小,也就是一条信息有几个 cells。每个 cells 都是 32 位整形值,对于 ARM 处理的
GIC 来说,一共有 3 个 cells,这三个 cells 的含义如下:
第一个 cells:中断类型, 0 表示 SPI 中断, 1 表示 PPI 中断。
第二个 cells:中断号,对于 SPI 中断来说中断号的范围为 0~987,对于 PPI 中断来说中断
号的范围为 0~15。
第三个 cells:标志, bit[3:0]表示中断触发类型,为 1 的时候表示上升沿触发,为 2 的时候
表示下降沿触发,为 4 的时候表示高电平触发,为 8 的时候表示低电平触发。 bit[15:8]为 PPI 中
断的 CPU 掩码。
第 4 行, interrupt-controller 节点为空,表示当前节点是中断控制器
3.2实例分析:I.MX6ULL 的中断控制器节点,节点内容如下所示
1 gpio5: gpio@020ac000 {
2 compatible = "fsl,imx6ul-gpio", "fsl,imx35-gpio";
3 reg = <0x020ac000 0x4000>;
4 interrupts = <GIC_SPI 74 IRQ_TYPE_LEVEL_HIGH>,
5 <GIC_SPI 75 IRQ_TYPE_LEVEL_HIGH>;
6 gpio-controller;
7 #gpio-cells = <2>;
8 interrupt-controller;
9 #interrupt-cells = <2>;
10 };第 4 行, interrupts 描述中断源信息,对于 gpio5 来说一共有两条信息,中断类型都是 SPI,
触发电平都是 IRQ_TYPE_LEVEL_HIGH。不同之处在于中断源,一个是 74,一个是 75,打开
可以打开《IMX6ULL 参考手册》的“Chapter 3 Interrupts and DMA Events”章节,找到表 3-1,
有如图 50.1.3.1 所示的内容:
图 50.1.3.1 中断表
从图 50.1.3.1 可以看出, GPIO5 一共用了 2 个中断号,一个是 74,一个是 75。其中 74 对
应 GPIO5_IO00~GPIO5_IO15 这低 16 个 IO, 75 对应 GPIO5_IO16~GPIOI5_IO31 这高 16 位 IO。
第 8 行, interrupt-controller 表明了 gpio5 节点也是个中断控制器,用于控制 gpio5 所有 IO
的中断。
第9行#interrupt-cells 描述了 interrupts 属性的个数,即指定了子中断描述
实例1(基于task机制)
设备树文件修改
gpio1: gpio@0209c000 {
compatible = "fsl,imx6ul-gpio", "fsl,imx35-gpio";
reg = <0x0209c000 0x4000>;
interrupts = <GIC_SPI 66 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 67 IRQ_TYPE_LEVEL_HIGH>;
gpio-controller;
#gpio-cells = <2>;
interrupt-controller;
#interrupt-cells = <2>; /*这个2,代表它的中断子节点描述是2个参数,和下面key节点的interrupts里的参数对应 */
};
/{
......
key {
compatible = "fsl,my-key";
#address-cells = <1>;
#size-cells = <1>;
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_key>;
gpios-key1 = <&gpio1 18 GPIO_ACTIVE_LOW>;
interrupt-parent = <&gpio1>;
interrupts = <18 IRQ_TYPE_EDGE_BOTH>;
status = "okay";
};
....
}
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include <linux/of_irq.h>
#include <linux/irq.h>
#include <linux/interrupt.h>
#include <linux/timer.h>
#define KEY_CNT 1 /* 设备号个数 */
#define KEY_NAME "key" /* 名字 */
/* 定义按键值 */
#define KEY0VALUE 0X01 /* 按键值 */
#define INVAKEY 0X00 /* 无效的按键值 */
/* key设备结构体 */
struct key_dev{
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev */
struct class *class; /* 类 */
struct device *device; /* 设备 */
int major; /* 主设备号 */
int minor; /* 次设备号 */
struct device_node *nd; /* 设备节点 */
int key_gpio; /* key所使用的GPIO编号 */
atomic_t releasekey; /* 标记是否完成一次完成的按键 */
atomic_t keyvalue; /* 按键值 */
int key_irq; /* 按键中断号*/
struct tasklet_struct task_key;
struct timer_list timer;
};
struct key_dev keydev; /* key设备 */
/*定时器处理*/
void timer_fun(unsigned long arg)
{
int value;
struct key_dev *dev = (struct key_dev *)arg;
value = gpio_get_value(dev->key_gpio); /* 读取 IO 值 */
if(value == 0){ /* 按下按键 */
atomic_set(&dev->keyvalue, KEY0VALUE);
printk("key down\r\n");
}
else{ /* 按键松开 */
if(atomic_read(&dev->keyvalue))
{
// atomic_set(&dev->keyvalue, 0x80 | KEY0VALUE);
printk("key up\r\n");
atomic_set(&dev->releasekey, 1);
}
}
}
/*中断下半部处理*/
void task_key_fun(unsigned long data)
{
struct key_dev *dev = (struct key_dev *)data;
// printk("task_key_fun %d\r\n",dev->key_irq);
mod_timer(&dev->timer, jiffies + msecs_to_jiffies(20));
}
irqreturn_t key_handler_t(int irq, void *dev_id)
{
struct key_dev *dev = (struct key_dev*)dev_id;
// printk("key_handler_t in %d\r\n",dev->key_irq);
#if 0
dev->curkeynum = 0;
dev->timer.data = (volatile long)dev_id;
mod_timer(&dev->timer, jiffies + msecs_to_jiffies(10));
#endif
tasklet_schedule(&dev->task_key);
return IRQ_RETVAL(IRQ_HANDLED);
}
/*
* @description : 初始化按键IO,open函数打开驱动的时候
* 初始化按键所使用的GPIO引脚。
* @param : 无
* @return : 无
*/
static int keyio_init(void)
{
int ret;
keydev.nd = of_find_node_by_path("/key");
if (keydev.nd== NULL) {
return -EINVAL;
}
keydev.key_gpio = of_get_named_gpio(keydev.nd ,"gpios-key1", 0);
if (keydev.key_gpio < 0) {
printk("can't get key0\r\n");
return -EINVAL;
}
printk("key_gpio=%d\r\n", keydev.key_gpio);
/* 初始化key所使用的IO */
ret=gpio_request(keydev.key_gpio, "key0"); /* 请求IO */
if(ret < 0 )
{
printk("gpio_request erro!\r\n");
goto gpio_request_erro;
}
gpio_direction_input(keydev.key_gpio); /* 设置为输入 */
keydev.key_irq = gpio_to_irq(keydev.key_gpio);
printk("the irq num = %d\r\n",keydev.key_irq);
#if 0
keydev.key_irq = irq_of_parse_and_map(keydev.nd,0);
printk("the irq num = %d\r\n",keydev.key_irq);
#endif
ret = request_irq(keydev.key_irq,key_handler_t,IRQF_TRIGGER_RISING|IRQF_TRIGGER_FALLING,"xcmkey",&keydev);
if(ret)
{
printk("request_irq erro!\r\n");
goto request_irq_erro;
}
printk("request_irq ok!\r\n");
tasklet_init(&keydev.task_key,task_key_fun,(unsigned long)&keydev);
init_timer(&keydev.timer);
keydev.timer.function = timer_fun; /* 设置定时处理函数 */
//timer.expires=jffies + msecs_to_jiffies(2000);/* 超时时间 2 秒 */
keydev.timer.data = (unsigned long)&keydev; /* 将设备结构体作为参数 */
add_timer(&keydev.timer);
return 0;
request_irq_erro:
gpio_free(keydev.key_gpio);
gpio_request_erro:
return -EINVAL;
}
/*
* @description : 打开设备
* @param - inode : 传递给驱动的inode
* @param - filp : 设备文件,file结构体有个叫做private_data的成员变量
* 一般在open的时候将private_data指向设备结构体。
* @return : 0 成功;其他 失败
*/
static int key_open(struct inode *inode, struct file *filp)
{
filp->private_data = &keydev; /* 设置私有数据 */
return 0;
}
/*
* @description : 从设备读取数据
* @param - filp : 要打开的设备文件(文件描述符)
* @param - buf : 返回给用户空间的数据缓冲区
* @param - cnt : 要读取的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 读取的字节数,如果为负值,表示读取失败
*/
static ssize_t key_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
int ret = 0;
int value;
struct key_dev *dev = filp->private_data;
value = atomic_read(&dev->releasekey);
if(value)
{
value = atomic_read(&dev->keyvalue);
ret = copy_to_user(buf, &value, sizeof(value));
atomic_set(&dev->keyvalue, 0); /* 无效的按键值 */
atomic_set(&dev->releasekey, 0); /* 无效的按键值 */
}
else
{
return -EINVAL;
}
return value;
}
/*
* @description : 向设备写数据
* @param - filp : 设备文件,表示打开的文件描述符
* @param - buf : 要写给设备写入的数据
* @param - cnt : 要写入的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 写入的字节数,如果为负值,表示写入失败
*/
static ssize_t key_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
return 0;
}
/*
* @description : 关闭/释放设备
* @param - filp : 要关闭的设备文件(文件描述符)
* @return : 0 成功;其他 失败
*/
static int key_release(struct inode *inode, struct file *filp)
{
return 0;
}
/* 设备操作函数 */
static struct file_operations key_fops = {
.owner = THIS_MODULE,
.open = key_open,
.read = key_read,
.write = key_write,
.release = key_release,
};
/*
* @description : 驱动入口函数
* @param : 无
* @return : 无
*/
static int __init mykey_init(void)
{
int ret;
/* 初始化原子变量 */
atomic_set(&keydev.keyvalue, INVAKEY);
atomic_set(&keydev.releasekey, 0);
/* 注册字符设备驱动 */
/* 1、创建设备号 */
if (keydev.major) { /* 定义了设备号 */
keydev.devid = MKDEV(keydev.major, 0);
register_chrdev_region(keydev.devid, KEY_CNT, KEY_NAME);
} else { /* 没有定义设备号 */
alloc_chrdev_region(&keydev.devid, 0, KEY_CNT, KEY_NAME); /* 申请设备号 */
keydev.major = MAJOR(keydev.devid); /* 获取分配号的主设备号 */
keydev.minor = MINOR(keydev.devid); /* 获取分配号的次设备号 */
}
/* 2、初始化cdev */
keydev.cdev.owner = THIS_MODULE;
cdev_init(&keydev.cdev, &key_fops);
/* 3、添加一个cdev */
cdev_add(&keydev.cdev, keydev.devid, KEY_CNT);
/* 4、创建类 */
keydev.class = class_create(THIS_MODULE, KEY_NAME);
if (IS_ERR(keydev.class)) {
return PTR_ERR(keydev.class);
}
/* 5、创建设备 */
keydev.device = device_create(keydev.class, NULL, keydev.devid, NULL, KEY_NAME);
if (IS_ERR(keydev.device)) {
return PTR_ERR(keydev.device);
}
ret = keyio_init(); /* 初始化按键IO */
if (ret < 0) {
return ret;
}
return 0;
}
/*
* @description : 驱动出口函数
* @param : 无
* @return : 无
*/
static void __exit mykey_exit(void)
{
del_timer(&keydev.timer);
gpio_free(keydev.key_gpio);
free_irq(keydev.key_irq,&keydev);
/* 注销字符设备驱动 */
cdev_del(&keydev.cdev);/* 删除cdev */
unregister_chrdev_region(keydev.devid, KEY_CNT); /* 注销设备号 */
device_destroy(keydev.class, keydev.devid);
class_destroy(keydev.class);
}
module_init(mykey_init);
module_exit(mykey_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("zuozhongkai");
实例2(基于队列queue机制)
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include <linux/of_irq.h>
#include <linux/irq.h>
#include <linux/interrupt.h>
#include <linux/timer.h>
/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
文件名 : keyqueue.c
作者 : 左忠凯
版本 : V1.0
描述 : Linux按键输入中断+定时器+下半部队列驱动实验
其他 : 无
论坛 : www.openedv.com
日志 : 初版V1.0 2019/7/18 左忠凯创建
***************************************************************/
#define KEY_CNT 1 /* 设备号个数 */
#define KEY_NAME "key_queue" /* 名字 */
/* 定义按键值 */
#define KEY0VALUE 0X01 /* 按键值 */
#define INVAKEY 0X00 /* 无效的按键值 */
/* key设备结构体 */
struct key_dev{
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev */
struct class *class; /* 类 */
struct device *device; /* 设备 */
int major; /* 主设备号 */
int minor; /* 次设备号 */
struct device_node *nd; /* 设备节点 */
int key_gpio; /* key所使用的GPIO编号 */
atomic_t releasekey; /* 标记是否完成一次完成的按键 */
atomic_t keyvalue; /* 按键值 */
int key_irq; /* 按键中断号*/
//struct tasklet_struct task_key;
struct work_struct key_work; /*队列queue结构体*/
struct timer_list timer;
};
struct key_dev keydev; /* key设备 */
/*定时器处理*/
void timer_fun(unsigned long arg)
{
int value;
struct key_dev *dev = (struct key_dev *)arg;
value = gpio_get_value(dev->key_gpio); /* 读取 IO 值 */
if(value == 0){ /* 按下按键 */
atomic_set(&dev->keyvalue, KEY0VALUE);
printk("key down\r\n");
}
else{ /* 按键松开 */
if(atomic_read(&dev->keyvalue))
{
// atomic_set(&dev->keyvalue, 0x80 | KEY0VALUE);
printk("key up\r\n");
atomic_set(&dev->releasekey, 1);
}
}
}
void key_work_fun(struct work_struct *work)
{
mod_timer(&keydev.timer, jiffies + msecs_to_jiffies(20));
}
/*中断下半部处理*/
void task_key_fun(unsigned long data)
{
struct key_dev *dev = (struct key_dev *)data;
// printk("task_key_fun %d\r\n",dev->key_irq);
mod_timer(&dev->timer, jiffies + msecs_to_jiffies(20));
}
irqreturn_t key_handler_t(int irq, void *dev_id)
{
struct key_dev *dev = (struct key_dev*)dev_id;
// printk("key_handler_t in %d\r\n",dev->key_irq);
#if 0
dev->curkeynum = 0;
dev->timer.data = (volatile long)dev_id;
mod_timer(&dev->timer, jiffies + msecs_to_jiffies(10));
#endif
//tasklet_schedule(&dev->task_key);/*task调度*/
schedule_work(&dev->key_work); /*队列调度*/
return IRQ_RETVAL(IRQ_HANDLED);
}
/*
* @description : 初始化按键IO,open函数打开驱动的时候
* 初始化按键所使用的GPIO引脚。
* @param : 无
* @return : 无
*/
static int keyio_init(void)
{
int ret;
keydev.nd = of_find_node_by_path("/key");
if (keydev.nd== NULL) {
return -EINVAL;
}
keydev.key_gpio = of_get_named_gpio(keydev.nd ,"gpios-key1", 0);
if (keydev.key_gpio < 0) {
printk("can't get key0\r\n");
return -EINVAL;
}
printk("key_gpio=%d\r\n", keydev.key_gpio);
/* 初始化key所使用的IO */
ret=gpio_request(keydev.key_gpio, "key0"); /* 请求IO */
if(ret < 0 )
{
printk("gpio_request erro!\r\n");
goto gpio_request_erro;
}
gpio_direction_input(keydev.key_gpio); /* 设置为输入 */
keydev.key_irq = gpio_to_irq(keydev.key_gpio);
printk("the irq num = %d\r\n",keydev.key_irq);
keydev.key_irq = irq_of_parse_and_map(keydev.nd,0);
printk("the irq num = %d\r\n",keydev.key_irq);
ret = request_irq(keydev.key_irq,key_handler_t,IRQF_TRIGGER_RISING|IRQF_TRIGGER_FALLING,"xcmkey",&keydev);
if(ret)
{
printk("request_irq erro!\r\n");
goto request_irq_erro;
}
printk("request_irq ok!\r\n");
// tasklet_init(&keydev.task_key,task_key_fun,(unsigned long)&keydev);
INIT_WORK(&keydev.key_work, key_work_fun);/*初始化中断下半部的队列*/
/*内核定时器初始化*/
init_timer(&keydev.timer);
keydev.timer.function = timer_fun; /* 设置定时处理函数 */
//timer.expires=jffies + msecs_to_jiffies(2000);/* 超时时间 2 秒 */
keydev.timer.data = (unsigned long)&keydev; /* 将设备结构体作为参数 */
add_timer(&keydev.timer);
return 0;
request_irq_erro:
gpio_free(keydev.key_gpio);
gpio_request_erro:
return -EINVAL;
}
/*
* @description : 打开设备
* @param - inode : 传递给驱动的inode
* @param - filp : 设备文件,file结构体有个叫做private_data的成员变量
* 一般在open的时候将private_data指向设备结构体。
* @return : 0 成功;其他 失败
*/
static int key_open(struct inode *inode, struct file *filp)
{
filp->private_data = &keydev; /* 设置私有数据 */
return 0;
}
/*
* @description : 从设备读取数据
* @param - filp : 要打开的设备文件(文件描述符)
* @param - buf : 返回给用户空间的数据缓冲区
* @param - cnt : 要读取的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 读取的字节数,如果为负值,表示读取失败
*/
static ssize_t key_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
int ret = 0;
int value;
struct key_dev *dev = filp->private_data;
value = atomic_read(&dev->releasekey);
if(value)
{
value = atomic_read(&dev->keyvalue);
ret = copy_to_user(buf, &value, sizeof(value));
atomic_set(&dev->keyvalue, 0); /* 无效的按键值 */
atomic_set(&dev->releasekey, 0); /* 无效的按键值 */
}
else
{
return -EINVAL;
}
return value;
}
/*
* @description : 向设备写数据
* @param - filp : 设备文件,表示打开的文件描述符
* @param - buf : 要写给设备写入的数据
* @param - cnt : 要写入的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 写入的字节数,如果为负值,表示写入失败
*/
static ssize_t key_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
return 0;
}
/*
* @description : 关闭/释放设备
* @param - filp : 要关闭的设备文件(文件描述符)
* @return : 0 成功;其他 失败
*/
static int key_release(struct inode *inode, struct file *filp)
{
return 0;
}
/* 设备操作函数 */
static struct file_operations key_fops = {
.owner = THIS_MODULE,
.open = key_open,
.read = key_read,
.write = key_write,
.release = key_release,
};
/*
* @description : 驱动入口函数
* @param : 无
* @return : 无
*/
static int __init mykey_init(void)
{
int ret;
/* 初始化原子变量 */
atomic_set(&keydev.keyvalue, INVAKEY);
atomic_set(&keydev.releasekey, 0);
/* 注册字符设备驱动 */
/* 1、创建设备号 */
if (keydev.major) { /* 定义了设备号 */
keydev.devid = MKDEV(keydev.major, 0);
register_chrdev_region(keydev.devid, KEY_CNT, KEY_NAME);
} else { /* 没有定义设备号 */
alloc_chrdev_region(&keydev.devid, 0, KEY_CNT, KEY_NAME); /* 申请设备号 */
keydev.major = MAJOR(keydev.devid); /* 获取分配号的主设备号 */
keydev.minor = MINOR(keydev.devid); /* 获取分配号的次设备号 */
}
/* 2、初始化cdev */
keydev.cdev.owner = THIS_MODULE;
cdev_init(&keydev.cdev, &key_fops);
/* 3、添加一个cdev */
cdev_add(&keydev.cdev, keydev.devid, KEY_CNT);
/* 4、创建类 */
keydev.class = class_create(THIS_MODULE, KEY_NAME);
if (IS_ERR(keydev.class)) {
return PTR_ERR(keydev.class);
}
/* 5、创建设备 */
keydev.device = device_create(keydev.class, NULL, keydev.devid, NULL, KEY_NAME);
if (IS_ERR(keydev.device)) {
return PTR_ERR(keydev.device);
}
ret = keyio_init(); /* 初始化按键IO */
if (ret < 0) {
return ret;
}
return 0;
}
/*
* @description : 驱动出口函数
* @param : 无
* @return : 无
*/
static void __exit mykey_exit(void)
{
del_timer(&keydev.timer);
gpio_free(keydev.key_gpio);
free_irq(keydev.key_irq,&keydev);
/* 注销字符设备驱动 */
cdev_del(&keydev.cdev);/* 删除cdev */
unregister_chrdev_region(keydev.devid, KEY_CNT); /* 注销设备号 */
device_destroy(keydev.class, keydev.devid);
class_destroy(keydev.class);
}
module_init(mykey_init);
module_exit(mykey_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("zuozhongkai");