RK3568驱动指南|第四篇-高级字符设备进阶-第28章 IO多路复用实验

瑞芯微RK3568芯片是一款定位中高端的通用型SOC,采用22nm制程工艺,搭载一颗四核Cortex-A55处理器和Mali G52 2EE 图形处理器。RK3568 支持4K 解码和 1080P 编码,支持SATA/PCIE/USB3.0 外围接口。RK3568内置独立NPU,可用于轻量级人工智能应用。RK3568 支持安卓 11 和 linux 系统,主要面向物联网网关、NVR 存储、工控平板、工业检测、工控盒、卡拉 OK、云终端、车载中控等行业。


【公众号】迅为电子

【粉丝群】824412014(加群获取驱动文档+例程)

【视频观看】嵌入式学习之Linux驱动(第四篇-高级字符设备进阶_全新升级)_基于RK3568

【购买链接】迅为RK3568开发板瑞芯微Linux安卓鸿蒙ARM核心板人工智能AI主板


第28章 IO多路复用实验

在上俩个章节中,我们对阻塞IO和非阻塞IO进行了学习,本章节将学习第三种IO模型-多路复用IO。

28.1 IO多路复用简介

IO多路复用是一种同步的IO模型。IO多路复用可以实现一个进程监视多个文件描述符。一旦某个文件描述符准备就绪,就通知应用程序进行相应的读写操作。没有文件描述符就绪时就会阻塞应用程序,从而释放出CPU资源。

在第25章中,我们以钓鱼为例,对IO多路复用有了一个简单的认识。下面对钓鱼例子进行回顾:小李同时放置了十个鱼竿,并把十个鱼竿连在了一个铃铛上。这样小李就不必在岸边等待。当铃铛响了就表示有鱼上钩,再回来挨个检查到底是哪个鱼竿有鱼上钩即可。接着进一步体会IO多路复用。

在应用层Linux提供了三种实现IO多路复用的模型,分别是select、poll 和 epoll。在本驱动手册中主要偏重于对驱动的讲解,所以应用层中select、poll 和 epoll函数的使用在这里不做重点讲解。

首先来学习下select、poll 和 epoll函数有什么区别呢?poll函数和seslect函数都可以监听多个文件描述符,通过轮询来获取已经准备好的文件描述符。但是epoll函数将主动轮询变成了被动通知,当事件发生时被动接收通知。为了方便理解,举个形象的例子。假如poll和select是公司的前台,某天一位客户来公司找硬件工程师-小李,请求前台帮忙找人。于是poll和select前台带着这位客户挨个屋子寻找小李,直到找到小李为止。假如epoll是公司的前台,他提前统计了公司每个员工的工位。当客户来找小李的时候,不必像poll select一样,可以直接带着客户到硬件部门去找小李。从上面的俩个例子,明显对比epoll的效率更高。假如公司园区很大,那么poll select需要花费很长时间寻找小李,而epoll已经提前知道小李坐在哪个工位了,直接带客户去找小李即可。

select,poll,epoll有什么区别呢?在单个线程中,select函数最大可以监视1024个文件描述符,而poll函数和select函数并没有什么区别,只是poll函数没有最大文件描述符的限制。在本章节的实验中,以poll为例进行实验。在Linux应用程序中poll函数如下所示:

函数原型:

int poll(struct pollfd *fds,nfds_t nfds,int timeout);

函数功能:

​ 监视并等待多个文件描述符的属性变化

函数参数:

第一个参数fds: 要监视的文件描述符集合以及要监视的事件,为一个数组,数组元素都是结构体pollfd类型,pollfd结构体如下所示:

struct pollfd {
    int fd;       //被监视的文件描述符
    short events;  //等待的事件
    short revents;  //实际发生的事件
}

在pollfd结构体中,第一个成员fd是被监视的文件描述符。第二个成员events是要监视的事件,可监视的事件类型如下所示:

  • POLLIN 有数据可以读取
  • POLLPRI 有紧急的数据需要读取
  • POLLOUT 可以写数据
  • POLLERR 指定的文件描述符发生错误
  • POLLHUP 指定的文件描述符挂起
  • POLLNVAL 无效的请求
  • POLLRDNORM 等同于POLLIN

