3286|5

263

帖子

0

资源

纯净的硅(初级)

【i.MX6ULL】驱动开发10——阻塞&非阻塞式按键检测 [复制链接]

上篇文章: 【i.MX6ULL】驱动开发9——Linux IO模型分析,介绍了linux中的五种I/O模型,本篇,就来使用阻塞式I/O非用阻塞式I/O两种方式进行按键的读取实验,并对比之前使用输入捕获和中断法检测的按键程序,查看CPU的使用率是否降低。

 

1 阻塞I/O方式的按键检测

1.1 阻塞I/O之等待队列

阻塞访问最大的好处就是当设备文件不可操作的时候进程可以进入休眠态,这样可以将CPU资源让出来。但是,当设备文件可以操作的时候就必须唤醒进程,一般在中断函数里面完成唤醒工作。Linux 内核提供了等待队列(wait queue)来实现阻塞进程的唤醒工作。

等待队列头使用结构体wait_queue_head_t 表示:

struct __wait_queue_head { 
    spinlock_t       lock; 
    struct list_head task_list; 
}; 
​
typedef struct __wait_queue_head wait_queue_head_t; 

使用 init_waitqueue_head 函数初始化等待队列头:

/**
 * q: 要初始化的等待队列头
 * return: 无
 */
void init_waitqueue_head(wait_queue_head_t *q) 

当设备不可用的时, 将这些进程对应的等待队列项(wait_queue_t )添加到等待队列里面:

struct __wait_queue { 
    unsigned int      flags; 
    void              *private; 
    wait_queue_func_t func; 
    struct list_head  task_list;
}; 
​
typedef struct __wait_queue wait_queue_t;

使用宏 DECLARE_WAITQUEUE 定义并初始化一个等待队列项:

DECLARE_WAITQUEUE(name, tsk)

当设备不可访问的时候就需要将进程对应的等待队列项添加到前面创建的等待队列头中:

/**
 * q: 要加入的等待队列头
 * wait:要加入的等待队列项
 * return: 无
 */
void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait) 

当设备可以访问以后再将进程对应的等待队列项从等待队列头中删除即可:

/**
 * q: 要删除的等待队列头
 * wait:要删除的等待队列项
 * return: 无
 */
void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait) 

当设备可以使用的时候就要唤醒进入休眠态的进程:

void wake_up(wait_queue_head_t *q) 
void wake_up_interruptible(wait_queue_head_t *q) 

1.2 阻塞I/O程序编写

这里仅介绍与之前按键程序的主要区别。

1.2.1驱动程序

阻塞读取逻辑如下,首先要定义一个等待队列,当按键没有按下时,就要阻塞等待了(将等待队列添加到等待队列头),然后进行行一次任务切换,交出CPU的使用权。等待有按键按下时,会有信号唤醒该等待,并将按键值返回给应用层的程序。

static ssize_t imx6uirq_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 imx6uirq_dev *dev = (struct imx6uirq_dev *)filp->private_data;
    
    /* 定义一个等待队列 <-------------------------- */
    DECLARE_WAITQUEUE(wait, current);
    
    /* 没有按键按下 <------------------------------ */
    if(atomic_read(&dev->releasekey) == 0)
    {
        /* 将等待队列添加到等待队列头 <------------ */
        add_wait_queue(&dev->r_wait, &wait);
        
        /* 设置任务状态 <-------------------------- */
        __set_current_state(TASK_INTERRUPTIBLE);
        
        /* 进行一次任务切换 <---------------------- */
        schedule();
        
        /* 判断是否为信号引起的唤醒 <-------------- */
        if(signal_pending(current))
        {
            ret = -ERESTARTSYS;
            goto wait_error;
        }
        
        /* 将当前任务设置为运行状态 <-------------- */
        __set_current_state(TASK_RUNNING);
        
        /* 将对应的队列项从等待队列头删除 <-------- */
        remove_wait_queue(&dev->r_wait, &wait);
    }
