本文在第一篇的基础上,重点分析linux下的vivid驱动代码和linux video驱动框架。本文主要从代码入手,然后总结出整个框架。

一、vivid驱动简介

vivid(Virtual Video Test Driver)驱动是在V4L2框架上实现的一个video驱动,它即是一个驱动模版,也是一个测试程序。vivid是一个虚拟摄像头驱动,它产生的数据都是内核模拟摄像头的数据。

网站资料:https://www.kernel.org/doc/html/v4.12/media/v4l-drivers/vivid.html

一般ubuntu系统都自带了vivid驱动,但是默认没有加载。笔者的使用是ubuntu 20.04,内核版本为5.15.1。执行以下命令加载vivid驱动:sudo modprobe vivid。然后可以看到设备节点/dev/videoX。通过VLC播放器可以捕获摄像头数据:

二、应用程序如何使用vivid驱动生成的节点

摄像头图像的捕获简单流程如下,结合应用层的代码流程,对于理解驱动的代码有很大的帮助。

接下来看看应用层代码ioctl函数和驱动相关函数的调用关系,读者自行查看驱动函数的具体实现。

下面给出笔者写的一个简单应用的例子:

#include <errno.h>
#include <fcntl.h>
#include <linux/fb.h>
#include <linux/videodev2.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>


#define CAMERA_FMT_DESC_MAX 100

struct cam_fmt{
    unsigned char description[32]; //字符串描述信息
    unsigned int pixelformat;      //像素格式
};

/*** 描述一个帧缓冲的信息 ***/
typedef struct cam_buf_info
{
    unsigned short *start; //帧缓冲起始地址
    unsigned long length;  //帧缓冲长度
} cam_buf_info;