第三个成员是返回事件,由Linux内核设置具体的返回事件。

第二个参数nfds: poll函数要监视的文件描述符数量

第三个参数timeout:指定等待的时间,单位是ms。无论I/O是否准备好,时间到POLL就会返回。如果timepoll大于0 等待指定的时间,如果timeout等于0,立即返回。如果timeout等于-1,事件发生以后才返回。

函数返回值:

​ 失败返回-1,成功返回revents不为0的文件描述符个数。

当应用程序使用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函数。

返回值:

向应用程序返回资源状态,可以返回的资源状态如下:

  • POLLIN 有数据可以读取
  • POLLPRI 有紧急的数据需要读取
  • POLLOUT 可以写数据
  • POLLERR 指定的文件描述符发生错误
  • POLLHUP 指定的文件描述符挂起
  • POLLNVAL 无效的请求
  • POLLRDNORM 等同于POLLIN,普通数据可读。

函数功能:

这个函数要进行下面两项工作。首先,对可能引起设备文件状态变化的等待队列调用poll_wait(),将对应的等待队列头添加到poll_table.然后返回表示是否能对设备进行无阻塞读写访问的掩码。

驱动程序的poll函数中调用poll_wait函数,注意!poll_wait函数是不会引起阻塞的。poll_wait函数原型如下所示:

void poll_wait(struct file *filp,wait_queue_head_t *queue,poll_table *wait);

参数queue是要添加到poll_table中的等待队列头,参数wait是poll_table,也就是file_operations中poll函数的wait参数。

实验程序编写

28.2.1 编写测试 APP

本实验对应的应用程序网盘路径为:iTOP-RK3568开发板【底板V1.7版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux驱动配套资料\04_Linux驱动例程\22\app。

在应用层Linux提供了三种API函数,分别是select poll和epoll。本次实验使用poll函数进行实验,如果对select 和epoll函数感兴趣,可以查找一些系统编程课程学习。

编写好的应用程序read.c如下所示:

#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main(int argc, char *argv[])  
{
    int fd;  //要监视的文件描述符
    char buf1[32] = {0};   
    char buf2[32] = {0};
    struct pollfd  fds[1];
    int ret;
    fd = open("/dev/test", O_RDWR);  //打开/dev/test设备,阻塞式访问
    if (fd < 0)
    {
        perror("open error \n");
        return fd;
    }
   //构造结构体
    fds[0] .fd =fd;
    fds[0].events = POLLIN; //监视数据是否可以读取
    printf("read before \n");
    while (1)
    {
        ret = poll(fds,1,3000);  //轮询文件是否可操作,超时3000ms
    if(!ret){  //超时
        printf("time out !!\n,");

    }else if(fds[0].revents == POLLIN) //如果返回事件是有数据可读取
    {
        read(fd,buf1,sizeof(buf1));  //从/dev/test文件读取数据
        printf("buf is %s \n,",buf1); //打印读取的数据
        sleep(1);
    }    
} 
    printf("read after\n");
close(fd);     //关闭文件
return 0;
}

上述代码第16行,在打开设备节点时不使用非阻塞方式,要使用阻塞方式,所以改为O_RDWR。

在上述代码的第28行,使用poll函数监视并等待多个文件描述符的属性变化。poll函数第一个参数是被监视的文件描述符,是pollfd结构体类型的数组,所以在14行定义了pollfd结构体类型的数组fds。poll函数第2个参数是要监视的文件描述符数量,这里监视的文件描述符为1个。poll函数第3个参数是指定等待的时间 3000ms。

然后编写应用程序write.c,实现向设备文件写入数据的功能,编写好的write.c如下所示:

#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main(int argc, char *argv[])  
{
    int fd;
    char buf1[32] = {0};   
    char buf2[32] = "nihao";
    fd = open("/dev/test",O_RDWR);  //打开/dev/test设备
    if (fd < 0)
    {
        perror("open error \n");
        return fd;
    }
    printf("write before \n");
    write(fd,buf2,sizeof(buf2));  //向/dev/test文件写入数据
     printf("write after\n");
    close(fd);     //关闭文件
    return 0;
}

