还记得我们在讲按键生成外部中断时候通过一个APP来调用驱动文件么?因为我们在用户态APP里通过while循环不断调用read函数去获取按键状态,占用了大量的系统资源,这种模式肯定是不行的。所以今天我们重新构造一下这个驱动程序(主要是和用户态APP交互的文件操作集合函数)。
阻塞和非阻塞
应用程序对驱动文件在进行IO操作的时候,一般分为阻塞和非阻塞两种模式,针对阻塞IO模式来说,如果APP不能操作对应的文件,那么对文件的访问进程就会被挂起来,一直等到可以获取到设备资源后才能执行。Linux驱动程序中,通过一个叫做等待队列(wait queue)的机制来唤醒被的阻塞操作。
和阻塞IO不同的是,非阻塞IO是不会将文件操作进程挂起来进入休眠状态,而是不停的轮训等待直到资源可以被使用或者放弃。就像以前那种会叫的烧水壶,阻塞IO是你烧上水了就可以去干别的事情,这件事就挂起来了,一般是通过中断将进程唤醒,也就是水烧开了壶叫了,你在过来处理事件就行了。而非阻塞操作就是你把水烧上每过一分钟就回来看一下,没开就回去干别的事等一分钟再回来看看,开了就去泡茶喝水。
阻塞IO处理起来是比较简单的,我们只需要在合适的时候将进程休眠,再在某些时候将其唤醒就可以了。所以今天我们从阻塞IO来看看怎么实现这个驱动和APP之间的交互。
等待队列
前面说了,阻塞访问是通过等待队列这个机制来管理进程或任务的休眠或唤醒的,使用等待队列一般要先创建一个等待队列头(wait_queue_head_t),然后通过一个函数wait_event将在当前进程符合符合某些特定要求时将当前进程插入到等待队列后休眠。在满足某些要求时,内核再将等待队列里的进程唤醒,一般情况唤醒都是通过中断来实现的。等待队列以双循环链表为基础结构,链表头和链表项分别为等待队列头和等待队列项(有时候也叫等待队列元素),分别用结构体 wait_queue_head_t和wait_queue_t(我用的Linux内核版本是4.1,好像是5.x版本以后这个wait_queue_t被wait_queue_entry_t取代了,基本结构都差不多!都是在include/linux/wait.h被描述的)。
等待队列头
先从代码上看一下等待队列头的内容:
1 struct __wait_queue_head { 2 spinlock_t lock; 3 struct list_head task_list; 4 }; 5 typedef struct __wait_queue_head wait_queue_head_t;
等待队列头里包含了一个自旋锁lock,用来用实现对等待队列的互斥访问;还有一个list_head是一个双向链表
1 struct list_head { 2 struct list_head *next, *prev; 3 };
用来存放等待的进程(从参数名称可以看出来:task_list)。这个list_head作为域的连接件,在等待队列项里也有这个元素,它通过一个双链表把等待task的head和等待的进程列表链接起来。
等待队列头的使用
等待队列头在声明以后需要初始化才能使用,初始化有两个方法:通过函数init_wait_queue_head或者一个叫DECLARE_WAIT_QUEUE_HEAD的宏直接声明加初始化。使用方法如下:
/*方式一*/ wait_queue_head_t myqueue; //声明队列头 init_waitqueue_head(&myqueue); //初始化队列头 /*方式二*/ DECLARE_WAIT_QUEUE_HEAD(myqueue); //声明并初始化队列头
初始化完成的队列头有两种方式使用,第一种只是通过队列头将指定的任务通过等待事件休眠或唤醒,第二种要结合队列项实现任务的控制,我们先看看第一种简单的方法。首先在new_dev结构体里添加一个队列头并在设备初始化的过程中将其初始化
1 struct new_dev 2 { 3 dev_t dev_id; 4 int major; 5 int minor; 6 struct class *class; 7 struct device *device; 8 struct cdev cdev; 9 struct device_node *dev_nd; 10 int dev_gpio; 11 12 struct irq_keydesc irqkey[KEY_NUM]; //按键描述数组 13 14 struct timer_list timer; //定时器 15 int timer_per; 16 17 atomic_t keyvalue; 18 atomic_t keyreleased; //1表示1次有效按键并被释放,0表示未被释放 19 20 wait_queue_head_t r_wait; //读等待队列头 21 }; 22 23 24 static int __init new_dev_init(void){ 25 . 26 . 27 . 28 . 29 //初始化原子变量 30 atomic_set(&new_dev.keyvalue,INVALKEYS); 31 atomic_set(&new_dev.keyreleased,0); 32 33 34 //初始化等待队列头 35 init_waitqueue_head(&new_dev.r_wait); 36 return ret; 37 . 38 . 39 . 40 .}
设备初始化过程的详细代码这里不放了,主要就是为了将清队列头初始化的过程。下面主要是文件操作集合中的read函数。
1 static ssize_t new_dev_read(struct file *filp,char __user *buf,size_t cnt,loff_t *offt) 2 { 3 int ret = 0; 4 unsigned char keyvalue; 5 unsigned char keyreleased; 6 struct new_dev *dev = filp->private_data; //私有数据 7 8 wait_event_interruptible(dev->r_wait,atomic_read(&dev->keyreleased)); //等待按键有效 9 // wait_event(dev->r_wait,atomic_read(&dev->keyreleased)); //等待按键有效 10 11 keyvalue = atomic_read(&dev->keyvalue); //获取按键状态标志及按键值 12 keyreleased = atomic_read(&dev->keyreleased); 13 14 if(keyreleased){ //出现按键被按下并释放的过程 15 if(keyvalue&0x80){ 16 //获取最高位,如果为1表示出现过按键被按下后释放掉状态,是有效状态 17 keyvalue &= ~0x80; 18 ret = copy_to_user(buf,&keyvalue,sizeof(keyvalue)); 19 } 20 else{ 21 goto data_err; 22 } 23 atomic_set(&dev->keyreleased,0); //恢复keyreleased的状态 24 } 25 else{ 26 goto data_err; //返回负数,在用户态跳出循环 27 } 28 return ret; 29 //异常处理 30 data_err: 31 return -EINVAL; 32 };
主要就是第8或9行里的代码,添加了等待事件的代码。等待事件的定义在wait.h里面定义的都有,下面是常用的几个
函数 | 描述 |
wait_event(wq, condition) |
等待以 wq 为等待队列头的等待队列被唤醒,前提是 condition 条件必须满足(为真),否则一直阻塞 。 此 函 数 会 将 进 程 设 置 为TASK_UNINTERRUPTIBLE 状态 |
wait_event_timeout(wq, condition, timeout) | 功能和 wait_event 类似,但是此函数可以添加超时时间,以 jiffies 为单位。此函数有返回值,如果返回 0 的话表示超时时间到,而且 condition为假。为 1 的话表示 condition 为真,也就是条件满足了。 |
wait_event_interruptible(wq, condition) | 与 wait_event 函数类似,但是此函数将进程设置为 TASK_INTERRUPTIBLE,就是可以被信号打断。 |
wait_event_interruptible_timeout(wq, condition, timeout) | 与 wait_event_timeout 函数类似,此函数也将进程设置为 TASK_INTERRUPTIBLE,可以被信号打断。 |
我们用了前两个,区别是第1个不支持中断,也就是无法被信号打断。通过kill命令或ctrl+C无法中断程序。用户APP在通过read函数进入内核态时,会根据wait_event的参数condition来决定后续工作。我们给定的condition是读取原子变量keyreleased的状态,为1时将dev里的队列头指向队列r_wait加入等待队列,直至被wakeup命令唤醒。程序在这里就被阻塞了,不会执行后续的内容,用户态APP里的while循环因为一个循环体没有执行完毕也就被挂起了。一直到按键被按下后消抖完成进入定时函数
1 static void timer_func(unsigned long arg){ 2 3 int value = 0; 4 struct new_dev *dev =(struct new_dev*)arg; 5 6 value = gpio_get_value(dev->irqkey[0].gpio); 7 if(value == 0){ 8 //按下 9 atomic_set(&dev->keyvalue,dev->irqkey[0].value); 10 } 11 else{ 12 //释放 13 atomic_set(&dev->keyvalue,0x80|(dev->irqkey[0].value)); //将最高位置一,表示按钮释放 14 atomic_set(&dev->keyreleased,1); 15 } 16 17 //唤醒进程 18 if(atomic_read(&dev->keyreleased)){ 19 //进程唤醒 20 // wake_up_interruptible(&dev->r_wait); 21 wake_up(&dev->r_wait); 22 } 23 }
第21行的wake_up就是将队列头指向的等待任务唤醒,程序执行完毕,用户态APP拿到值后进行while循环下一个循环,又被阻塞
唤醒的函数有两种
void wake_up(wait_queue_head_t *q) void wake_up_interruptible(wait_queue_head_t *q)
区别就是wake_up可以唤醒处于TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE两种状态的进程,而第二个只能唤醒TASK_INTERRUPTIBLE 状态的进程。
下面值最终的驱动程序
/** * @file blockIO.c * @author your name (you@domain.com) * @brief 阻塞IO、等待事件 * @version 0.1 * @date 2022-08-01 * * @copyright Copyright (c) 2022 * */ #include <linux/module.h> #include <linux/kernel.h> #include <linux/init.h> #include <linux/fs.h> #include <linux/uaccess.h> #include <linux/io.h> #include <linux/types.h> #include <linux/cdev.h> #include <linux/device.h> #include <linux/of.h> #include <linux/of_address.h> #include <linux/of_irq.h> #include <linux/gpio.h> #include <linux/of_gpio.h> #include <linux/irq.h> #include <linux/interrupt.h> #include <linux/wait.h> #include <linux/ide.h> #define DEVICE_CNT 1 #define DEVICE_NAME "imx6uirq" #define KEY_NUM 1 #define KEY0VALUE 0x01 #define INVALKEYS 0xFF /** * @brief 按键中断结构体 * */ struct irq_keydesc { int gpio; //io编号 int irqnum; //中断号 unsigned char value; //键值 char name[10]; //按键名字 irqreturn_t (*handler)(int,void*); //中断处理函数 }; /** * @brief 设备结构体 * */ struct new_dev { dev_t dev_id; int major; int minor; struct class *class; struct device *device; struct cdev cdev; struct device_node *dev_nd; int dev_gpio; struct irq_keydesc irqkey[KEY_NUM]; //按键描述数组 struct timer_list timer; //定时器 int timer_per; atomic_t keyvalue; atomic_t keyreleased; //1表示1次有效按键并被释放,0表示未被释放 wait_queue_head_t r_wait; //读等待队列头 }; struct new_dev new_dev; static int new_dev_open(struct inode *inode, struct file *filp) { filp->private_data = &new_dev; /* 设置私有数据 */ return 0; } static ssize_t new_dev_read(struct file *filp,char __user *buf,size_t cnt,loff_t *offt) { int ret = 0; unsigned char keyvalue; unsigned char keyreleased; struct new_dev *dev = filp->private_data; //私有数据 wait_event_interruptible(dev->r_wait,atomic_read(&dev->keyreleased)); //等待按键有效 // wait_event(dev->r_wait,atomic_read(&dev->keyreleased)); //等待按键有效 keyvalue = atomic_read(&dev->keyvalue); //获取按键状态标志及按键值 keyreleased = atomic_read(&dev->keyreleased); if(keyreleased){ //出现按键被按下并释放的过程 if(keyvalue&0x80){ //获取最高位,如果为1表示出现过按键被按下后释放掉状态,是有效状态 keyvalue &= ~0x80; ret = copy_to_user(buf,&keyvalue,sizeof(keyvalue)); } else{ goto data_err; } atomic_set(&dev->keyreleased,0); //恢复keyreleased的状态 } else{ goto data_err; //返回负数,在用户态跳出循环 } return ret; //异常处理 data_err: return -EINVAL; }; /** * @brief 文件操作集合 * */ static const struct file_operations key_fops = { .owner = THIS_MODULE, // .write = new_dev_write, .open = new_dev_open, .read = new_dev_read, // .release = new_dev_release, }; static irqreturn_t key0_handle_irq(int irq, void *dev_id) { int value = 0; struct new_dev *dev = dev_id; dev->timer.data = dev_id; mod_timer(&dev->timer,jiffies + msecs_to_jiffies(10)); return IRQ_HANDLED; } static void timer_func(unsigned long arg){ int value = 0; struct new_dev *dev =(struct new_dev*)arg; value = gpio_get_value(dev->irqkey[0].gpio); if(value == 0){ //按下 atomic_set(&dev->keyvalue,dev->irqkey[0].value); } else{ //释放 atomic_set(&dev->keyvalue,0x80|(dev->irqkey[0].value)); //将最高位置一,表示按钮释放 atomic_set(&dev->keyreleased,1); } //唤醒进程 if(atomic_read(&dev->keyreleased)){ //进程唤醒 // wake_up_interruptible(&dev->r_wait); wake_up(&dev->r_wait); } } static int dev_gpio_init(struct new_dev *dev) { int ret = 0; int i = 0; //搜索设备树节点 dev->dev_nd = of_find_node_by_path("/key"); if(dev->dev_nd == NULL){ printk("can't find device key\r\n"); ret = -EINVAL; goto fail_nd; } for(i=0;i<KEY_NUM;i++) { dev->irqkey[i].gpio = of_get_named_gpio(dev->dev_nd,"key-gpios",i); //多个按键获取 if(dev->irqkey[i].gpio<0){ ret = -EINVAL; goto fail_gpio_num; } ret = gpio_request(dev->irqkey[i].gpio,dev->irqkey[i].name); if(ret){ ret = -EBUSY; goto fail_gpio_request; } gpio_direction_input(dev->irqkey[i].gpio); dev->irqkey[i].irqnum = gpio_to_irq(dev->irqkey[i].gpio); //获取中断号 // dev->irqkey[i].irqnum = irq_of_parse_and_map(dev->dev_nd,i) //方法2获取中断号 } dev->irqkey[0].handler = key0_handle_irq; dev->irqkey[0].value = KEY0VALUE; for(i=0;i<KEY_NUM;i++){ memset(dev->irqkey[i].name,0,sizeof(dev->irqkey[i].name)); sprintf(dev->irqkey[i].name,"KEY%d",i); //将格式化数据写入字符串中 ret = request_irq(dev->irqkey[i].irqnum, //中断号 key0_handle_irq, //中断处理函数 IRQ_TYPE_EDGE_RISING|IRQ_TYPE_EDGE_FALLING, //中断处理函数 dev->irqkey[i].name, //中断名称 dev //设备结构体 ); if(ret){ printk("irq %d request err\r\n",dev->irqkey[i].irqnum); goto fail_irq; } } //此处不设置定时值,防止定时器add后直接运行 init_timer(&dev->timer); dev->timer.function = timer_func; return 0; fail_gpio_request: fail_irq: for(i=0; i<KEY_NUM;i++){ gpio_free(dev->irqkey[i].gpio); } fail_gpio_num: fail_nd: return ret; } static int __init new_dev_init(void){ int ret = 0; //申请设备号 new_dev.major = 0; if(new_dev.major){ //手动指定设备号,使用指定的设备号 new_dev.dev_id = MKDEV(new_dev.major,0); ret = register_chrdev_region(new_dev.dev_id,DEVICE_CNT,DEVICE_NAME); } else{ //设备号未指定,申请设备号 ret = alloc_chrdev_region(&new_dev.dev_id,0,DEVICE_CNT,DEVICE_NAME); new_dev.major = MAJOR(new_dev.dev_id); new_dev.minor = MINOR(new_dev.dev_id); } printk("dev id geted!\r\n"); if(ret<0){ //设备号申请异常,跳转至异常处理 goto faile_devid; } //字符设备cdev初始化 new_dev.cdev.owner = THIS_MODULE; cdev_init(&new_dev.cdev,&key_fops); //文件操作集合映射 ret = cdev_add(&new_dev.cdev,new_dev.dev_id,DEVICE_CNT); if(ret<0){ //cdev初始化异常,跳转至异常处理 goto fail_cdev; } printk("chr dev inited!\r\n"); //自动创建设备节点 new_dev.class = class_create(THIS_MODULE,DEVICE_NAME); if(IS_ERR(new_dev.class)){ //class创建异常处理 printk("class err!\r\n"); ret = PTR_ERR(new_dev.class); goto fail_class; } printk("dev class created\r\n"); new_dev.device = device_create(new_dev.class,NULL,new_dev.dev_id,NULL,DEVICE_NAME); if(IS_ERR(new_dev.device)){ //设备创建异常处理 printk("device err!\r\n"); ret = PTR_ERR(new_dev.device); goto fail_device; } printk("device created!\r\n"); ret = dev_gpio_init(&new_dev); if(ret<0){ goto fail_gpio_init; } //初始化原子变量 atomic_set(&new_dev.keyvalue,INVALKEYS); atomic_set(&new_dev.keyreleased,0); //初始化等待队列头 init_waitqueue_head(&new_dev.r_wait); return ret; fail_gpio_init: fail_device: //device创建失败,意味着class创建成功,应该将class销毁 printk("device create err,class destroyed\r\n"); class_destroy(new_dev.class); fail_class: //类创建失败,意味着设备应该已经创建成功,此刻应将其释放掉 printk("class create err,cdev del\r\n"); cdev_del(&new_dev.cdev); fail_cdev: //cdev初始化异常,意味着设备号已经申请完成,应将其释放 printk("cdev init err,chrdev register\r\n"); unregister_chrdev_region(new_dev.dev_id,DEVICE_CNT); faile_devid: //设备号申请异常,由于是第一步操作,不需要进行其他处理 printk("dev id err\r\n"); return ret; } static void __exit new_dev_exit(void){ int i = 0; //释放中断 for(i=0;i<KEY_NUM;i++){ free_irq(new_dev.irqkey[i].irqnum,&new_dev); } for(i=0;i<KEY_NUM;i++){ gpio_free(new_dev.irqkey[i].gpio); } del_timer_sync(&new_dev.timer); cdev_del(&new_dev.cdev); unregister_chrdev_region(new_dev.dev_id,DEVICE_CNT); device_destroy(new_dev.class,new_dev.dev_id); class_destroy(new_dev.class); } module_init(new_dev_init); module_exit(new_dev_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("ZeqiZ");blockIO
总而言之就是一句话:使用方法可以看出来,通过队列头和wake_up,可以针对一个进程进行阻塞和唤醒。
等待队列项
前面的过程是针对一个进程进行阻塞,可以有些时候驱动文件是不会只有一个进程进行访问的。如果有多个进程都需要对其进行访问,就不能光使用队列头了,这时候就需要等待队列的后续内容:等待队列项了。每一个访问设备的进程在等待队列里都是一个等待队列项,当设备不可用的时候,就要把这些进程添加到等待队列里。等待队列项用一个结构体来形容,内容如下:
1 struct __wait_queue { 2 unsigned int flags; 3 void *private; 4 wait_queue_func_t func; 5 struct list_head task_list; 6 }; 7 8 typedef struct __wait_queue wait_queue_t; 9 typedef int (*wait_queue_func_t)(wait_queue_t *wait, unsigned mode, int flags, void *key);
参数flags用来标识队列元素状态和属性
*private用来指向关联进程task_struct结构体指针
*func用来指向等待队列被唤醒时的回调函数。
task_list是用来链接列表的指向(双向),在上面将队列头的时候说过了。 flags的定义在wait.h里有/* __wait_queue::flags */ #define WQ_FLAG_EXCLUSIVE 0x01 #define WQ_FLAG_WOKEN 0x02
具体的含义暂时还不了解,我们这会还用不到,就不说了。
队列项的初始化
等待队列项在使用时需要先初始化,我们一般使用一个宏来对其声明并初始化
#define DECLARE_WAITQUEUE(name, tsk) \ wait_queue_t name = __WAITQUEUE_INITIALIZER(name, tsk)
参数name是我们新创建的队列项的名称
参数tsk是进程,我们一般使用全局变量current来将当前进程设置为一个等待队列项。
将进程添加到等待队列中
如果设备不可被访问时,我们要讲进程对应的等待队列项链接到前面创建的等待队列头后构成等待队列,只有添加到等待队列中的进程才能进入休眠状态,在设备可以被访问后再将队列项从队列中移出即可,添加和移出的API如下:
extern void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait); extern void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);
参数wait_queue_head_t就是我们前面创建的队列头
*wait就是需要休眠进程对应的队列项
这里看下对应的read函数操作
1 static ssize_t new_dev_read(struct file *filp,char __user *buf,size_t cnt,loff_t *offt) 2 { 3 int ret = 0; 4 unsigned char keyvalue; 5 unsigned char keyreleased; 6 struct new_dev *dev = filp->private_data; //私有数据 7 8 DECLARE_WAITQUEUE(wait,current); //定义并初始化等待队列项 名称为wait,指向当前进程 9 add_wait_queue(&dev->r_wait,&wait); //将队列项wait链接到队列头 10 __set_current_state(TASK_INTERRUPTIBLE); //将当前任务设置为可被中断 11 schedule(); //切换,进入休眠状态 12 13 //按键未按下,通过信号唤醒后运行 14 if(signal_pending(current)){ //判定当前是否有信号 15 //signal_pending函数用于判定当前进程是否有信号,不为0时表示有信号需要处理 16 ret = -ERESTARTSYS; 17 goto data_err; 18 } 19 20 //按键按下,将任务移出队列 21 __set_current_state(TASK_RUNNING); //将当前任务设置为运行状态 22 remove_wait_queue(&dev->r_wait,&wait); 23 24 keyvalue = atomic_read(&dev->keyvalue); //获取按键状态标志及按键值 25 keyreleased = atomic_read(&dev->keyreleased); 26 27 if(keyreleased){ //出现按键被按下并释放的过程 28 if(keyvalue&0x80){ 29 //获取最高位,如果为1表示出现过按键被按下后释放掉状态,是有效状态 30 keyvalue &= ~0x80; 31 ret = copy_to_user(buf,&keyvalue,sizeof(keyvalue)); 32 } 33 else{ 34 ret = -EINVAL; 35 goto data_err; 36 } 37 atomic_set(&dev->keyreleased,0); //恢复keyreleased的状态 38 } 39 else{ 40 goto data_err; //返回负数,在用户态跳出循环 41 } 42 return 0; 43 //异常处理 44 data_err: 45 __set_current_state(TASK_RUNNING); //将当前任务设置为运行状态 46 remove_wait_queue(&dev->r_wait,&wait); 47 return ret; 48 }
在用户态APP调用read函数以后,new_dev_read函数被调用。
第8行,我们通过宏声明了一个名称为wait的等待队列项,并指向当前进程
第9行将队列项wait添加至等待队列
第10行,当前进程设置为TASK_INTERRUPTIBLE状态,这是针对等待某事件或其他资源而睡眠的进程设置的。在内核发送信号给该进程时表明等待的事件已经发生或资源已经可用,进程状态变为 TASK_RUNNING,此时只要被调度器选中就立即可恢复运行。
第11行调用sechedule()函数让进程直接进入睡眠状态,进程在定时器回调函数中通过wake_up()唤醒等待队列里的进程开始继续执行13行开始的内容
第13行,我们先用signal_pending(current)函数判定当前进程是否有信号需要处理(kill或Ctrl+C),函数返回值不为0时说明进程是被信号唤醒的,这时不需要对按键产生的中断进行相应,通过goto语句跳转至第44行的异常处理将当前进程设置为运行状态并将其移出等待队列。
第20行开始,我们跳过了前面的if判定,说明进程是被通过定时器里的wake_up函数唤醒,直接切换进程运行状态(TASK_RUNNING,表示进程处于可运行状态,但并不意味着进程已经实际上已分配到 CPU ,它可能会一直等到调度器选中它。该状态只是确保进程一旦被 CPU 选中时立马可以运行,而无需等待外部事件。)然后进程被移出队列,直接开始执行下面的内容。
下面是整个驱动的代码
/** * @file blockIO_wq.c * @author your name (you@domain.com) * @brief 阻塞IO——等待队列版本 * @version 0.1 * @date 2022-08-02 * * @copyright Copyright (c) 2022 * */ #include <linux/module.h> #include <linux/kernel.h> #include <linux/init.h> #include <linux/fs.h> #include <linux/uaccess.h> #include <linux/io.h> #include <linux/types.h> #include <linux/cdev.h> #include <linux/device.h> #include <linux/of.h> #include <linux/of_address.h> #include <linux/of_irq.h> #include <linux/gpio.h> #include <linux/of_gpio.h> #include <linux/irq.h> #include <linux/interrupt.h> #include <linux/wait.h> #include <linux/ide.h> #define DEVICE_CNT 1 #define DEVICE_NAME "imx6uirq" #define KEY_NUM 1 #define KEY0VALUE 0x01 #define INVALKEYS 0xFF /** * @brief 按键中断结构体 * */ struct irq_keydesc { int gpio; //io编号 int irqnum; //中断号 unsigned char value; //键值 char name[10]; //按键名字 irqreturn_t (*handler)(int,void*); //中断处理函数 }; /** * @brief 设备结构体 * */ struct new_dev { dev_t dev_id; int major; int minor; struct class *class; struct device *device; struct cdev cdev; struct device_node *dev_nd; int dev_gpio; struct irq_keydesc irqkey[KEY_NUM]; //按键描述数组 struct timer_list timer; //定时器 int timer_per; atomic_t keyvalue; atomic_t keyreleased; //1表示1次有效按键并被释放,0表示未被释放 wait_queue_head_t r_wait; //读等待队列头 }; struct new_dev new_dev; static int new_dev_open(struct inode *inode, struct file *filp) { filp->private_data = &new_dev; /* 设置私有数据 */ return 0; } static ssize_t new_dev_read(struct file *filp,char __user *buf,size_t cnt,loff_t *offt) { int ret = 0; unsigned char keyvalue; unsigned char keyreleased; struct new_dev *dev = filp->private_data; //私有数据 DECLARE_WAITQUEUE(wait,current); //定义并初始化等待队列项 名称为wait,指向当前进程 add_wait_queue(&dev->r_wait,&wait); //将队列项wait链接到队列头 __set_current_state(TASK_INTERRUPTIBLE); //将当前任务设置为可被中断 schedule(); //切换,进入休眠状态 //按键未按下,通过信号唤醒后运行 if(signal_pending(current)){ //判定当前是否有信号 //signal_pending函数用于判定当前进程是否有信号,不为0时表示有信号需要处理 ret = -ERESTARTSYS; goto data_err; } //按键按下,将任务移出队列 __set_current_state(TASK_RUNNING); //将当前任务设置为运行状态 remove_wait_queue(&dev->r_wait,&wait); keyvalue = atomic_read(&dev->keyvalue); //获取按键状态标志及按键值 keyreleased = atomic_read(&dev->keyreleased); if(keyreleased){ //出现按键被按下并释放的过程 if(keyvalue&0x80){ //获取最高位,如果为1表示出现过按键被按下后释放掉状态,是有效状态 keyvalue &= ~0x80; ret = copy_to_user(buf,&keyvalue,sizeof(keyvalue)); } else{ ret = -EINVAL; goto data_err; } atomic_set(&dev->keyreleased,0); //恢复keyreleased的状态 } else{ goto data_err; //返回负数,在用户态跳出循环 } return 0; //异常处理 data_err: __set_current_state(TASK_RUNNING); //将当前任务设置为运行状态 remove_wait_queue(&dev->r_wait,&wait); return ret; } /** * @brief 文件操作集合 * */ static const struct file_operations key_fops = { .owner = THIS_MODULE, // .write = new_dev_write, .open = new_dev_open, .read = new_dev_read, // .release = new_dev_release, }; static irqreturn_t key0_handle_irq(int irq, void *dev_id) { int value = 0; struct new_dev *dev = dev_id; dev->timer.data = dev_id; mod_timer(&dev->timer,jiffies + msecs_to_jiffies(10)); return IRQ_HANDLED; } static void timer_func(unsigned long arg){ int value = 0; struct new_dev *dev =(struct new_dev*)arg; value = gpio_get_value(dev->irqkey[0].gpio); if(value == 0){ //按下 atomic_set(&dev->keyvalue,dev->irqkey[0].value); } else{ //释放 atomic_set(&dev->keyvalue,0x80|(dev->irqkey[0].value)); //将最高位置一,表示按钮释放 atomic_set(&dev->keyreleased,1); } //唤醒进程 if(atomic_read(&dev->keyreleased)){ //进程唤醒 // wake_up_interruptible(&dev->r_wait); wake_up(&dev->r_wait); } } static int dev_gpio_init(struct new_dev *dev) { int ret = 0; int i = 0; //搜索设备树节点 dev->dev_nd = of_find_node_by_path("/key"); if(dev->dev_nd == NULL){ printk("can't find device key\r\n"); ret = -EINVAL; goto fail_nd; } for(i=0;i<KEY_NUM;i++) { dev->irqkey[i].gpio = of_get_named_gpio(dev->dev_nd,"key-gpios",i); //多个按键获取 if(dev->irqkey[i].gpio<0){ ret = -EINVAL; goto fail_gpio_num; } ret = gpio_request(dev->irqkey[i].gpio,dev->irqkey[i].name); if(ret){ ret = -EBUSY; goto fail_gpio_request; } gpio_direction_input(dev->irqkey[i].gpio); dev->irqkey[i].irqnum = gpio_to_irq(dev->irqkey[i].gpio); //获取中断号 // dev->irqkey[i].irqnum = irq_of_parse_and_map(dev->dev_nd,i) //方法2获取中断号 } dev->irqkey[0].handler = key0_handle_irq; dev->irqkey[0].value = KEY0VALUE; for(i=0;i<KEY_NUM;i++){ memset(dev->irqkey[i].name,0,sizeof(dev->irqkey[i].name)); sprintf(dev->irqkey[i].name,"KEY%d",i); //将格式化数据写入字符串中 ret = request_irq(dev->irqkey[i].irqnum, //中断号 key0_handle_irq, //中断处理函数 IRQ_TYPE_EDGE_RISING|IRQ_TYPE_EDGE_FALLING, //中断处理函数 dev->irqkey[i].name, //中断名称 dev //设备结构体 ); if(ret){ printk("irq %d request err\r\n",dev->irqkey[i].irqnum); goto fail_irq; } } //此处不设置定时值,防止定时器add后直接运行 init_timer(&dev->timer); dev->timer.function = timer_func; return 0; fail_gpio_request: fail_irq: for(i=0; i<KEY_NUM;i++){ gpio_free(dev->irqkey[i].gpio); } fail_gpio_num: fail_nd: return ret; } static int __init new_dev_init(void){ int ret = 0; //申请设备号 new_dev.major = 0; if(new_dev.major){ //手动指定设备号,使用指定的设备号 new_dev.dev_id = MKDEV(new_dev.major,0); ret = register_chrdev_region(new_dev.dev_id,DEVICE_CNT,DEVICE_NAME); } else{ //设备号未指定,申请设备号 ret = alloc_chrdev_region(&new_dev.dev_id,0,DEVICE_CNT,DEVICE_NAME); new_dev.major = MAJOR(new_dev.dev_id); new_dev.minor = MINOR(new_dev.dev_id); } printk("dev id geted!\r\n"); if(ret<0){ //设备号申请异常,跳转至异常处理 goto faile_devid; } //字符设备cdev初始化 new_dev.cdev.owner = THIS_MODULE; cdev_init(&new_dev.cdev,&key_fops); //文件操作集合映射 ret = cdev_add(&new_dev.cdev,new_dev.dev_id,DEVICE_CNT); if(ret<0){ //cdev初始化异常,跳转至异常处理 goto fail_cdev; } printk("chr dev inited!\r\n"); //自动创建设备节点 new_dev.class = class_create(THIS_MODULE,DEVICE_NAME); if(IS_ERR(new_dev.class)){ //class创建异常处理 printk("class err!\r\n"); ret = PTR_ERR(new_dev.class); goto fail_class; } printk("dev class created\r\n"); new_dev.device = device_create(new_dev.class,NULL,new_dev.dev_id,NULL,DEVICE_NAME); if(IS_ERR(new_dev.device)){ //设备创建异常处理 printk("device err!\r\n"); ret = PTR_ERR(new_dev.device); goto fail_device; } printk("device created!\r\n"); ret = dev_gpio_init(&new_dev); if(ret<0){ goto fail_gpio_init; } //初始化原子变量 atomic_set(&new_dev.keyvalue,INVALKEYS); atomic_set(&new_dev.keyreleased,0); //初始化等待队列头 init_waitqueue_head(&new_dev.r_wait); return ret; fail_gpio_init: fail_device: //device创建失败,意味着class创建成功,应该将class销毁 printk("device create err,class destroyed\r\n"); class_destroy(new_dev.class); fail_class: //类创建失败,意味着设备应该已经创建成功,此刻应将其释放掉 printk("class create err,cdev del\r\n"); cdev_del(&new_dev.cdev); fail_cdev: //cdev初始化异常,意味着设备号已经申请完成,应将其释放 printk("cdev init err,chrdev register\r\n"); unregister_chrdev_region(new_dev.dev_id,DEVICE_CNT); faile_devid: //设备号申请异常,由于是第一步操作,不需要进行其他处理 printk("dev id err\r\n"); return ret; } static void __exit new_dev_exit(void){ int i = 0; //释放中断 for(i=0;i<KEY_NUM;i++){ free_irq(new_dev.irqkey[i].irqnum,&new_dev); } for(i=0;i<KEY_NUM;i++){ gpio_free(new_dev.irqkey[i].gpio); } del_timer_sync(&new_dev.timer); cdev_del(&new_dev.cdev); unregister_chrdev_region(new_dev.dev_id,DEVICE_CNT); device_destroy(new_dev.class,new_dev.dev_id); class_destroy(new_dev.class); } module_init(new_dev_init); module_exit(new_dev_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("ZeqiZ");View Code
然后把APP的代码也放出来
/** * @file irqAPP.c * @author your name (you@domain.com) * @brief 按键中断APP测试程序 * @version 0.1 * @date 2022-07-26 * * @copyright Copyright (c) 2022 * */ #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <sys/ioctl.h> /** * @brief * * @param argc //参数个数 * @param argv //参数 * @return int */ int main(int argc,char *argv[]) { char *filename; //文件名 filename = argv[1]; // int value = 0; int ret = 0; //初始化操作返回值 int f = 0; //初始化文件句柄 unsigned char data; //内核传来的值 f = open(filename, O_RDWR); //打开文件 if(f < 0){ printf("file open error\r\n"); return -1; } while(1){ ret = read(f,&data,sizeof(data)); if(ret<0){ //读取错误 printf("data read err\r\n") } else{ if(data) //获取数据有效 printf("key value = %#X\r\n",data); } } close(f); //关闭文件 return 0; }测试APP
加载模块以后运行一下
可以看出来CPU占用率是很低的,这样做就没问题了!