int main(int argc, char* argv[])
{
    int v4l2_fd = -1;
    int ret = 0;
    const char *device = "/dev/video0";
    struct v4l2_capability cap = {};
    struct v4l2_fmtdesc fmtdesc = {};
    struct cam_fmt cam_fmts[CAMERA_FMT_DESC_MAX];
    struct v4l2_frmsizeenum frmsize = {};
    struct v4l2_frmivalenum frmival = {};
    int i;
    struct v4l2_format fmt = {};
    struct v4l2_streamparm streamparm = {};
    struct v4l2_requestbuffers reqbuf = {};
    struct v4l2_buffer buf = {};
    cam_buf_info buf_infos[3];


    /* 打开摄像头 */
    v4l2_fd = open(device, O_RDWR);
    if (0 > v4l2_fd) {
        fprintf(stderr, "open error: %s: %s\n", device, strerror(errno));
        return -1;
    }

    /* 查询设备功能 */
    ioctl(v4l2_fd, VIDIOC_QUERYCAP, &cap);

    /* 判断是否是视频采集设备 */
    if (!(V4L2_CAP_VIDEO_CAPTURE & cap.capabilities)) {
        fprintf(stderr, "Error: %s: No capture video device!\n", device);
        close(v4l2_fd);
        return -1;
    }

    /* 枚举摄像头所支持的所有像素格式以及描述信息 */
    fmtdesc.index = 0;
    fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    while (0 == ioctl(v4l2_fd, VIDIOC_ENUM_FMT, &fmtdesc)) {
        // 将枚举出来的格式以及描述信息存放在数组中
        cam_fmts[fmtdesc.index].pixelformat = fmtdesc.pixelformat;
        strcpy((char *) cam_fmts[fmtdesc.index].description, (const char *) fmtdesc.description);
        fmtdesc.index++;
        if (fmtdesc.index >= CAMERA_FMT_DESC_MAX) {
            break;
        }
    }

    frmsize.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    frmival.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    for (i = 0; cam_fmts[i].pixelformat; i++) {
        printf("format<0x%x>, description<%s>\n", cam_fmts[i].pixelformat, cam_fmts[i].description);

        /* 枚举出摄像头所支持的所有视频采集分辨率 */
        frmsize.index = 0;
        frmsize.pixel_format = cam_fmts[i].pixelformat;
        frmival.pixel_format = cam_fmts[i].pixelformat;
        while (0 == ioctl(v4l2_fd, VIDIOC_ENUM_FRAMESIZES, &frmsize)) {
            printf("size<%d*%d> ", frmsize.discrete.width, frmsize.discrete.height);
            frmsize.index++;

            /* 获取摄像头视频采集帧率 */
            frmival.index = 0;
            frmival.width = frmsize.discrete.width;
            frmival.height = frmsize.discrete.height;
            while (0 == ioctl(v4l2_fd, VIDIOC_ENUM_FRAMEINTERVALS, &frmival)) {
                printf("<%dfps>", frmival.discrete.denominator / frmival.discrete.numerator);
                frmival.index++;
            }
            printf("\n");
        }
        printf("\n");
    }

    /* 设置帧格式 */
    fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; //type类型
    fmt.fmt.pix.width = 640;       //视频帧宽度
    fmt.fmt.pix.height = 480;     //视频帧高度
    fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_RGB24;  //像素格式
    if (0 > ioctl(v4l2_fd, VIDIOC_S_FMT, &fmt)) {
        fprintf(stderr, "ioctl error: VIDIOC_S_FMT: %s\n", strerror(errno));
        close(v4l2_fd);
        return -1;
    }

    /*** 判断是否已经设置为我们要求的RGB24像素格式
    如果没有设置成功表示该设备不支持RGB24像素格式 */
    if (V4L2_PIX_FMT_RGB24 != fmt.fmt.pix.pixelformat) {
        fprintf(stderr, "Error: the device does not support %x format!\n", V4L2_PIX_FMT_RGB24);
        return -1;
    }

    /* 获取streamparm */
    streamparm.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    ioctl(v4l2_fd, VIDIOC_G_PARM, &streamparm);

    /** 判断是否支持帧率设置 **/
    if (V4L2_CAP_TIMEPERFRAME & streamparm.parm.capture.capability) {
        streamparm.parm.capture.timeperframe.numerator = 1;
        streamparm.parm.capture.timeperframe.denominator = 50; //设置帧率
        if (0 > ioctl(v4l2_fd, VIDIOC_S_PARM, &streamparm)) {
            fprintf(stderr, "ioctl error: VIDIOC_S_PARM: %s\n", strerror(errno));
            return -1;
        }
    }

    /* 申请帧缓冲 */
    reqbuf.count = 3; //帧缓冲的数量
    reqbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    reqbuf.memory = V4L2_MEMORY_MMAP;
    if (0 > ioctl(v4l2_fd, VIDIOC_REQBUFS, &reqbuf)) {
        close(v4l2_fd);
        fprintf(stderr, "ioctl error: VIDIOC_REQBUFS: %s\n", strerror(errno));
        return -1;
    }

    /* 建立内存映射 */
    buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    buf.memory = V4L2_MEMORY_MMAP;
    for (buf.index = 0; buf.index < 3; buf.index++) {
        ioctl(v4l2_fd, VIDIOC_QUERYBUF, &buf);
        buf_infos[buf.index].length = buf.length;
        buf_infos[buf.index].start = (unsigned short *)
            mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, v4l2_fd, buf.m.offset);
        if (MAP_FAILED == buf_infos[buf.index].start) {
            perror("mmap error");
            return -1;
        }
    }

    /* 入队 */
    for (buf.index = 0; buf.index < 3; buf.index++) {
        if (0 > ioctl(v4l2_fd, VIDIOC_QBUF, &buf)) {
            fprintf(stderr, "ioctl error: VIDIOC_QBUF: %s\n", strerror(errno));
            return -1;
        }
    }

    /* 开启捕获 */
    enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    if (0 > ioctl(v4l2_fd, VIDIOC_STREAMON, &type)) {
        fprintf(stderr, "ioctl error: VIDIOC_STREAMON: %s\n", strerror(errno));
        return -1;
    }

    /* 读取涉嫌头数据 */
    struct v4l2_buffer data_buf = {};
    data_buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    data_buf.memory = V4L2_MEMORY_MMAP;
    for (;;) {
        for (data_buf.index = 0; data_buf.index < 3; data_buf.index++) {
            ioctl(v4l2_fd, VIDIOC_DQBUF, &data_buf); //出队

            // 把从摄像头获取的数据放到目标缓冲区,如QT,显卡缓冲区
            //memcpy(dst_buffer, buf_infos[data_buf.index].start, buf_infos[data_buf.index].length);

            // 数据处理完之后、再入队
            ioctl(v4l2_fd, VIDIOC_QBUF, &buf);
        }
    }

    return 0;
}