28.2.2 驱动程序编写

本实验对应的驱动程序网盘路径为:iTOP-RK3568开发板【底板V1.7版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux驱动配套资料\04_Linux驱动例程\22\module。

IO多路复用实验需要应用程序和驱动程序进行配合,接下来编写驱动程序。编写好的驱动程序如下所示:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

struct device_test{
    dev_t dev_num;  //设备号
     int major ;  //主设备号
    int minor ;  //次设备号
    struct cdev cdev_test; // cdev
    struct class *class;   //类
    struct device *device; //设备
    char kbuf[32];
    int  flag;  //标志位
};
struct  device_test dev1;  
DECLARE_WAIT_QUEUE_HEAD(read_wq); //定义并初始化等待队列头

/*打开设备函数*/
static int cdev_test_open(struct inode *inode, struct file *file)
{
    file->private_data=&dev1;//设置私有数据
    printk("This is cdev_test_open\r\n");
    return 0;
}

/*向设备写入数据函数*/
static ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{
     struct device_test *test_dev=(struct device_test *)file->private_data;

    if (copy_from_user(test_dev->kbuf, buf, size) != 0) // copy_from_user:用户空间向内核空间传数据
    {
        printk("copy_from_user error\r\n");
        return -1;
    }
    test_dev->flag=1;
    wake_up_interruptible(&read_wq);
    return 0;
}

/**从设备读取数据*/
static ssize_t cdev_test_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{
    struct device_test *test_dev=(struct device_test *)file->private_data;
    if(file->f_flags & O_NONBLOCK ){
        if (test_dev->flag !=1)
        return -EAGAIN;
    }
    wait_event_interruptible(read_wq,test_dev->flag);

    if (copy_to_user(buf, test_dev->kbuf, strlen( test_dev->kbuf)) != 0) // copy_to_user:内核空间向用户空间传数据
    {
        printk("copy_to_user error\r\n");
        return -1;
    }
    printk("This is cdev_test_read\r\n");
    return 0;
}

static int cdev_test_release(struct inode *inode, struct file *file)
{
    printk("This is cdev_test_release\r\n");
    return 0;
}

static  __poll_t  cdev_test_poll(struct file *file, struct poll_table_struct *p){
     struct device_test *test_dev=(struct device_test *)file->private_data;  //设置私有数据
     __poll_t mask=0;    
     poll_wait(file,&read_wq,p);     //应用阻塞
     if (test_dev->flag == 1)    
     {
         mask |= POLLIN; 
     }
     return mask; 
}

/*设备操作函数*/
struct file_operations cdev_test_fops = {
    .owner = THIS_MODULE, //将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
    .open = cdev_test_open, //将open字段指向chrdev_open(...)函数
    .read = cdev_test_read, //将open字段指向chrdev_read(...)函数
    .write = cdev_test_write, //将open字段指向chrdev_write(...)函数
    .release = cdev_test_release, //将open字段指向chrdev_release(...)函数
    .poll = cdev_test_poll,  //将poll字段指向chrdev_poll(...)函数
};