​
    keyvalue = atomic_read(&dev->keyvalue);
    releasekey = atomic_read(&dev->releasekey);
​
    /* 有按键按下 */
    if (releasekey)
    {
        //printk("releasekey!\r\n");
        if (keyvalue & 0x80)
        {
            keyvalue &= ~0x80;
            ret = copy_to_user(buf, &keyvalue, sizeof(keyvalue));
        }
        else
        {
            goto data_error;
        }
        atomic_set(&dev->releasekey, 0); /* 按下标志清零 */
    }
    else
    {
        goto data_error;
    }
    return 0;
​
wait_error:
    set_current_state(TASK_RUNNING);           /* 设置任务为运行态 */
    remove_wait_queue(&dev->r_wait, &wait);    /* 将等待队列移除 */
    return ret;
    
data_error:
    return -EINVAL;
}

按键的定时器去抖逻辑中的,读取到按键后,触发唤醒,这里以其中的一个按键为例,其逻辑如下:

void timer1_function(unsigned long arg)
{
    unsigned char value;
    struct irq_keydesc *keydesc;
    struct imx6uirq_dev *dev = (struct imx6uirq_dev *)arg;
​
    keydesc = &dev->irqkeydesc[0];
​
    value = gpio_get_value(keydesc->gpio); /* 读取IO值 */
    if(value == 1) /* 按下按键 */
    {
        printk("get key1: high\r\n");
        atomic_set(&dev->keyvalue, keydesc->value);
    }
    else /* 按键松开 */
    {
        printk("key1 release\r\n");
        atomic_set(&dev->keyvalue, 0x80 | keydesc->value);
        atomic_set(&dev->releasekey, 1); /* 标记松开按键,即完成一次完整的按键过程 */            
    }
    
    /* 唤醒进程 */
    if(atomic_read(&dev->releasekey))
    {
        wake_up_interruptible(&dev->r_wait);
    }
}

1.2.2 应用程序

应用程序不需要修改,还使用之前的轮询读取的方式,为了在测试时看出阻塞与非阻塞方式的区别,在read函数前后添加打印,如果程序运行正常,会先打印read前一句的打印,直到有按键按下后,read函数才被接触阻塞,read后一句的打印才会打印出。

/* 循环读取按键值数据! */
while(1)
{
    printf("[APP] read begin...\r\n");
    read(fd, &keyvalue, sizeof(keyvalue));
    printf("[APP] read end\r\n");
    if (keyvalue == KEY1VALUE)
    {
        printf("[APP] KEY1 Press, value = %#X\r\n", keyvalue);
    }
    else if (keyvalue == KEY2VALUE)
    {
        printf("[APP] KEY2 Press, value = %#X\r\n", keyvalue);
    }
}

1.2 实验

和之前一样,使用Makefile编译驱动程序和应用程序,并复制到nfs根文件系统中。

开始测试,按如下图,当没有按键按下时,应用程序被阻塞:

1.png

按键程序在后台运行,此时使用top指令开查看CPU的使用率,可以发现阻塞式按键驱动这种方式,CPU的暂用率几乎为0,虽然按键应用程序中仍实现循环读取的方式,但因平时读取不到按键值,按键应用程序被阻塞住了,CPU的使用权被让出,自然CPU的使用率就降下来了。

2.png

2 非阻塞I/O方式的按键检测

按键应用程序以非阻塞的方式读取,按键驱动程序也要以非阻塞的方式立即返回。应用程序可以通过select、poll或epoll函数来 查询设备是否可以操作,驱动程序使用poll函数。

2.1 非阻塞I/O之select/poll

  • select函数原型:

/**
 * nfs: 所要监视的这三类文件描述集合中,最大文件描述符加1
 * readfds: 用于监视指定描述符集的读变化
 * writefds: 用于监视文件是否可以进行写操作
 * exceptfds: 用于监视文件的异常
 * timeout: 超时时间
 * return: 0 超时发生, -1 发生错误, 其他值 可以进行操作的文件描述符个数 
 */