三、vivid驱动分析

        vivid驱动涉及很多模块,笔者把驱动里面涉及到的模块分为以下几类:

      v4l2-tpg(Test Pattern Generator)是video的测试数据生成器,主要是产生各种格式,各种颜色模式的图像数据,相当于摄像头模块。media controller是多媒体管理框架,它提供了一种标准化的方法来处理多种媒体类型,包括音频、视频、图像等,并用于描述媒体设备的拓扑结构,即描述多个媒体设备之间的连接关系。CEC(Consumer Electronics Control)是一种用于家庭电器设备之间通信的协议,常用于远程控制电视、播放器等设备。v4l2(Video for linux2)提供了一套通用的API来处理视频设备的框架。本文重点研究vivid驱动的v4l2框架部分,对CEC、v4l2-tpg和Media controller暂不做分析。可见,一个驱动往往会涉及到几个子模块,要理清各个模块的关系,哪些需要重点分析,哪些需要当做黑盒处理,并结合应用层来分析数据流向和处理过程。

3.1、vivid驱动初始化

使用了platform的驱动-总线-设备模型。

3.2、应用层和内核的调用关系

前面有介绍应用层如何使用vivid驱动生成的节点,在这里我们具体分析一下调用关系。在vivid驱动初始化里面提到,提供给应用层使用的设备节点/dev/videoX是video_register_device函数来创建的,经过追踪代码发现,其会注册以下struct file_operations结构体:

static const struct file_operations v4l2_fops = {
	.owner = THIS_MODULE,
	.read = v4l2_read,
	.write = v4l2_write,
	.open = v4l2_open,
	.get_unmapped_area = v4l2_get_unmapped_area,
	.mmap = v4l2_mmap,
	.unlocked_ioctl = v4l2_ioctl,
#ifdef CONFIG_COMPAT
	.compat_ioctl = v4l2_compat_ioctl32,
#endif
	.release = v4l2_release,
	.poll = v4l2_poll,
	.llseek = no_llseek,
};

所以执行v4l2_fd = open(device, O_RDWR)开摄像头,就会调用v4l2_open函数。应用层调用ioctl也就会调用到v4l2_ioctl函数。应用层调用mmap的时候就会调用到v4l2_mmap函数。而v4l2_open、v4l2_ioctl和v4l2_mmap又会调用到以下结构体中对应的函数:

static const struct v4l2_file_operations vivid_fops = {
	.owner		= THIS_MODULE,
	.open           = v4l2_fh_open,
	.release        = vivid_fop_release,
	.read           = vb2_fop_read,
	.write          = vb2_fop_write,
	.poll		= vb2_fop_poll,
	.unlocked_ioctl = video_ioctl2,
	.mmap           = vb2_fop_mmap,
};

所以调用关系如下图所示:

3.2、如何申请内存

并结合上面提到的应用层和内核的调用关系,接下来详细介绍通过ioctl来申请内存的过程。

1、函数调用关系。(温馨提醒:鼠标右键图片另存为...把图片保存下来看得就会比较清晰)

2、如何确定num_buffers、num_planes、plane_sizes参数:

3、最终申请到的内存存放的位置

内存申请完成后,可以通过很多的关键结构体找到帧缓存区内存的地址。比如通过struct video_device-->struct vb2_queue->struct vb2_buffer-->struct vb2_plane-->struct vb2_vmalloc_buf。驱动的各个关键结构体直接都是环环相扣的。

3.3、mmap 内核中的内存映射到用户层

上面介绍了v4l2框架是如何在内核中申请内存的,成功在内核申请了3个帧大小的内存,摄像头的数据最终都是要给到应用程序,这就涉及内核数据和应用层数据的传输问题。内核数据和应用层数据的交互一般有以下几种方式:1、copy_to_user和copy_from_user,相当于两个不同的内存区域相互之间复制粘贴;2、mmap内存映射;3、dma,本质也是两个内存区域相互复制粘贴,只是不需要cpu参与复制粘贴操作。接下来介绍vivid驱动的mmap方式来同步内核到应用层的数据。