static int __init chr_fops_init(void) //驱动入口函数
{
    /*注册字符设备驱动*/
    int ret;
    /*1 创建设备号*/
    ret = alloc_chrdev_region(&dev1.dev_num, 0, 1, "alloc_name"); //动态分配设备号
    if (ret < 0)
    {
       goto err_chrdev;
    }
    printk("alloc_chrdev_region is ok\n");

    dev1.major = MAJOR(dev1.dev_num); //获取主设备号
   dev1.minor = MINOR(dev1.dev_num); //获取次设备号

    printk("major is %d \r\n", dev1.major); //打印主设备号
    printk("minor is %d \r\n", dev1.minor); //打印次设备号
     /*2 初始化cdev*/
    dev1.cdev_test.owner = THIS_MODULE;
    cdev_init(&dev1.cdev_test, &cdev_test_fops);

    /*3 添加一个cdev,完成字符设备注册到内核*/
   ret =  cdev_add(&dev1.cdev_test, dev1.dev_num, 1);
    if(ret<0)
    {
        goto  err_chr_add;
    }
    /*4 创建类*/
  dev1. class = class_create(THIS_MODULE, "test");
    if(IS_ERR(dev1.class))
    {
        ret=PTR_ERR(dev1.class);
        goto err_class_create;
    }
    /*5  创建设备*/
  dev1.device = device_create(dev1.class, NULL, dev1.dev_num, NULL, "test");
    if(IS_ERR(dev1.device))
    {
        ret=PTR_ERR(dev1.device);
        goto err_device_create;
    }

return 0;

err_device_create:
        class_destroy(dev1.class);                 //删除类
err_class_create:
       cdev_del(&dev1.cdev_test);                 //删除cdev
err_chr_add:
        unregister_chrdev_region(dev1.dev_num, 1); //注销设备号
err_chrdev:
        return ret;
}
static void __exit chr_fops_exit(void) //驱动出口函数
{
    /*注销字符设备*/
    unregister_chrdev_region(dev1.dev_num, 1); //注销设备号
    cdev_del(&dev1.cdev_test);                 //删除cdev
    device_destroy(dev1.class, dev1.dev_num);       //删除设备
    class_destroy(dev1.class);                 //删除类
}
module_init(chr_fops_init);
module_exit(chr_fops_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("topeet");

首先在第9行代码添加头文件。然后在第94行将poll字段指向chrdev_poll(…)函数,最后在73行到84行编写这个函数。

28.3 运行测试

28.3.1 编译驱动程序

在上一小节中的poll.c代码同一目录下创建 Makefile 文件,Makefile 文件内容如下所示:

export ARCH=arm64#设置平台架构
export CROSS_COMPILE=aarch64-linux-gnu-#交叉编译器前缀
obj-m += poll.o    #此处要和你的驱动源文件同名
KDIR :=/home/topeet/Linux/linux_sdk/kernel    #这里是你的内核目录                                                                                                                            
PWD ?= $(shell pwd)
all:
    make -C $(KDIR) M=$(PWD) modules    #make操作
clean:
    make -C $(KDIR) M=$(PWD) clean    #make clean操作

对于Makefile的内容注释已在上图添加,保存退出之后,来到存放poll.c和Makefile文件目录下,如下图(图 28-1)所示:

img

然后使用命令“make”进行驱动的编译,编译完成如下图(图 28-2)所示:

RK3568驱动指南|第四篇-高级字符设备进阶-第28章 IO多路复用实验_第1张图片

编译完生成poll.ko目标文件,如下图(图 28-3)所示:

img

至此驱动模块就编译成功了,下面进行应用程序read.c和write.c的编译。

28.3.2 编译应用程序

来到存放应用程序read.c和write.c的文件夹下,使用以下命令对read.c和write.c进行交叉编译,编译完成如下图(图 28-4)所示:

aarch64-linux-gnu-gcc -o read read.c -static

aarch64-linux-gnu-gcc -o write write.c -static

img

生成的read write文件就是之后放在开发板上运行的可执行文件,至此应用程序的编译就完成了。

28.3.3 测试

开发板启动之后,使用以下命令进行驱动模块的加载,如下图(图 28-5)所示:

RK3568驱动指南|第四篇-高级字符设备进阶-第28章 IO多路复用实验_第2张图片

在加载驱动程序之后,会生成如下图(图 28-6)所示的设备节点,在应用程序中也是操作这个设备节点。

img

首先运行read可执行程序,如下(图 28-7)所示,在三秒钟以后打印“time out”。

RK3568驱动指南|第四篇-高级字符设备进阶-第28章 IO多路复用实验_第3张图片

然后运行write可执行程序写入数据,如下(图 28-8)所示:

RK3568驱动指南|第四篇-高级字符设备进阶-第28章 IO多路复用实验_第4张图片

接着可以看到read读取到了数据,如下(图 28-9)所示:

RK3568驱动指南|第四篇-高级字符设备进阶-第28章 IO多路复用实验_第5张图片

RK3568驱动指南|第四篇-高级字符设备进阶-第28章 IO多路复用实验_第6张图片

你可能感兴趣的:(#,RK3568驱动指南,第四期,RK3568驱动开发指南,sql,数据库,java,驱动开发)