??按键驱动是操作 GPIO,在驱动程序中使用一个整形变量来表示按键值,应用程序通过 read 函数来读取按键值,判断按键有没有按下。 ??因为保存按键值的变量是个共享资源,驱动程序要向其写入按键值,应用程序要读取按键值。所以要对其进行保护,对于整形变量而言首选的是原子操作,使用原子操作对变量进行赋值以及读取。 ??读取按键一般采用中断的方式,并且采用定时器来实现按键消抖。此外在编写驱动的时候一定要考虑到阻塞和非阻塞的情况。对于按键输入驱动程序,最好的方式就是驱动程序能主动向应用程序发出通知,报告自己可以访问,然后应用程序在从驱动程序中读取或写入数据,我们通过异步通知这个机制来完成此功能。 ??因此,Linux 按键输入驱动的编写,要涉及到共享资源的原子操作,中断、定时器、阻塞、非阻塞和异步通知等功能。 ??在实际的工程中按键驱动一般使用 Linux 下的 input 子系统专门用于输入设备。
一、驱动程序
#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 <linux/timer.h>
#include <linux/of_irq.h>
#include <linux/irq.h>
#include <linux/fcntl.h>
#include <linux/wait.h>
#include <linux/poll.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#define MYTESTIRQ_NAME "mytest_key"
struct irq_keydesc {
int gpio;
int irqnum;
unsigned char value;
char name[10];
irqreturn_t (*handler)(int, void *);
};
struct mytestirq_dev{
dev_t devid;
struct cdev cdev;
struct class *class;
struct device *device;
int major;
int minor;
struct device_node *nd;
atomic_t keyvalue;
atomic_t releasekey;
struct timer_list timer;
struct irq_keydesc irqkeydesc;
unsigned char curkeynum;
wait_queue_head_t r_wait;
struct fasync_struct *async_queue;
};
struct mytestirq_dev mytestirq;
static irqreturn_t key0_handler(int irq, void *dev_id)
{
struct mytestirq_dev *dev = (struct mytestirq_dev*)dev_id;
dev->curkeynum = 0;
dev->timer.data = (volatile long)dev_id;
mod_timer(&dev->timer, jiffies + msecs_to_jiffies(10));
return IRQ_RETVAL(IRQ_HANDLED);
}
void timer_function(unsigned long arg)
{
unsigned char value;
unsigned char num;
struct irq_keydesc *keydesc;
struct mytestirq_dev *dev = (struct mytestirq_dev *)arg;
num = dev->curkeynum;
keydesc = &dev->irqkeydesc;
value = gpio_get_value(keydesc->gpio);
if(value == 0)
{
atomic_set(&dev->keyvalue, keydesc->value);
}else
{
atomic_set(&dev->keyvalue, 0x80 | keydesc->value);
atomic_set(&dev->releasekey, 1);
}
if(atomic_read(&dev->releasekey))
{
if(dev->async_queue)
kill_fasync(&dev->async_queue, SIGIO, POLL_IN);
}
}
static int keyio_init(void)
{
unsigned char i = 0;
char name[10];
int ret = 0;
mytestirq.nd = of_find_node_by_path("/key");
if (mytestirq.nd== NULL){
printk("key node not find!\r\n");
return -EINVAL;
}
mytestirq.irqkeydesc.gpio = of_get_named_gpio(mytestirq.nd ,"key-gpio", i);
if (mytestirq.irqkeydesc.gpio < 0)
{
printk("can't get key%d\r\n", i);
}
memset(mytestirq.irqkeydesc.name, 0, sizeof(name));
sprintf(mytestirq.irqkeydesc.name, "KEY%d", i);
gpio_request(mytestirq.irqkeydesc.gpio, name);
gpio_direction_input(mytestirq.irqkeydesc.gpio);
mytestirq.irqkeydesc.irqnum = irq_of_parse_and_map(mytestirq.nd, i);
mytestirq.irqkeydesc.handler = key0_handler;
mytestirq.irqkeydesc.value = 0X01;
ret = request_irq(mytestirq.irqkeydesc.irqnum, mytestirq.irqkeydesc.handler,
IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING, mytestirq.irqkeydesc.name, &mytestirq);
if(ret < 0)
{
printk("irq %d request failed!\r\n", mytestirq.irqkeydesc.irqnum);
return -EFAULT;
}
init_timer(&mytestirq.timer);
mytestirq.timer.function = timer_function;
init_waitqueue_head(&mytestirq.r_wait);
return 0;
}
static int mytestirq_open(struct inode *inode, struct file *filp)
{
filp->private_data = &mytestirq;
return 0;
}
static ssize_t mytestirq_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
int ret = 0;
unsigned char keyvalue = 0;
unsigned char releasekey = 0;
struct mytestirq_dev *dev = (struct mytestirq_dev *)filp->private_data;
if (filp->f_flags & O_NONBLOCK)
{
if(atomic_read(&dev->releasekey) == 0)
return -EAGAIN;
} else
{
ret = wait_event_interruptible(dev->r_wait, atomic_read(&dev->releasekey));
if (ret)
{
goto wait_error;
}
}
keyvalue = atomic_read(&dev->keyvalue);
releasekey = atomic_read(&dev->releasekey);
if (releasekey)
{
ret = copy_to_user(buf, &keyvalue, sizeof(keyvalue));
atomic_set(&dev->releasekey, 0);
} else
{
goto data_error;
}
return 0;
wait_error:
return ret;
data_error:
return -EINVAL;
}
unsigned int mytestirq_poll(struct file *filp, struct poll_table_struct *wait)
{
unsigned int mask = 0;
struct mytestirq_dev *dev = (struct mytestirq_dev *)filp->private_data;
poll_wait(filp, &dev->r_wait, wait);
if(atomic_read(&dev->releasekey))
{
mask = POLLIN | POLLRDNORM;
}
return mask;
}
static int mytestirq_fasync(int fd, struct file *filp, int on)
{
struct mytestirq_dev *dev = (struct mytestirq_dev *)filp->private_data;
return fasync_helper(fd, filp, on, &dev->async_queue);
}
static int mytestirq_release(struct inode *inode, struct file *filp)
{
return mytestirq_fasync(-1, filp, 0);
}
static struct file_operations mytestirq_fops = {
.owner = THIS_MODULE,
.open = mytestirq_open,
.read = mytestirq_read,
.poll = mytestirq_poll,
.fasync = mytestirq_fasync,
.release = mytestirq_release,
};
static int __init mytestirq_init(void)
{
if (mytestirq.major)
{
mytestirq.devid = MKDEV(mytestirq.major, 0);
register_chrdev_region(mytestirq.devid, 1, MYTESTIRQ_NAME);
} else
{
alloc_chrdev_region(&mytestirq.devid, 0, 1, MYTESTIRQ_NAME);
mytestirq.major = MAJOR(mytestirq.devid);
mytestirq.minor = MINOR(mytestirq.devid);
}
cdev_init(&mytestirq.cdev, &mytestirq_fops);
cdev_add(&mytestirq.cdev, mytestirq.devid, 1);
mytestirq.class = class_create(THIS_MODULE, MYTESTIRQ_NAME);
if (IS_ERR(mytestirq.class))
{
return PTR_ERR(mytestirq.class);
}
mytestirq.device = device_create(mytestirq.class, NULL, mytestirq.devid, NULL, MYTESTIRQ_NAME);
if (IS_ERR(mytestirq.device))
{
return PTR_ERR(mytestirq.device);
}
atomic_set(&mytestirq.keyvalue, 0XFF);
atomic_set(&mytestirq.releasekey, 0);
keyio_init();
return 0;
}
static void __exit mytestirq_exit(void)
{
unsigned i = 0;
del_timer_sync(&mytestirq.timer);
free_irq(mytestirq.irqkeydesc.irqnum, &mytestirq);
gpio_free(mytestirq.irqkeydesc.gpio);
cdev_del(&mytestirq.cdev);
unregister_chrdev_region(mytestirq.devid, 1);
device_destroy(mytestirq.class, mytestirq.devid);
class_destroy(mytestirq.class);
}
module_init(mytestirq_init);
module_exit(mytestirq_exit);
MODULE_LICENSE("GPL");
二、原子操作
??Linux 内核定义了叫做 atomic_t 的结构体来完成整形数据的原子操作,在使用中用原子变量来代替整形变量,此结构体定义在 include/linux/types.h 文件中。对原子变量进行操作的API函数主要有:int atomic_read(atomic_t *v) 和void atomic_set(atomic_t *v, int i) ??代码的第48和49行,就是定义了两个变量:
atomic_t keyvalue;
atomic_t releasekey;
??使用的方法有:
atomic_set(&dev->releasekey, 1);
atomic_read(&dev->releasekey)
三、中断
3.1、中断号
??每个中断都有一个中断号,通过中断号即可区分不同的中断,在 Linux 内核中使用一个 int 变量表示中断号。中断信息一般已经写到了设备树里面,因此可以通过 irq_of_parse_and_map 函数从 interupts 属性中提取到对应的设备号,函数原型如下:
unsigned int irq_of_parse_and_map(struct device_node *dev,int index)
??函数参数和返回值含义如下: ??dev: 设备节点。 ??index:索引号, interrupts 属性可能包含多条中断信息,通过 index 指定要获取的信息。 ??返回值:中断号。
3.2、request_irq 函数
??在 Linux 内核中要想使用某个中断是需要申请的, request_irq 函数用于申请中断,request_irq 函数原型如下:
int request_irq(unsigned int irq,irq_handler_t handler,unsigned long flags,const char *name,void *dev)
??函数参数和返回值含义如下: ??irq:要申请中断的中断号。 ??handler:中断处理函数,当中断发生以后就会执行此中断处理函数。 ??flags:中断标志,可以在文件 include/linux/interrupt.h 里面查看所有的中断标志 ??name:中断名字,设置以后可以在/proc/interrupts 文件中看到对应的中断名字。 ??dev: 如果将 flags 设置为 IRQF_SHARED 的话, dev 用来区分不同的中断,一般情况下将dev 设置为设备结构体, dev 会传递给中断处理函数 irq_handler_t 的第二个参数。 ??返回值: 0 中断申请成功,其他负值 中断申请失败,如果返回-EBUSY 的话表示中断已经被申请了。
3.3、free_irq 函数
??使用中断的时候需要通过 request_irq 函数申请,使用完成以后就要通过 free_irq 函数释放掉相应的中断。free_irq函数原型如下所示:
void free_irq(unsigned int irq,void *dev)
??函数参数和返回值含义如下: ??irq: 要释放的中断。 ??dev:如果中断设置为共享(IRQF_SHARED)的话,此参数用来区分具体的中断。 ??返回值:无
3.4、中断处理函数
??使用 request_irq 函数申请中断的时候需要设置中断处理函数,中断处理函数格式如下所示:
irqreturn_t (*irq_handler_t) (int, void *)
??第一个参数是要中断处理函数要相应的中断号。第二个参数是一个指向 void 的指针,也就是个通用指针,需要与 request_irq 函数的 dev 参数保持一致。用于区分共享中断的不同设备,dev 也可以指向设备数据结构。 ??中断处理函数的返回值为 irqreturn_t 类型。
3.5、代码中对中断的使用
??第一、在keyio_init()函数内获取中断号:
mytestirq.irqkeydesc.irqnum = irq_of_parse_and_map(mytestirq.nd, i);
??第二、在keyio_init()函数内申请中断
mytestirq.irqkeydesc.handler = key0_handler;
mytestirq.irqkeydesc.value = 0X01;
ret = request_irq(mytestirq.irqkeydesc.irqnum, mytestirq.irqkeydesc.handler, IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING, mytestirq.irqkeydesc.name, &mytestirq);
??其中中断号为:mytestirq.irqkeydesc.irqnum ,中断处理函数定义为 mytestirq.irqkeydesc.handler = key0_handler; ??第三、中断处理函数内容为:
static irqreturn_t key0_handler(int irq, void *dev_id)
{
struct mytestirq_dev *dev = (struct mytestirq_dev*)dev_id;
dev->curkeynum = 0;
dev->timer.data = (volatile long)dev_id;
mod_timer(&dev->timer, jiffies + msecs_to_jiffies(10));
return IRQ_RETVAL(IRQ_HANDLED);
}
??在中断处理函数内开启定时器,消除按键抖动。 ??第四、在mytestirq_exit 函数内释放中断
free_irq(mytestirq.irqkeydesc.irqnum, &mytestirq);
四、定时器
??Linux 内核定采用系统时钟来实现时器,linux内核通过一系列的 API 函数来初始化此定时器。 Linux 内核定时器使用很简单,只需要提供超时时间(相当于定时值)和定时处理函数即可,当超时时间到了以后设置的定时处理函数就会执行,和我们使用硬件定时器的套路一样,只是使用内核定时器不需要做一大堆的寄存器初始化工作。在使用内核定时器的时候要注意一点,内核定时器并不是周期性运行的,超时以后就会自动关闭,因此如果想要实现周期性定时,那么就需要在定时处理函数中重新开启定时器。
4.1、timer_list 结构体
??Linux 内核使用 timer_list 结构体表示内核定时器, timer_list 定义在文件include/linux/timer.h 中
struct timer_list {
struct list_head entry;
unsigned long expires;
struct tvec_base *base;
void (*function)(unsigned long);
unsigned long data;
int slack;
};
??要使用内核定时器首先要先定义一个 timer_list 变量,表示定时器, tiemr_list 结构体的expires 成员变量表示超时时间,单位为节拍数。比如我们现在需要定义一个周期为 2 秒的定时器,那么这个定时器的超时时间就是 jiffies+(2HZ),因此 expires=jiffies+(2HZ)。 function 就是定时器超时以后的定时处理函数,我们要做的工作就放到这个函数里面,需要我们编写这个定时处理函数。
4.2、init_timer 函数
??init_timer 函数负责初始化 timer_list 类型变量,init_timer 函数原型如下:
void init_timer(struct timer_list *timer)
??timer:要初始化定时器。
4.3、mod_timer 函数
??mod_timer 函数用于修改定时值,如果定时器还没有激活的话, mod_timer 函数会激活定时器。函数原型如下:
int mod_timer(struct timer_list *timer, unsigned long expires)
??函数参数和返回值含义如下: ??timer:要修改超时时间(定时值)的定时器。 ??expires:修改后的超时时间。 ??返回值: 0,调用 mod_timer 函数前定时器未被激活; 1,调用 mod_timer 函数前定时器已被激活
4.4、del_timer_sync 函数
??del_timer_sync 函数用于删除一个定时器,不管定时器有没有被激活,都可以使用此函数删除,del_timer_sync 会等待其他处理器使用完定时器再删除。
4.5、内核定时器一般的使用流程
struct timer_list timer;
void function(unsigned long arg)
{
mod_timer(&dev->timertest, jiffies + msecs_to_jiffies(2000));
}
void init(void)
{
init_timer(&timer);
timer.function = function;
timer.expires=jffies + msecs_to_jiffies(2000);
timer.data = (unsigned long)&dev;
add_timer(&timer);
}
void exit(void)
{
del_timer(&timer);
del_timer_sync(&timer);
}
4.6、代码中定时器的使用
??第一、定义一个timer_list 的定时器
struct timer_list timer;
??第二、创建定时器,并声明回调函数为:timer_function
init_timer(&mytestirq.timer);
mytestirq.timer.function = timer_function;
??第三、定义定时处理回调函数
void timer_function(unsigned long arg)
{
unsigned char value;
unsigned char num;
struct irq_keydesc *keydesc;
struct mytestirq_dev *dev = (struct mytestirq_dev *)arg;
num = dev->curkeynum;
keydesc = &dev->irqkeydesc;
value = gpio_get_value(keydesc->gpio);
if(value == 0)
{
atomic_set(&dev->keyvalue, keydesc->value);
}else
{
atomic_set(&dev->keyvalue, 0x80 | keydesc->value);
atomic_set(&dev->releasekey, 1);
}
if(atomic_read(&dev->releasekey))
{
if(dev->async_queue)
kill_fasync(&dev->async_queue, SIGIO, POLL_IN);
}
}
??第四、在按键中断函数key0_handler 内更改定时器。
dev->curkeynum = 0;
dev->timer.data = (volatile long)dev_id;
mod_timer(&dev->timer, jiffies + msecs_to_jiffies(10));
??第五、在mytestirq_exit函数内删除定时器
del_timer_sync(&mytestirq.timer);
五、阻塞 IO
??当应用程序对设备驱动进行操作的时候,如果不能获取到设备资源,那么阻塞式 IO 就会将应用程序对应的线程挂起,直到设备资源可以获取为止。对于非阻塞 IO,应用程序对应的线程不会挂起,它要么一直轮询等待,直到设备资源可以使用,要么就直接放弃。 ??为了降低CPU 使用率,当程序以阻塞方式访问备时一般使用等待队列,而当程序以非阻塞方式访问备时一般使用轮询。 ??阻塞访问最大的好处就是当设备文件不可操作的时候进程可以进入休眠态,这样可以将CPU 资源让出来。但是,当设备文件可以操作的时候就必须唤醒进程,一般在中断函数里面完成唤醒工作。 Linux 内核提供了等待队列(wait queue)来实现阻塞进程的唤醒工作。
5.1、等待队列头
??如果我们要在驱动中使用等待队列,必须创建并初始化一个等待队列头,等待队列头使用结构体wait_queue_head_t 表示, wait_queue_head_t 结构体定义在文件 include/linux/wait.h 中,结构体内容为
struct __wait_queue_head {
spinlock_t lock;
struct list_head task_list;
};
typedef struct __wait_queue_head wait_queue_head_t;
5.2、init_waitqueue_head 函数
??定义好等待队列头以后需要初始化, 使用 init_waitqueue_head 函数初始化等待队列头,函数原型如下:
void init_waitqueue_head(wait_queue_head_t *q)
??参数 q 就是要初始化的等待队列头。
5.3、等待队列项
??等待队列头就是一个等待队列的头部,每个访问设备的进程都是一个队列项,当设备不可用的时候就要将这些进程对应的等待队列项添加到等待队列里面。结构体 wait_queue_t 表示等待队列项。使用宏 DECLARE_WAITQUEUE 定义并初始化一个等待队列项,宏的内容如下:
DECLARE_WAITQUEUE(name, tsk)
??name 就是等待队列项的名字, tsk 表示这个等待队列项属于哪个任务(进程),一般设置为current , 在 Linux 内 核 中 current 相 当 于 一 个 全 局 变 量 , 表 示 当 前 进 程 。 因 此 宏DECLARE_WAITQUEUE 就是给当前正在运行的进程创建并初始化了一个等待队列项
5.4、将队列项添加/移除等待队列头
??当设备不可访问的时候就需要将进程对应的等待队列项添加到前面创建的等待队列头中,只有添加到等待队列头中以后进程才能进入休眠态。当设备可以访问以后再将进程对应的等待队列项从等待队列头中移除即可,等待队列项添加 API 函数如下:
void add_wait_queue(wait_queue_head_t *q,wait_queue_t *wait)
??函数参数和返回值含义如下: ??q: 等待队列项要加入的等待队列头。 ??wait:要加入的等待队列项。 ??返回值:无。 ??等待队列项移除 API 函数如下:
void remove_wait_queue(wait_queue_head_t *q,wait_queue_t *wait)
??函数参数和返回值含义如下: ??q: 要删除的等待队列项所处的等待队列头。 ??wait:要删除的等待队列项。 ??返回值:无。
5.5、等待唤醒
??当设备可以使用的时候就要唤醒进入休眠态的进程,唤醒可以使用如下函数void wake_up_interruptible(wait_queue_head_t *q) ,参数 q 就是要唤醒的等待队列头
六、非阻塞 IO
??如果用户应用程序以非阻塞的方式访问设备,设备驱动程序就要提供非阻塞的处理方式,也就是轮询。 poll、 epoll 和 select 可以用于处理轮询,应用程序通过 select、 epoll 或 poll 函数来查询设备是否可以操作,如果可以操作的话就从设备读取或者向设备写入数据。当应用程序调用 select、 epoll 或 poll 函数的时候设备驱动程序中的 poll 函数就会执行,因此需要在设备驱动程序中编写 poll 函数。 ??当应用程序调用 select 或 poll 函数来对驱动程序进行非阻塞访问的时候,驱动程序file_operations 操作集中的 poll 函数就会执行。所以驱动程序的编写者需要提供对应的 poll 函数, poll 函数原型如下所示:
unsigned int (*poll) (struct file *filp, struct poll_table_struct *wait)
??函数参数和返回值含义如下: ??filp: 要打开的设备文件(文件描述符)。 ??wait: 结构体 poll_table_struct 类型指针, 由应用程序传递进来的。一般将此参数传递给poll_wait 函数。 ??返回值:向应用程序返回设备或者资源状态 ??我们需要在驱动程序的 poll 函数中调用 poll_wait 函数, poll_wait 函数不会引起阻塞,只是将应用程序添加到 poll_table 中, poll_wait 函数原型如下:
void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
??参数 wait_address 是要添加到 poll_table 中的等待队列头,参数 p 就是 poll_table,就是file_operations 中 poll 函数的 wait 参数。
七、异步通知
??在使用阻塞或者非阻塞的方式来读取驱动中按键值都是应用程序主动读取的,对于非阻塞方式来说还需要应用程序通过 poll 函数不断的轮询。还有一种方式就是驱动程序主动向应用程序发出通知,报告自己可以访问,然后应用程序在从驱动程序中读取或写入数据,类似于中断。 Linux 提供了异步通知这个机制来完成此功能。 ??阻塞、非阻塞、异步通知,这三种是针对不同的场合提出来的不同的解决方法,没有优劣之分,在实际的工作和学习中,根据自己的实际需求选择合适的处理方法即可。
7.1、fasync_struct 结构体
??在使用异步通知时,首先需要在驱动程序中定义一个 fasync_struct 结构体指针变量, fasync_struct 结构体内容如下:
struct fasync_struct {
spinlock_t fa_lock;
int magic;
int fa_fd;
struct fasync_struct *fa_next;
struct file *fa_file;
struct rcu_head fa_rcu;
};
7.2、fasync 函数
??如果要使用异步通知,需要在设备驱动中实现 file_operations 操作集中的 fasync 函数,此函数格式如下所示:
int (*fasync) (int fd, struct file *filp, int on)
??async 函数里面一般通过调用 fasync_helper 函数来初始化前面定义的 fasync_struct 结构体指针, fasync_helper 函数原型如下:
int fasync_helper(int fd, struct file * filp, int on, struct fasync_struct **fapp)
??fasync_helper 函数的前三个参数就是 fasync 函数的那三个参数,第四个参数就是要初始化的 fasync_struct 结构体指针变量。当应用程序通过“fcntl(fd, F_SETFL, flags | FASYNC)”改变fasync 标记的时候,驱动程序 file_operations 操作集中的 fasync 函数就会执行。 ??驱动程序中的 fasync 函数参考示例如下:
struct xxx_dev {
......
struct fasync_struct *async_queue;
};
static int xxx_fasync(int fd, struct file *filp, int on)
{
struct xxx_dev *dev = (xxx_dev)filp->private_data;
if (fasync_helper(fd, filp, on, &dev->async_queue) < 0)
return -EIO;
return 0;
}
static int xxx_release(struct inode *inode, struct file *filp)
{
return xxx_fasync(-1, filp, 0);
}
static struct file_operations xxx_ops = {
......
.fasync = xxx_fasync,
......
};
7.3、kill_fasync 函数
??当设备可以访问的时候,驱动程序需要向应用程序发出信号,相当于产生“中断”。 kill_fasync函数负责发送指定的信号, kill_fasync 函数原型如下所示:
void kill_fasync(struct fasync_struct **fp, int sig, int band)
??函数参数和返回值含义如下: ??fp:要操作的 fasync_struct。 ??sig: 要发送的信号。 ??band: 可读时设置为 POLL_IN,可写时设置为 POLL_OUT。 ??返回值: 无
|