int select(int    nfds,  
           fd_set *readfds,  
           fd_set *writefds, 
           fd_set *exceptfds,  
           struct timeval *timeout) 

其中超时时间使用结构体timeval表示:

struct timeval { 
   long tv_sec;  /* 秒   */ 
   long tv_usec; /* 微妙 */  
}; 

当timeout为NULL的时候就表示无限等待。

  • poll函数原型:

/**
 * fds: 要监视的文件描述符集合以及要监视的事件,为一个数组
 * nfds: 监视的文件描述符数量
 * timeout: 超时时间,单位为 ms
 * return: 0 超时发生, -1 发生错误, 其他值 可以进行操作的文件描述符个数 
 */
int poll(struct pollfd *fds,  
         nfds_t nfds,  
         nt     timeout) 

2.2 非阻塞I/O程序编写

2.2.1 驱动程序

poll函数处理部分:

unsigned int imx6uirq_poll(struct file *filp, struct poll_table_struct *wait)
{
    unsigned int mask = 0;
    struct imx6uirq_dev *dev = (struct imx6uirq_dev *)filp->private_data;
​
    /* 将等待队列头添加到poll_table中 */
    poll_wait(filp, &dev->r_wait, wait);
    
    /* 按键按下 */
    if(atomic_read(&dev->releasekey))
    {
        mask = POLLIN | POLLRDNORM;            /* 返回PLLIN */
    }
    return mask;
}
​
/* 设备操作函数 */
static struct file_operations imx6uirq_fops = {
    .owner = THIS_MODULE,
    .open = imx6uirq_open,
    .read = imx6uirq_read,
    .poll = imx6uirq_poll,
};

read函数处理部分:

static ssize_t imx6uirq_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 imx6uirq_dev *dev = (struct imx6uirq_dev *)filp->private_data;
​
    /* 非阻塞访问 */
    if (filp->f_flags & O_NONBLOCK)
    {
        /* 没有按键按下,返回-EAGAIN */
        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)
    {
        //printk("releasekey!\r\n");
        if (keyvalue & 0x80)
        {
            keyvalue &= ~0x80;
            ret = copy_to_user(buf, &keyvalue, sizeof(keyvalue));
        }
        else
        {
            goto data_error;
        }
        atomic_set(&dev->releasekey, 0); /* 按下标志清零 */
    }
    else
    {
        goto data_error;
    }
    return 0;
​
wait_error:
    return ret;
data_error:
    return -EINVAL;
}

 

2.2.2 应用程序

2.2.2.1 poll方式读取

注意open函数的参数是O_NONBLOCK,即非阻塞访问,并且为了在测试时看出阻塞读取与非阻塞读取的区别,在poll函数前后添加打印,如果程序正常运行,poll函数则不会被阻塞,500ms超时未读取到按键值后会再次循环读取,实际效果就是可以看打一直有打印输出。

   
 filename = argv[1];
    fd = open(filename, O_RDWR | O_NONBLOCK);    /* 非阻塞访问 */
    if (fd < 0)
    {
        printf("[APP] Can't open file %s\r\n", filename);
        return -1;
    }
​
    /* 构造结构体 */
    fds.fd = fd;
    fds.events = POLLIN;
    while(1)
    {
        printf("[APP] poll begin... \r\n", data);
        ret = poll(&fds, 1, 500);
        printf("[APP] poll end \r\n", data);
        /* 数据有效 */
        if (ret > 0)
        {
            ret = read(fd, &data, sizeof(data));
            if(ret < 0)
            {
                /* 读取错误 */
            }
            else
            {
                if(data)
                {
                    printf("[APP] key value = %d \r\n", data);
                }
            }     
        }
        /* 超时 */
        else if (ret == 0)
        {
            /* 用户自定义超时处理 */
        }
        /* 错误 */
        else
        {
            /* 用户自定义错误处理 */
        }
    }

2.2.2.2 select方式读取