vivid驱动的内存映射就是把通过内核申请的帧缓冲区内存映射到用户空间,这样用户空间就可以直接读取帧数据。mmap分为文件映射和匿名映射,文件映射是将虚拟地址映射到物理页,然后再将磁盘的数据同步到物理页,或者把物理页的数据同步到磁盘;而匿名映射只是将虚拟地址映射到物理页 ,数据的交互只发生在物理内存中。很明显vivid驱动的mmap实现的是匿名映射。

四、摄像头的控制

下面介绍通过ioctl来给调用自定义的v4l2_ctrl函数:

还可以通过/dev/v4l-subdevX来给摄像头传递自定义的参数。具体的实现方式将在下一篇文章讲解。

五、关键数据结构

struct video_device{
	...
	const struct v4l2_file_operations *fops;
	struct v4l2_device *v4l2_dev;
	struct v4l2_ctrl_handler *ctrl_handler;
	struct vb2_queue *queue;
	const struct v4l2_ioctl_ops *ioctl_ops;
	...
}

struct video_device是V4L2框架的所有重要的结构体的汇总,不仅包含应用层操作/dev/videoX的fops函数,还可以通过它可以找到其他相应的回调函数,比如控制摄像头相关的回调函数int (*s_ctrl)(struct v4l2_ctrl *ctrl);也可以通过它来找到帧缓冲区的地址struct vb2_vmalloc_buf->vaddr。

struct v4l2_ctrl_handler {
	struct mutex _lock;
	struct mutex *lock;
	struct list_head ctrls;
	struct list_head ctrl_refs;
	struct v4l2_ctrl_ref *cached;
	struct v4l2_ctrl_ref **buckets;
	v4l2_ctrl_notify_fnc notify;
	void *notify_priv;
	u16 nr_of_buckets;
	int error;
};

struct v4l2_ctrl_handler可以把所有的struct v4l2_ctrl通过链表连接到一起。

struct v4l2_ctrl {
	struct list_head node;
	...
	struct v4l2_ctrl_handler *handler;
	const struct v4l2_ctrl_ops *ops;
	..
}

struct v4l2_ctrl包含了标准的摄像头控制函数,比如调节亮度V4L2_CID_BRIGHTNESS,控制图像翻转等;也可以注册成为自定义的摄像头控制函数。

struct vb2_queue {
	...
	const struct vb2_ops		*ops;
	const struct vb2_mem_ops	*mem_ops;
	const struct vb2_buf_ops	*buf_ops;
	struct vb2_buffer		*bufs[VB2_MAX_FRAME];
	unsigned int		num_buffers;
	...
}

struct vb2_queue是一个非常重要的结构体,包含了分配帧缓冲区的回调函数,帧缓冲区的初始化、数据流的start_streaming和stop_streaming等操作,还可以通过它来找到帧缓冲区的地址。

        各个结构体之间都是有着千丝万缕的关系,通过某一个结构体就可以找他其他所有结构体,理清楚各个主要的结构体之间的关系,是理解V4L2框架的基础。

六、总结

想从应用层代码和驱动代码两个方面去分析,并且还要捋清楚各个主要结构体之间的关系,函数之间的调用关系,才能比较透彻得理解V4L2这个框架。

驱动最终会通过vmalloc_user函数来申请内存,然后应用程序通过mmap的方式,把内核下申请的内存映射到用户空间。

vivid驱动的代码只是把vmalloc_user申请到的用于存放帧数据的内存,通过软件模拟产生相应的数据帧。在实际应用中,比如使用fpga采集到的摄像头真实数据,并存放在该内存中,就可以把数据传到用户空间了。

后记

笔者将在下一篇博客介绍如果把fpga采集到的摄像头数据通过v4l2框架,送到应用层,这样应用层代码不需要修改就可以获取到fpga摄像头数据了。

参考资料

1、linux内核的vivid代码:drivers/media/test-drivers/vivid

2、 网站资料:https://www.kernel.org/doc/html/v4.12/media/v4l-drivers/vivid.html

更多推荐