STM32MP157驱动开发——阻塞与非阻塞IO(下)
0.前言
上一节理解了阻塞与非阻塞IO的原理,以及两种模式下常用的事件处理方式:阻塞型IO使用等待队列处理事件、非阻塞IO使用select、poll方式处理。本小节就分别使用两种方式进行简单的启动开发和验证。
一、阻塞型IO驱动开发
在使用中断方式进行按键驱动开发实验中,应用程序使用 read 不断读取按键状态,在中断服务函数中对按键进行延时消抖。这种方法有一个缺点:测试App在一个 while 死循环中不断的读取键值,会占用很高的CPU资源。
这一节就使用阻塞型IO,让测试程序在没有按键触发时,进入休眠状态,就可以大大降低CPU使用率。
相关的驱动程序在中断模式下的按键驱动基础上进行修改:
blockio.c:
#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/semaphore.h>
#include <linux/of_gpio.h>
#include <linux/of_address.h>
#include <linux/of.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include <linux/irq.h>
#include <linux/of_irq.h>
#define KEY_CNT 1
#define KEY_NAME "key"
/*定义按键状态*/
enum key_status {
KEY_PRESS = 0, /*按键按下*/
KEY_RELEASE, /*按键释放*/
KEY_KEEP, /*按键保持*/
};
/*key设备结构体*/
struct key_dev{
dev_t devid; /*设备号*/
struct cdev cdev; /*cdev*/
struct class *class; /*类*/
struct device *device; /*设备*/
struct device_node *nd; /*设备节点*/
int key_gpio; /*key所使用的的gpio号*/
struct timer_list timer; /*定时器*/
int irq_num; /*中断号*/
atomic_t status; /*按键状态*/
wait_queue_head_t r_wait; /*等待队列头*/
};
struct key_dev keydev; /*创建key设备*/
/*按键防抖处理,也就是开启定时器延时15ms,这里作为中断处理函数*/
static irqreturn_t key_interrupt(int irq, void *dev_id)
{
mod_timer(&keydev.timer, jiffies + msecs_to_jiffies(15));
return IRQ_HANDLED;
}
/*解析设备树中的按键属性*/
static int key_parse_dt(void){
int ret;
const char *str;
/*设置Key所使用的GPIO*/
/*获取设备节点*/
keydev.nd = of_find_node_by_path("/key");
if(keydev.nd == NULL){
printk("keydev node not find!\r\n");
return -EINVAL;
}
/*获取status值*/
ret = of_property_read_string(keydev.nd, "status", &str);
if(ret < 0){
printk("read status failed!\r\n");
return -EINVAL;
}
if(strcmp(str, "okay")){
printk("key device not okay!\r\n");
return -EINVAL;
}
/*获取compatible属性并匹配*/
ret = of_property_read_string(keydev.nd, "compatible", &str);
if(ret < 0){
printk("read compatible failed!\r\n");
return -EINVAL;
}
if(strcmp(str, "amonter,key")){
printk("keydev: compatible match failed!\r\n");
return -EINVAL;
}
/*获取设备树中的gpio属性,得到KEY0的设备编号*/
keydev.key_gpio = of_get_named_gpio(keydev.nd, "key-gpio", 0);
if(keydev.key_gpio < 0){
printk("can't get key-gpio");
return -EINVAL;
}
printk("key-gpio num = %d\r\n", keydev.key_gpio);
/*获取GPIO对应的中断号*/
keydev.irq_num = irq_of_parse_and_map(keydev.nd, 0);
if(!keydev.irq_num){
return -EINVAL;
}
printk("irq-num = %d\r\n", keydev.irq_num);
return 0;
}
static int key_gpio_init(void)
{
int ret;
unsigned long irq_flags;
/*向GPIO子系统申请使用GPIO*/
ret = gpio_request(keydev.key_gpio, "KEY0");
if(ret){
printk(KERN_ERR "keydev: failed to request key-gpio!\r\n");
return ret;
}
/*设置PG3为GPIO输入模式*/
ret = gpio_direction_input(keydev.key_gpio);
if(ret < 0){
printk("can't set gpio mode!\r\n");
return ret;
}
/*获取设备树中指定的中断触发类型*/
irq_flags = irq_get_trigger_type(keydev.irq_num);
if(IRQF_TRIGGER_NONE == irq_flags){
irq_flags = IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING;;
}
/*申请中断*/
ret = request_irq(keydev.irq_num, key_interrupt, irq_flags, "Key0_IRQ", NULL);
if(ret){
gpio_free(keydev.key_gpio);
return ret;
}
return 0;
}
static void key_timer_function(struct timer_list *arg)
{
static int last_val = 1;
int current_val;
/*读取按键值并判断按键当前状态*/
current_val = gpio_get_value(keydev.key_gpio);
if(0 == current_val && last_val){ /*按键按下*/
atomic_set(&keydev.status, KEY_PRESS);
wake_up_interruptible(&keydev.r_wait);
} else if(1 == current_val && !last_val) { /*按键释放*/
atomic_set(&keydev.status, KEY_RELEASE);
wake_up_interruptible(&keydev.r_wait);
} else{ /*保持*/
atomic_set(&keydev.status, KEY_KEEP);
}
last_val = current_val;
}
/*设备open操作函数*/
static int key_open(struct inode *inode, struct file *filp)
{
return 0;
}
/*设备read操作函数*/
static ssize_t key_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
int ret;
/* 加入等待队列,当有按键按下或松开动作发生时,才会被唤醒 */
ret = wait_event_interruptible(keydev.r_wait, KEY_KEEP != atomic_read(&keydev.status));
if(ret){
return ret;
}
/* 将按键状态信息发送给应用程序 */
ret = copy_to_user(buf, &keydev.status, sizeof(int));
/* 状态重置 */
atomic_set(&keydev.status, KEY_KEEP);
return ret;
}
/*设备write操作函数*/
static ssize_t key_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
return 0;
}
/*设备release操作函数*/
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,
};
/*驱动入口函数*/
static int __init mykey_init(void){
int ret;
/*初始化队列头*/
init_waitqueue_head(&keydev.r_wait);
/*初始化按键状态*/
atomic_set(&keydev.status, KEY_KEEP);
/*设备树解析*/
ret = key_parse_dt();
if(ret){ /*出错*/
return ret;
}
/*gpio中断初始化*/
ret = key_gpio_init();
if(ret){ /*出错*/
return ret;
}
/*注册字符设备驱动*/
/*创建设备号,直接使用系统分配的设备号*/
ret = alloc_chrdev_region(&keydev.devid, 0, KEY_CNT, KEY_NAME); //由系统分配设备号
if(ret < 0){
pr_err("%s couldn't alloc_chrdev_region, ret = %d\r\n", KEY_NAME, ret);
goto free_gpio;
}
/*初始化cdev*/
keydev.cdev.owner = THIS_MODULE;
cdev_init(&keydev.cdev, &key_fops);
/*添加一个cdev*/
cdev_add(&keydev.cdev, keydev.devid, KEY_CNT);
if(ret < 0){
goto del_unregister;
}
/*创建类*/
keydev.class = class_create(THIS_MODULE, KEY_NAME);
if(IS_ERR(keydev.class)){
goto del_cdev;
}
/*创建设备*/
keydev.device = device_create(keydev.class, NULL, keydev.devid, NULL,KEY_NAME);
if(IS_ERR(keydev.device)){
goto destroy_class;
}
/*初始化timer,设置定时器处理函数*/
timer_setup(&keydev.timer, key_timer_function, 0); //未设置周期,所以不会激活定时期
return 0;
destroy_class:
device_destroy(keydev.class, keydev.devid);
del_cdev:
cdev_del(&keydev.cdev);
del_unregister:
unregister_chrdev_region(keydev.devid, KEY_CNT);
free_gpio:
free_irq(keydev.irq_num, NULL);
gpio_free(keydev.key_gpio);
return -EIO;
}
/*驱动出口函数*/
static void __exit mykey_exit(void){
cdev_del(&keydev.cdev);
unregister_chrdev_region(keydev.devid, KEY_CNT);
del_timer_sync(&keydev.timer); /*删除timer*/
device_destroy(keydev.class, keydev.devid);
class_destroy(keydev.class);
free_irq(keydev.irq_num, NULL); /*释放中断*/
gpio_free(keydev.key_gpio); /*释放IO*/
}
module_init(mykey_init);
module_exit(mykey_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("amonter");
MODULE_INFO(intree, "Y");
修改部分:
①在设备结构体中去掉了自旋锁属性,改用原子变量来对状态变换进行保护。因为自旋锁在拿不到锁时会轮询等待,而阻塞型IO的等待队列会使线程进入休眠,机制冲突。
②在设备结构体中添加一个等待队列头属性
③在定时器处理函数中对按键状态进行判断,如果状态改变则唤醒等待队列,并修改status变量。这样就可以解除被key_read阻塞的线程。
④在key_read函数中,判断是否有按键状态改变的动作发生,如果没有就将它加入到等待队列中,进行阻塞。如果等待队列被唤醒并且条件condition不为按键在保持状态,则解除阻塞。然后将读取到的按键状态发送给用户程序。由于采用了wait_event_interruptible函数,所以进入休眠状态可以被信号打断。
⑤在驱动入口函数中初始化等待队列头,以及使用原子操作将按键初始状态设置为KEEP
注意
:使用等待队列实现阻塞式访问需要注意两点:a. 将任务或进程加入等待队列头;b. 在合适的地方唤醒等待队列,一般是中断处理函数中
测试App直接使用上一节中断模式下的按键驱动的测试App即可。
可以看到本小节开发的驱动,CPU占用几乎可以忽略不计,而如果使用上一节的驱动程序,CPU占用则将近50%:
这里可以使用kill -9 PID
来杀死某一进程,PID为该进程的句柄。
二、非阻塞IO驱动开发
在阻塞型IO驱动基础上进行修改,使用非阻塞IO模式进行驱动开发。
noblock.c:
#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/semaphore.h>
#include <linux/of_gpio.h>
#include <linux/of_address.h>
#include <linux/of.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include <linux/irq.h>
#include <linux/of_irq.h>
#include <linux/poll.h>
#define KEY_CNT 1
#define KEY_NAME "key"
/*定义按键状态*/
enum key_status {
KEY_PRESS = 0, /*按键按下*/
KEY_RELEASE, /*按键释放*/
KEY_KEEP, /*按键保持*/
};
/*key设备结构体*/
struct key_dev{
dev_t devid; /*设备号*/
struct cdev cdev; /*cdev*/
struct class *class; /*类*/
struct device *device; /*设备*/
struct device_node *nd; /*设备节点*/
int key_gpio; /*key所使用的的gpio号*/
struct timer_list timer; /*定时器*/
int irq_num; /*中断号*/
atomic_t status; /*按键状态*/
wait_queue_head_t r_wait; /*等待队列头*/
};
struct key_dev keydev; /*创建key设备*/
/*按键防抖处理,也就是开启定时器延时15ms,这里作为中断处理函数*/
static irqreturn_t key_interrupt(int irq, void *dev_id)
{
mod_timer(&keydev.timer, jiffies + msecs_to_jiffies(15));
return IRQ_HANDLED;
}
/*解析设备树中的按键属性*/
static int key_parse_dt(void){
int ret;
const char *str;
/*设置Key所使用的GPIO*/
/*获取设备节点*/
keydev.nd = of_find_node_by_path("/key");
if(keydev.nd == NULL){
printk("keydev node not find!\r\n");
return -EINVAL;
}
/*获取status值*/
ret = of_property_read_string(keydev.nd, "status", &str);
if(ret < 0){
printk("read status failed!\r\n");
return -EINVAL;
}
if(strcmp(str, "okay")){
printk("key device not okay!\r\n");
return -EINVAL;
}
/*获取compatible属性并匹配*/
ret = of_property_read_string(keydev.nd, "compatible", &str);
if(ret < 0){
printk("read compatible failed!\r\n");
return -EINVAL;
}
if(strcmp(str, "amonter,key")){
printk("keydev: compatible match failed!\r\n");
return -EINVAL;
}
/*获取设备树中的gpio属性,得到KEY0的设备编号*/
keydev.key_gpio = of_get_named_gpio(keydev.nd, "key-gpio", 0);
if(keydev.key_gpio < 0){
printk("can't get key-gpio");
return -EINVAL;
}
printk("key-gpio num = %d\r\n", keydev.key_gpio);
/*获取GPIO对应的中断号*/
keydev.irq_num = irq_of_parse_and_map(keydev.nd, 0);
if(!keydev.irq_num){
return -EINVAL;
}
printk("irq-num = %d\r\n", keydev.irq_num);
return 0;
}
static int key_gpio_init(void)
{
int ret;
unsigned long irq_flags;
/*向GPIO子系统申请使用GPIO*/
ret = gpio_request(keydev.key_gpio, "KEY0");
if(ret){
printk(KERN_ERR "keydev: failed to request key-gpio!\r\n");
return ret;
}
/*设置PG3为GPIO输入模式*/
ret = gpio_direction_input(keydev.key_gpio);
if(ret < 0){
printk("can't set gpio mode!\r\n");
return ret;
}
/*获取设备树中指定的中断触发类型*/
irq_flags = irq_get_trigger_type(keydev.irq_num);
if(IRQF_TRIGGER_NONE == irq_flags){
irq_flags = IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING;;
}
/*申请中断*/
ret = request_irq(keydev.irq_num, key_interrupt, irq_flags, "Key0_IRQ", NULL);
if(ret){
gpio_free(keydev.key_gpio);
return ret;
}
return 0;
}
static void key_timer_function(struct timer_list *arg)
{
static int last_val = 1;
int current_val;
/*读取按键值并判断按键当前状态*/
current_val = gpio_get_value(keydev.key_gpio);
if(0 == current_val && last_val){ /*按键按下*/
atomic_set(&keydev.status, KEY_PRESS);
wake_up_interruptible(&keydev.r_wait);
} else if(1 == current_val && !last_val) { /*按键释放*/
atomic_set(&keydev.status, KEY_RELEASE);
wake_up_interruptible(&keydev.r_wait);
} else{ /*保持*/
atomic_set(&keydev.status, KEY_KEEP);
}
last_val = current_val;
}
/*设备open操作函数*/
static int key_open(struct inode *inode, struct file *filp)
{
return 0;
}
/*设备read操作函数*/
static ssize_t key_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
int ret;
if(filp->f_flags & O_NONBLOCK){ /*非阻塞式访问*/
if(KEY_KEEP == atomic_read(&keydev.status)){
return -EAGAIN;
} else { /*阻塞式访问*/
/*加入等待队列,当按键状态改变时,才会被唤醒*/
ret = wait_event_interruptible(keydev.r_wait, KEY_KEEP != atomic_read(&keydev.status));
if(ret){
return ret;
}
}
}
/* 将按键状态信息发送给应用程序 */
ret = copy_to_user(buf, &keydev.status, sizeof(int));
/* 状态重置 */
atomic_set(&keydev.status, KEY_KEEP);
return ret;
}
/*设备write操作函数*/
static ssize_t key_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
return 0;
}
/*设备release操作函数*/
static int key_release(struct inode *inode, struct file *filp)
{
return 0;
}
/*poll函数,用来处理非阻塞访问*/
static unsigned int key_poll(struct file *filp, struct poll_table_struct *wait)
{
unsigned int mask = 0;
poll_wait(filp, &keydev.r_wait, wait);
if(KEY_KEEP != atomic_read(&keydev.status)){
mask = POLLIN | POLLRDNORM; //返回POLLIN
}
return mask;
}
/*设备操作函数*/
static struct file_operations key_fops = {
.owner = THIS_MODULE,
.open = key_open,
.read = key_read,
.write = key_write,
.release = key_release,
.poll = key_poll,
};
/*驱动入口函数*/
static int __init mykey_init(void){
int ret;
/*初始化队列头*/
init_waitqueue_head(&keydev.r_wait);
/*初始化按键状态*/
atomic_set(&keydev.status, KEY_KEEP);
/*设备树解析*/
ret = key_parse_dt();
if(ret){ /*出错*/
return ret;
}
/*gpio中断初始化*/
ret = key_gpio_init();
if(ret){ /*出错*/
return ret;
}
/*注册字符设备驱动*/
/*创建设备号,直接使用系统分配的设备号*/
ret = alloc_chrdev_region(&keydev.devid, 0, KEY_CNT, KEY_NAME); //由系统分配设备号
if(ret < 0){
pr_err("%s couldn't alloc_chrdev_region, ret = %d\r\n", KEY_NAME, ret);
goto free_gpio;
}
/*初始化cdev*/
keydev.cdev.owner = THIS_MODULE;
cdev_init(&keydev.cdev, &key_fops);
/*添加一个cdev*/
cdev_add(&keydev.cdev, keydev.devid, KEY_CNT);
if(ret < 0){
goto del_unregister;
}
/*创建类*/
keydev.class = class_create(THIS_MODULE, KEY_NAME);
if(IS_ERR(keydev.class)){
goto del_cdev;
}
/*创建设备*/
keydev.device = device_create(keydev.class, NULL, keydev.devid, NULL,KEY_NAME);
if(IS_ERR(keydev.device)){
goto destroy_class;
}
/*初始化timer,设置定时器处理函数*/
timer_setup(&keydev.timer, key_timer_function, 0); //未设置周期,所以不会激活定时期
return 0;
destroy_class:
device_destroy(keydev.class, keydev.devid);
del_cdev:
cdev_del(&keydev.cdev);
del_unregister:
unregister_chrdev_region(keydev.devid, KEY_CNT);
free_gpio:
free_irq(keydev.irq_num, NULL);
gpio_free(keydev.key_gpio);
return -EIO;
}
/*驱动出口函数*/
static void __exit mykey_exit(void){
cdev_del(&keydev.cdev);
unregister_chrdev_region(keydev.devid, KEY_CNT);
del_timer_sync(&keydev.timer); /*删除timer*/
device_destroy(keydev.class, keydev.devid);
class_destroy(keydev.class);
free_irq(keydev.irq_num, NULL); /*释放中断*/
gpio_free(keydev.key_gpio); /*释放IO*/
}
module_init(mykey_init);
module_exit(mykey_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("amonter");
MODULE_INFO(intree, "Y");
修改部分:
①使用poll函数作为非阻塞处理函数,所以加入头文件"/linux/poll.h"
②在 key_read 函数中判断是否为非阻塞式读取访问,如果是则判断按键状态是否改变,如果没有状态改变就返回-EAGAIN。
③此驱动保留了阻塞型访问方式,所以设备结构体中仍然保留相关属性,并且在read函数中也保留了相应的判断和处理方式。
④设计key_poll函数,当应用程序调用 select 或者 poll 函数时 key_poll 就会执行。并在 key_poll 中调用 poll_wait 函数将等待队列头添加到 poll_table,如果按键状态改变就返回 POLLIN,表示有数据可以读取。同时,在设备驱动操作集 file_operations 中添加poll方法,并指定为key_poll。
测试App:
由于使用非阻塞式访问,所以测试App需要做相应修改,添加使用 select 或 poll的访问操作。
noblockio_App.c:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <poll.h>
int main(int argc, char *argv[])
{
fd_set readfds;
int key_val;
int fd;
int ret;
/* 判断传参个数是否正确 */
if (2 != argc)
{
printf("Usage:\n" "\t./keyApp /dev/key\n");
return -1;
}
/* 打开设备 */
fd = open(argv[1], O_RDONLY | O_NONBLOCK);
if (0 > fd){
printf("ERROR: %s file open failed!\n", argv[1]);
return -1;
}
FD_ZERO(&readfds);
FD_SET(fd, &readfds);
/* 循环轮训读取按键数据 */
while(1){
ret = select(fd + 1, &readfds, NULL, NULL, NULL);
switch (ret){
case 0: /* 超时 */
/* 用户自定义超时处理 */
break;
case -1: /* 错误 */
/* 用户自定义错误处理 */
break;
default:
if (FD_ISSET(fd, &readfds))
{
read(fd, &key_val, sizeof(int));
if (0 == key_val)
printf("Key Press\n");
else if (1 == key_val)
printf("Key Release\n");
}
break;
}
}
/* 关闭设备 */
close(fd);
return 0;
}
使用select进行非阻塞访问,在一个while死循环中使用select不断轮询,检查是否有数据可以读取,如果有则调用read函数读取按键数据。可以看到CPU占用也很小: