Part13:按键驱动程序的不同实现(查询/中断/poll/异步通知/同步互斥阻塞)

0 写在前面
part12文章是对ARM架构下Linux内核的异常(中断)处理体系结果的原理讲述,有了这些基本的认识后,下面根据不同实现方式
实现按键驱动程序的编写。
说明一点:下面的实现不会贴全部代码,代码可从https://blog.csdn.net/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;
}

运行结果:
key_drv_poll运行结果

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.csdn.net/qq_42800075/article/details/105670841获取

==== 欢迎交流~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值