select方式读取与poll方式类似,都是非阻塞读取,程序类似:

while(1)
{
    FD_ZERO(&readfds);
    FD_SET(fd, &readfds);
    /* 构造超时时间 */
    timeout.tv_sec = 0;
    timeout.tv_usec = 500000; /* 500ms */
    ret = select(fd + 1, &readfds, NULL, NULL, &timeout);
    switch (ret)
    {
            /* 超时 */
        case 0:
            /* 用户自定义超时处理 */
            break;
            /* 错误 */
        case -1:
            /* 用户自定义错误处理 */
            break;
            /* 可以读取数据 */
        default:
            if(FD_ISSET(fd, &readfds))
            {
                ret = read(fd, &data, sizeof(data));
                if (ret < 0)
                {
                    /* 读取错误 */
                }
                else
                {
                    if (data)
                    {
                        printf("key value=%d\r\n", data);
                    }
                }
            }
            break;
    }
}

2.3 实验

2.3.1 poll方式读取

和之前一样,使用Makefile编译驱动程序和应用程序,并复制到nfs根文件系统中。

开始测试,按如下图,当没有按键按下时,应用程序也没有被阻塞,从不断的打印就可以看出应用程序在循环运行。当有按键按下时,能够读取到对应的按键值。

3.png

按键程序在后台运行,此时使用top指令开查看CPU的使用率,可以发现非阻塞式按键驱动这种方式,CPU的暂用率也几乎为0,虽然按键应用程序中仍实现循环读取的方式,但poll函数有500ms的超时设置,在超时等待的时间里,CPU的使用权也是被让出,所以CPU的使用率也降下来了。

4.png

2.3.2 select方式读取

select方式读取与poll方式读取的效果一样。

使用ps指令查看poll方式的按键进行号,使用kill杀带该进程,再运行select方式的按键应用程序:

5.png

select非阻塞读取的方式,CPU的暂用率也几乎为0:

6.png

3 总结

本篇使用两种I/O模型进行按键读取:阻塞式I/O非用阻塞式I/O,通过实际的实验,对比两者方式的实际运行效果与主要区别,并查看CPU的占用率,两种方式的CPU使用率都几乎为0。

此帖出自ARM技术论坛

回复

1万

帖子

2853

资源

管理员

个人签名

玩板看这里:

http://bbs.eeworld.com.cn/elecplay.html

EEWorld测评频道众多好板等你来玩,还可以来频道许愿树许愿说说你想要玩的板子,我们都在努力为大家实现!


回复

6741

帖子

0

资源

五彩晶圆(初级)

在单片机按键算法,一般采用的是键盘扫描程序,算法简单有效
这种Linux 内核提供了等待队列实现阻塞进程的唤醒工作
当然也需要消抖必须的,

想知道,按键在松手后有效或者说灵敏度高要注意什么
还有将这些进程对应的等待队列项,怎样考虑消耗资源少运行效率高这些的


回复

1477

帖子

3

资源

版主

感谢分享


回复

5249

帖子

18

资源

五彩晶圆(中级)

谢谢分享!

个人签名

默认摸鱼


回复

530

帖子

0

资源

一粒金砂(高级)

电机控制进阶的课程不更新了吗


回复
您需要登录后才可以回帖 登录 | 注册

查找数据手册?

EEWorld Datasheet 技术支持

最新文章 更多>>
    关闭
    站长推荐上一条 1/9 下一条

    About Us 关于我们 客户服务 联系方式 器件索引 网站地图 最新更新 手机版

    站点相关: 安防电子 汽车电子 手机便携 工业控制 家用电子 医疗电子 测试测量 网络通信 物联网

    北京市海淀区知春路23号集成电路设计园量子银座1305 电话:(010)82350740 邮编:100191

    电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 电信业务审批[2006]字第258号函 京公网安备 11010802033920号 Copyright © 2005-2022 EEWORLD.com.cn, Inc. All rights reserved
    快速回复 返回顶部 返回列表