Part13:按键驱动程序的不同实现->查询/中断/poll/异步通知/同步互斥阻塞
0 写在前面
part12文章是对ARM架构下Linux内核的异常(中断)处理体系结果的原理讲述,有了这些基本的认识后,下面根据不同实现方式
实现按键驱动程序的编写。
说明一点:下面的实现不会贴全部代码,代码可从https://blog.youkuaiyun.com/qq_42800075/article/details/105670841获取
1 按键驱动程序——查询
按键驱动程序:key_drv_polling.c
/* open函数用于配置4个引脚为输入 */
static int key_drv_open(struct inode* inode, struct file* file)
{
/*配置GPF0,2-->s2,3 GPG3-->s4为输入引脚*/
*pGPFCON &= ~((3<<0) | (3<<4));
*pGPGCON &= ~((3<<6) | (3<<22));
return 0;
}
/* read函数用于读取四个引脚状态 */
ssize_t key_drv_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
{
unsigned char key_vals[4];
int regval;
regval = *pGPFDAT;
key_vals[0] = (regval & (1<<0)) ? 1:0;
key_vals[1] = (regval & (1<<2)) ? 1:0;
regval = *pGPGDAT;
key_vals[2] = (regval & (1<<3)) ? 1:0;
key_vals[3] = (regval & (1<<11))? 1:0;
/* 返回引脚状态 */
copy_to_user(buf, key_vals, sizeof(key_vals));
return sizeof(key_vals);
}
按键驱动测试函数:key_drv_ts.c
int main(void)
{
int fd, val = 1;
int cnt = 0;
unsigned char key_val[4];
fd = open("/dev/key_drv", O_RDWR);
if(fd < 0)
printf("can't open /dev/key_drv!\n");
while(1)
{
// 不断查询引脚是否按下
read(fd, key_val, sizeof(key_val));
// 其一引脚按下则打印按键值
if(!key_val[0] || !key_val[1] || !key_val[2] || !key_val[3])
printf("%04d key pressed: %d %d %d %d\n", ++cnt, key_val[0],
key_val[1], key_val[2], key_val[3]);
}
return 0;
}
查询方式实现的按键驱动程序编写简单,但缺点也很明显,霸占CPU资源
2 按键驱动程序——中断
为减少CPU资源的占用,可使用中断方式。对于中断编程,按Part12文章讲述的三步走:
1)注册中断处理函数
2)实现中断处理函数
3)卸载中断处理函数
下面的驱动程序按这三步实现,代码如下
按键驱动程序:key_drv_itr.c
// 1 通过 request_irq 注册中断处理函数 key_irq
// 注册时,设置对应的中断IRQ_EINT0/2/11/19 触发方式:双边沿触发 dev_name dev_id
// 注册后,即将中断处理函数key_iqr添加到irq_desc[irqno]->action链表中
static int key_drv_open(struct inode* inode, struct file* file)
{
request_irq(IRQ_EINT0,&key_irq,IRQT_BOTHEDGE,"s2",&pins_desc[0]);
request_irq(IRQ_EINT2,&key_irq,IRQT_BOTHEDGE,"s3",&pins_desc[1]);
request_irq(IRQ_EINT11,&key_irq,IRQT_BOTHEDGE,"s4",&pins_desc[2]);
request_irq(IRQ_EINT19,&key_irq,IRQT_BOTHEDGE,"s5",&pins_desc[3]);
return 0;
}
// 2 实现中断处理函数 key_irq
// 使用s3c2410_gpio_getpin获取引脚状态,如果按下,则设置全局变量key_val,
// 并再key_drv_read函数中返回给用户程序
static irqreturn_t key_irq(int irq, void *dev_id)
{
struct pin_desc *p = (struct pin_desc*)dev_id;
unsigned int pin_val;
pin_val = s3c2410_gpio_getpin(p->pin);
if(pin_val)
{
/* 松开 */
key_val = 0x80 | p->key_val;
}else
{
/* 按下 */
key_val = p->key_val;
}
ev_press = 1; // 表示中断发生了,如果ev_press=0,用户程序可会睡眠的
wake_up_interruptible(&key_waitq); //唤醒key_waitq队列中的进程
}
// 3 卸载函数:根据dev_id删除 action链表中对应的节点
// 两个参数的用途:irq_desc[IRQ_EINT0]->action 定位到链表头指针,再根据pin_desc[x]
// 找到对应节点并删除。注意IRQ_EINT0 是个宏,实际展开后是一个中断号irqno
int key_drv_close(struct inode *inode, struct file *file)
{
free_irq(IRQ_EINT0, &pins_desc[0]);
free_irq(IRQ_EINT2, &pins_desc[1]);
free_irq(IRQ_EINT11, &pins_desc[2]);
free_irq(IRQ_EINT19, &pins_desc[3]);
}
ssize_t key_drv_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
{
if(size != 1) // 强制只读一个字节数值
return -EINVAL;
// 如果ev_press,则将该进程加入到key_waitq等待队列中,即调度出去
// 直到中断发生时,中断处理函数设置ev_press=1并唤醒该进程
wait_event_interruptible(key_waitq, ev_press);
copy_to_user(buf, &key_val, 1);
ev_press = 0; // 注意执行完read之后清零ev_press,让该进程继续睡,不然会把一次按下当作多次,误操作
return 1;
}
测试函数:key_drv_ts.c
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
int main(void)
{
int fd, val = 1;
int cnt = 0;
unsigned char key_val;
fd = open("/dev/key_drv", O_RDWR);
if(fd < 0)
printf("can't open /dev/key_drv!\n");
while(1)
{
read(fd, &key_val,1); //如果没中断发生,该进程会进入休眠
printf("key_val = 0x%x\n", key_val);
}
return 0;
}
补充:打开s2 s3 s4 s5四个设备
执行命令: exec 5</dev/key_drv
注意:当rmmod 失败时,需lsmod查看是否正在占用这个模块,看Used 是否等于0
不为0时,需先取消占用,执行命令: exec 5<&-
接下来,按其中任一按键,则会在中断打印,如下所示
3 按键驱动程序——poll机制
上面中断方式,read执行后,如果没有中断发生进程就休眠了,即read不会返回,如果
要求在每隔一定时间检测有没按键发生呢?这时候就可利用poll机制了,在指定时间没有
中断发生,则休眠。如果在指定时间范围内,有中断发生,则唤醒执行处理
代码如下:
#include <linux/poll.h> //记得添加这个头文件
static unsigned key_drv_poll(struct file *file, poll_table *wait)
{
unsigned int mask = 0;
poll_wait(file, &key_waitq, wait);
if(ev_press) //如果中断发生,设置mask使其!=0,从而唤醒进程
mask |= POLLIN | POLLRDNORM;
return mask;
}
static struct file_operations key_drv_opts = {
.owner = THIS_MODULE,
.open = key_drv_open,
.read = key_drv_read,
.release = key_drv_close,
.poll = key_drv_poll, // 添加这一项
};
测试函数:key_drv_ts.c
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <poll.h> //记得添加这个头文件,其实什么函数需包含什么头文件不需记住,man 2 poll即可
int main(void)
{
int fd, val = 1;
int cnt = 0;
int ret;
unsigned char key_val;
struct pollfd fds[1];
fd = open("/dev/key_drv", O_RDWR);
if(fd < 0)
printf("can't open /dev/key_drv!\n");
fds[0].fd = fd;
fds[0].events = POLLIN;
while(1)
{
ret = poll(fds, 1, 5000);//设置超时时间5s,超时会返回0
if (ret == 0)
{
printf("time out\n");
}
else{
read(fd, &key_val,1);
printf("key_val = 0x%x\n", key_val);'
}
}
return 0;
}
运行结果:
4 按键驱动程序——异步通知
前面三种都是被动通知的,即应用程序主动去查询。
一个应用程序当然不可能这么无聊,因为还有其它重要事情要干呀。因此如何实现有数据来了主动通知用户程序呢?
异步通知,明确几个要点
1)谁通知? --->驱动程序
2)通知谁? ---->用户程序
3)如何通知 ---> 驱动程序发送信号
4)谁来处理 ---> 用户程序通过signal定义一个信号接收处理函数
(类似QT的信号与槽机制,绑定一个信号,通过槽函数处理)
OK,明确上面几步之后,来看看如何实现,代码如下
// 在中断处理函数里通知用户程序——通过kill_fasync函数发送
static struct fasync_struct *key_async;
static irqreturn_t key_irq(int irq, void *dev_id)
{
struct pin_desc *p = (struct pin_desc*)dev_id;
unsigned int pin_val;
pin_val = s3c2410_gpio_getpin(p->pin);
if(pin_val)
{
/* 松开 */
key_val = 0x80 | p->key_val;
}else
{
/* 按下 */
key_val = p->key_val;
}
ev_press = 1;
wake_up_interruptible(&key_waitq);
kill_fasync( &key_async, SIGIO, POLL_IN); // 驱动程序发送信号给用户app
return IRQ_RETVAL(IRQ_HANDLED);
}
// 用于创建并初始化 key_async这个指针所指结构体,里面指定了发送目标的PID
// 因此用户程序需告诉pid 给驱动程序
// 而 key_drv_fasync 在用户程序调用 fcntl 时会调用,用于告诉驱动程序pid
static int key_drv_fasync (int fd, struct file *filp, int on)
{
printk("driver: key_drv_fasync\n");
return fasync_helper(fd, filp, on, &key_async);
}
static struct file_operations key_drv_opts = {
.owner = THIS_MODULE,
.open = key_drv_open,
.read = key_drv_read,
.release = key_drv_close,
.poll = key_drv_poll,
.fasync = key_drv_fasync, // 添加一项
};
用户程序如下:key_drv_ts.c
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <poll.h>
#include <signal.h>
int fd;
void handle_key_irq(int signum)
{
unsigned char key_val;
read(fd, &key_val, 1);
printf("key_val: 0x%x\n", key_val);
}
int main(void)
{
int val = 1;
int cnt = 0;
int ret;
int Oflags;
signal(SIGIO, handle_key_irq);
fd = open("/dev/key_drv", O_RDWR);
if(fd < 0)
printf("can't open /dev/key_drv!\n");
/* 以下设置用于告诉驱动程序PID,即在flags变化时调用key_drv_fasync初始化那个结构体*/
fcntl(fd, F_SETOWN, getpid());
Oflags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, Oflags| FASYNC);
while(1)
{
sleep(1000);
}
return 0;
}
测试结果:
5 按键驱动程序——同步、互斥、阻塞
如果要求只能一个程序打开按键驱动,怎么实现呢?
有两种方法:
其一:原子操作,代码如下
static atomic_t canopen = ATOMIC_INIT(1); //定义一个原子量,并初始化为1
static int key_drv_open(struct inode* inode, struct file* file)
{
if (!atomic_dec_and_test(&canopen)) //先减再测试是否为0,第一次打开为0,后面打开则报错返回
{
atomic_inc(&canopen);
return -EBUSY;
}
request_irq(IRQ_EINT0,&key_irq,IRQT_BOTHEDGE,"s2",&pins_desc[0]);
request_irq(IRQ_EINT2,&key_irq,IRQT_BOTHEDGE,"s3",&pins_desc[1]);
request_irq(IRQ_EINT11,&key_irq,IRQT_BOTHEDGE,"s4",&pins_desc[2]);
request_irq(IRQ_EINT19,&key_irq,IRQT_BOTHEDGE,"s5",&pins_desc[3]);
return 0;
}
测试函数:key_drv_ts.c
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <poll.h>
#include <signal.h>
int main(void)
{
int val = 1, fd;
int cnt = 0;
int ret;
fd = open("/dev/key_drv", O_RDWR);
if(fd < 0)
printf("can't open /dev/key_drv!\n");
while(1)
{
ret = read(fd, &key_val, 1); //第一次打开,进程会睡眠
printf("key_val: 0x%x, ret = %d\n",
key_val, ret);
sleep(5);
}
return 0;
}
测试结果图如下
其二: 信号量
只需在open函数获取一个信号量,而在close函数释放即可
static DECLARE_MUTEX(key_lock); //定义并初始化一个互斥锁
static int key_drv_open(struct inode* inode, struct file* file)
{
#if 0
if (!atomic_dec_and_test(&canopen))
{
atomic_inc(&canopen);
return -EBUSY;
}
#endif
// 获取信号量
down(&key_lock);
request_irq(IRQ_EINT0,&key_irq,IRQT_BOTHEDGE,"s2",&pins_desc[0]);
request_irq(IRQ_EINT2,&key_irq,IRQT_BOTHEDGE,"s3",&pins_desc[1]);
request_irq(IRQ_EINT11,&key_irq,IRQT_BOTHEDGE,"s4",&pins_desc[2]);
request_irq(IRQ_EINT19,&key_irq,IRQT_BOTHEDGE,"s5",&pins_desc[3]);
return 0;
}
int key_drv_close(struct inode *inode, struct file *file)
{
free_irq(IRQ_EINT0, &pins_desc[0]);
free_irq(IRQ_EINT2, &pins_desc[1]);
free_irq(IRQ_EINT11, &pins_desc[2]);
free_irq(IRQ_EINT19, &pins_desc[3]);
up(&key_lock); //记得退出时释放锁
}
ssize_t key_drv_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
{
if(size != 1)
return -EINVAL;
// 如果没有按键动作,休眠
wait_event_interruptible(key_waitq, ev_press);
// 如果有按键动作,返回键值
copy_to_user(buf, &key_val, 1);
ev_press = 0;
return 1;
}
测试函数:key_drv_ts.c
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <poll.h>
#include <signal.h>
int main(void)
{
int val = 1, fd;
int cnt = 0;
int ret, key_val;
fd = open("/dev/key_drv", O_RDWR);
if(fd < 0)
printf("can't open /dev/key_drv!\n");
while(1)
{
ret = read(fd, &key_val, 1);
printf("key_val: 0x%x, ret = %d\n",
key_val, ret);
}
return 0;
}
测试结果
此外,还可以配合阻塞与非阻塞方式,
阻塞常与信号量搭配使用。默认open时是阻塞的,所谓阻塞,即open时如果不能获取信号量,则进程阻塞休眠不返回
相反,非阻塞在无法获取信号量时会立即返回。代码实现如下
static int key_drv_open(struct inode* inode, struct file* file)
{
if(file->f_flags & O_NONBLOCK) //如果非阻塞,则尝试获取信号量
{
if(down_trylock(&key_lock))
return -EBUSY;
}else //阻塞,则获得信号量
{
down(&key_lock);
}
request_irq(IRQ_EINT0,&key_irq,IRQT_BOTHEDGE,"s2",&pins_desc[0]);
request_irq(IRQ_EINT2,&key_irq,IRQT_BOTHEDGE,"s3",&pins_desc[1]);
request_irq(IRQ_EINT11,&key_irq,IRQT_BOTHEDGE,"s4",&pins_desc[2]);
requesto_irq(IRQ_EINT19,&key_irq,IRQT_BOTHEDGE,"s5",&pins_desc[3]);
return 0;
}
ssize_t key_drv_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
{
if(size != 1)
return -EINVAL;
if(file->f_flags & O_NONBLOCK) //如果非阻塞,且没有按键按下,则直接返回
{
if(!ev_press)
return -EAGAIN;
}
else //如果阻塞,则休眠
{
// 如果没有按键动作,休眠
wait_event_interruptible(key_waitq, ev_press);
}
// 如果有按键动作,返回键值
copy_to_user(buf, &key_val, 1);
ev_press = 0;
return 1;
}
测试函数
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <poll.h>
#include <signal.h>
int main(void)
{
int val = 1, fd;
int cnt = 0;
int ret, key_val;
fd = open("/dev/key_drv", O_RDWR | O_NONBLOCK); //非阻塞方式打开
if(fd < 0)
printf("can't open /dev/key_drv!\n");
while(1)
{
ret = read(fd, &key_val, 1);
printf("key_val: 0x%x, ret = %d\n",
key_val, ret);
sleep(5);
}
return 0;
}
测试结果:
OK,以上是本篇文章的所有内容,主要提取核心代码进行讲解,完整代码可从https://blog.youkuaiyun.com/qq_42800075/article/details/105670841获取
==== 欢迎交流~