精华内容
下载资源
问答
  • Linux V4L2驱动详解

    2019-10-15 02:11:56
    嵌入式LinuxV4L2驱动详解 目录 一、API介绍 二、注册和open 三、基本ioctl处理 四、输入和输出 五、颜色与格式 六、格式协商 七、基本的帧IO 八、流IO 九、控制
  • v4l2设备读取数据的方式有两种,一种是read方式,一种是streaming方式。read方式很容易理解,就是通过read函数读取,而streaming方式是在内核空间中维护一个缓存队列,然后将内存映射到用户空间,应用读取图像数据...

    系列文章
    Linux V4L2驱动框架分析之(一):架构介绍
    Linux V4L2驱动框架分析之(二):平台v4l2设备驱动
    Linux V4L2驱动框架分析之(三):v4l2设备的缓存管理
    Linux V4L2驱动框架分析之(四):sensor驱动

    v4l2设备读取数据的方式有两种,一种是read方式,一种是streaming方式。read方式很容易理解,就是通过read函数读取,而streaming方式是在内核空间中维护一个缓存队列,然后将内存映射到用户空间,应用读取图像数据就是一个不断地出队列和入队列的过程,如下图所示:
    在这里插入图片描述
    使用streaming方式,需要管理多块缓冲,内核通过vb2_queue来管理,vb2_queue即缓冲队列。

    应用程序查询设备功能,判断设备是否支持streaming方式:

    if (ioctl(fd, VIDIOC_QUERYCAP, cap) < 0)
    {
        printf("ERR(%s):VIDIOC_QUERYCAP failed\n", __func__);
        return -1;
    }
    

    传入的cap参数是一个truct v4l2_capability结构体的指针,该结构体的定义如下:

    /* include/uapi/linux/videodev2.h */
    struct v4l2_capability {
    	__u8	driver[16];
    	__u8	card[32];
    	__u8	bus_info[32];
    	__u32   version;
    	__u32	capabilities;
    	__u32	device_caps;
    	__u32	reserved[3];
    };
    

    其中最重要的是capabilities字段,这个字段标记着v4l2设备的功能,capabilities有以下部分标记位:
    在这里插入图片描述
    struct video_device结构体有一成员queue:

    struct video_device
    {
    	......
    	struct vb2_queue *queue;
    	......
    };
    

    采用streaming方式时,平台v4l2设备驱动需要设置与初始化该成员。

    struct vb2_queue结构体定义如下:

    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 list_head		queued_list;
    	unsigned int			queued_count;
    
    
    	struct list_head		done_list;
    	wait_queue_head_t		done_wq;
    	
    	......
    };
    

    一般在v4l2_file_operations的open回调里设置并调用vb2_queue_init函数初始化vb2_queue,最关健的要设置vb2_queue的ops、mem_ops成员。

    vb2_queue的mem_ops成员里有申请、释放和映射缓存的等回调函数,数据类型为struct vb2_mem_ops,定义如下:

    struct vb2_mem_ops {
    
    	//申请缓存
    	void		*(*alloc)(struct device *dev, unsigned long attrs,
    				  unsigned long size,
    				  enum dma_data_direction dma_dir,
    				  gfp_t gfp_flags);
    	void		(*put)(void *buf_priv);
    	struct dma_buf *(*get_dmabuf)(void *buf_priv, unsigned long flags);
    
    	void		*(*get_userptr)(struct device *dev, unsigned long vaddr,
    					unsigned long size,
    					enum dma_data_direction dma_dir);
    	void		(*put_userptr)(void *buf_priv);
    
    	void		(*prepare)(void *buf_priv);
    	void		(*finish)(void *buf_priv);
    
    	void		*(*attach_dmabuf)(struct device *dev,
    					  struct dma_buf *dbuf,
    					  unsigned long size,
    					  enum dma_data_direction dma_dir);
    	void		(*detach_dmabuf)(void *buf_priv);
    	int		(*map_dmabuf)(void *buf_priv);
    	void		(*unmap_dmabuf)(void *buf_priv);
    
    	void		*(*vaddr)(void *buf_priv);
    	void		*(*cookie)(void *buf_priv);
    
    	unsigned int	(*num_users)(void *buf_priv);
    
    	//映射缓存
    	int		(*mmap)(void *buf_priv, struct vm_area_struct *vma);
    };
    

    vb2_queue支持这三种形式缓存,内核提供了三种vb2_mem_ops操作函数集:

    /* drivers/media/v4l2-core/videobuf2-vmalloc.c 
       分配的缓冲区,虚拟地址连续,物理地址不一定连续
     */
    const struct vb2_mem_ops vb2_vmalloc_memops = {
    	.alloc		= vb2_vmalloc_alloc,
    	.put		= vb2_vmalloc_put,
    	.get_userptr	= vb2_vmalloc_get_userptr,
    	.put_userptr	= vb2_vmalloc_put_userptr,
    #ifdef CONFIG_HAS_DMA
    	.get_dmabuf	= vb2_vmalloc_get_dmabuf,
    #endif
    	.map_dmabuf	= vb2_vmalloc_map_dmabuf,
    	.unmap_dmabuf	= vb2_vmalloc_unmap_dmabuf,
    	.attach_dmabuf	= vb2_vmalloc_attach_dmabuf,
    	.detach_dmabuf	= vb2_vmalloc_detach_dmabuf,
    	.vaddr		= vb2_vmalloc_vaddr,
    	.mmap		= vb2_vmalloc_mmap,
    	.num_users	= vb2_vmalloc_num_users,
    };
    
    /* drivers/media/v4l2-core/videobuf2-dma-sg.c 
       对于支持Scatter/Gather的DMA,可使用vb2_dma_sg_memops 
     */
    const struct vb2_mem_ops vb2_dma_sg_memops = {
    	.alloc		= vb2_dma_sg_alloc,
    	.put		= vb2_dma_sg_put,
    	.get_userptr	= vb2_dma_sg_get_userptr,
    	.put_userptr	= vb2_dma_sg_put_userptr,
    	.prepare	= vb2_dma_sg_prepare,
    	.finish		= vb2_dma_sg_finish,
    	.vaddr		= vb2_dma_sg_vaddr,
    	.mmap		= vb2_dma_sg_mmap,
    	.num_users	= vb2_dma_sg_num_users,
    	.get_dmabuf	= vb2_dma_sg_get_dmabuf,
    	.map_dmabuf	= vb2_dma_sg_map_dmabuf,
    	.unmap_dmabuf	= vb2_dma_sg_unmap_dmabuf,
    	.attach_dmabuf	= vb2_dma_sg_attach_dmabuf,
    	.detach_dmabuf	= vb2_dma_sg_detach_dmabuf,
    	.cookie		= vb2_dma_sg_cookie,
    };
    
    /* drivers/media/v4l2-core/videobuf2-dma-contig.c */
    const struct vb2_mem_ops vb2_dma_contig_memops = {
    	.alloc		= vb2_dc_alloc,
    	.put		= vb2_dc_put,
    	.get_dmabuf	= vb2_dc_get_dmabuf,
    	.cookie		= vb2_dc_cookie,
    	.vaddr		= vb2_dc_vaddr,
    	.mmap		= vb2_dc_mmap,
    	.get_userptr	= vb2_dc_get_userptr,
    	.put_userptr	= vb2_dc_put_userptr,
    	.prepare	= vb2_dc_prepare,
    	.finish		= vb2_dc_finish,
    	.map_dmabuf	= vb2_dc_map_dmabuf,
    	.unmap_dmabuf	= vb2_dc_unmap_dmabuf,
    	.attach_dmabuf	= vb2_dc_attach_dmabuf,
    	.detach_dmabuf	= vb2_dc_detach_dmabuf,
    	.num_users	= vb2_dc_num_users,
    };
    

    根据具体设备使用缓存区的方式,选择对应的vb2_mem_ops。

    应用程序申请缓存:

    struct v4l2_requestbuffers req;
    
    req.count = nr_bufs; //缓存数量
    req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    req.memory = V4L2_MEMORY_MMAP;
    
    if (ioctl(fd, VIDIOC_REQBUFS, &req) < 0)
    {
        printf("ERR(%s):VIDIOC_REQBUFS failed\n", __func__);
        return -1;
    }
    

    到内核态的执行流程:
    在这里插入图片描述
    申请缓冲之后,把缓冲放入队列:

    struct v4l2_buffer v4l2_buffer;
    
    for(i = 0; i < nr_bufs; i++)
    {
    	memset(&v4l2_buffer, 0, sizeof(struct v4l2_buffer));
    	v4l2_buffer.index = i; //想要放入队列的缓存
    	v4l2_buffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    	v4l2_buffer.memory = V4L2_MEMORY_MMAP;	
    
        ret = ioctl(fd, VIDIOC_QBUF, &v4l2_buffer);
        if(ret < 0)
        {
            printf("Unable to queue buffer.\n");
            return -1;
        }
    }
    

    到内核态的执行流程:
    在这里插入图片描述
    把缓冲放入队列之后就是映射缓存了。因为如果使用read方式读取的话,图像数据是从内核空间拷贝会应用空间,而一副图像的数据一般来讲是比较大的,所以效率会比较低。而如果使用映射的方式,讲内核空间的内存应用到用户空间,那么用户空间读取数据就想在操作内存一样,不需要经过内核空间到用户空间的拷贝,大大提高效率。

    struct v4l2_buffer v4l2_buffer;
    void* addr;
    
    memset(&v4l2_buffer, 0, sizeof(struct v4l2_buffer));
    v4l2_buffer.index = i; //想要查询的缓存
    v4l2_buffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    v4l2_buffer.memory = V4L2_MEMORY_MMAP;
    
    /* 查询缓存信息 */
    ret = ioctl(fd, VIDIOC_QUERYBUF, &v4l2_buffer);
    if(ret < 0)
    {
        printf("Unable to query buffer.\n");
        return -1;
    }
    
    /* 映射 */
    addr = mmap(NULL /* start anywhere */ ,
                v4l2_buffer.length, PROT_READ | PROT_WRITE, MAP_SHARED,
                fd, v4l2_buffer.m.offset);
    

    获取图像数据其实就是一个不断地入队列和出队列地过程,在出队列前要调用poll等待数据准备完成,应用程序poll操作到内核态:
    在这里插入图片描述
    具体的驱动程序在获得到图像数据后,需要从struct vb2_buffer的queued_list链表得到一个struct vb2_buffer,把数据填入vb2_buffer,之后将该vb2_buffer挂入到struct vb2_buffer的done_list链表,最终唤醒进程。

    数据准备完成,进缓存出队:
    在这里插入图片描述
    如果是映射缓存,出队后,应用程序可直接操作缓存里的图像数据,处理完数据之后再次把缓存放入队列。

    展开全文
  • 一.前言最近项目需要做一个工业条形读码器,在底层应该会适配linux v4l2框架,就自己研究了一下在应用层怎么使用v4l2进行编程,查阅了相关资料,主要是网上的博客资料,有:...比较学...

    一.前言

    最近项目需要做一个工业条形读码器,在底层应该会适配linux v4l2框架,就自己研究了一下在应用层怎么使用v4l2进行编程,查阅了相关资料,主要是网上的博客资料,有:

    https://www.cnblogs.com/emouse/archive/2013/03/04/2943243.html

    https://www.jianshu.com/p/fd5730e939e7

    大家可以进行查阅,比较学习,敝人也是参考,在一些地方进行了补充,希望可以共同学习,话不多说,喝杯咖啡,开动。

    二.简介

    百度这么说v4l2

    百度说,v4l2是Video4linux2的简称,是linux中关于视频设备的内核驱动,在Linux中,视频设备是设备文件,可以像访问普通文件一样对其进行读写,摄像头设备文件位置是/dev/video0。V4L2在设计时,是要支持很多广泛的设备的,它们之中只有一部分在本质上是真正的视频设备。

    因为我们这篇文章不涉及内核部分摄像头驱动的实现,大致可以简单说下,内核部分的实现分两部分:1.我们要根据摄像头的种类,实现具体的摄像头传感器的驱动,这里可能有一些数据和控制的通信总线的协议。2.然后这个具体的驱动需要适配这个v4l2这个框架,然后向用户层映射成一个字符设备文件。而我们的主题就是对这个设备文件进行操作,这就是应用层该做的事,你可能会说,这能学到什么,而我要说,学习是渐进的,先理解应用层的需求,对内核驱动的实现也会有更深的理解,你说呢?

    三.使用V4L2的一般步骤

    使用V4L2进行视频采集,一般是五个步骤:

    1.打开设备,进行初始化参数设置

    2.申请图像帧缓冲,并进行内存映射

    3.把帧缓冲进行入队操作,开始视频流进行采集

    4.进行出队,然后对数据进行处理,然后入队,如此往复

    5.释放资源,停止采集工作

    流程如下:

    0ac427d267d4

    视频采集流程图

    四.V4L2编程例子

    我这里是使用vmware+ubuntu14.04的环境,笔记本自带一个前置摄像头。

    首先先保证你的摄像头可以在虚拟机下使用。

    1.windows + r运行services.msc命令,打开服务列表,找到如下所示服务,手动开启

    0ac427d267d4

    image.png

    2.开启虚拟机,在 虚拟机->可移动设备 可以看到你的usb摄像头,在虚拟机设置里,usb控制器一栏注意兼容性,可能要设置成兼容3.0,不然有问题。

    3.ubuntu下使用cheese命令,查看你的摄像头有没有问题

    0ac427d267d4

    image.png

    前期工作完成,下面是源码+Makefile

    /*v4l2_example.c*/

    #include

    #include

    #include

    #include

    #include

    #include

    #include

    #include

    #include

    #include

    #include

    #define TRUE (1)

    #define FALSE (0)

    #define FILE_VIDEO "/dev/video0"

    #define IMAGE "./img/demo"

    #define IMAGEWIDTH 640

    #define IMAGEHEIGHT 480

    #define FRAME_NUM 4

    int fd;

    struct v4l2_buffer buf;

    struct buffer

    {

    void * start;

    unsigned int length;

    long long int timestamp;

    } *buffers;

    int v4l2_init()

    {

    struct v4l2_capability cap;

    struct v4l2_fmtdesc fmtdesc;

    struct v4l2_format fmt;

    struct v4l2_streamparm stream_para;

    //打开摄像头设备

    if ((fd = open(FILE_VIDEO, O_RDWR)) == -1)

    {

    printf("Error opening V4L interface\n");

    return FALSE;

    }

    //查询设备属性

    if (ioctl(fd, VIDIOC_QUERYCAP, &cap) == -1)

    {

    printf("Error opening device %s: unable to query device.\n",FILE_VIDEO);

    return FALSE;

    }

    else

    {

    printf("driver:\t\t%s\n",cap.driver);

    printf("card:\t\t%s\n",cap.card);

    printf("bus_info:\t%s\n",cap.bus_info);

    printf("version:\t%d\n",cap.version);

    printf("capabilities:\t%x\n",cap.capabilities);

    if ((cap.capabilities & V4L2_CAP_VIDEO_CAPTURE) == V4L2_CAP_VIDEO_CAPTURE)

    {

    printf("Device %s: supports capture.\n",FILE_VIDEO);

    }

    if ((cap.capabilities & V4L2_CAP_STREAMING) == V4L2_CAP_STREAMING)

    {

    printf("Device %s: supports streaming.\n",FILE_VIDEO);

    }

    }

    //显示所有支持帧格式

    fmtdesc.index=0;

    fmtdesc.type=V4L2_BUF_TYPE_VIDEO_CAPTURE;

    printf("Support format:\n");

    while(ioctl(fd,VIDIOC_ENUM_FMT,&fmtdesc)!=-1)

    {

    printf("\t%d.%s\n",fmtdesc.index+1,fmtdesc.description);

    fmtdesc.index++;

    }

    //检查是否支持某帧格式

    struct v4l2_format fmt_test;

    fmt_test.type=V4L2_BUF_TYPE_VIDEO_CAPTURE;

    fmt_test.fmt.pix.pixelformat=V4L2_PIX_FMT_RGB32;

    if(ioctl(fd,VIDIOC_TRY_FMT,&fmt_test)==-1)

    {

    printf("not support format RGB32!\n");

    }

    else

    {

    printf("support format RGB32\n");

    }

    //查看及设置当前格式

    printf("set fmt...\n");

    fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;

    fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_RGB32; //jpg格式

    //fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;//yuv格式

    fmt.fmt.pix.height = IMAGEHEIGHT;

    fmt.fmt.pix.width = IMAGEWIDTH;

    fmt.fmt.pix.field = V4L2_FIELD_INTERLACED;

    printf("fmt.type:\t\t%d\n",fmt.type);

    printf("pix.pixelformat:\t%c%c%c%c\n",fmt.fmt.pix.pixelformat & 0xFF, (fmt.fmt.pix.pixelformat >> 8) & 0xFF,(fmt.fmt.pix.pixelformat >> 16) & 0xFF, (fmt.fmt.pix.pixelformat >> 24) & 0xFF);

    printf("pix.height:\t\t%d\n",fmt.fmt.pix.height);

    printf("pix.width:\t\t%d\n",fmt.fmt.pix.width);

    printf("pix.field:\t\t%d\n",fmt.fmt.pix.field);

    if(ioctl(fd, VIDIOC_S_FMT, &fmt) == -1)

    {

    printf("Unable to set format\n");

    return FALSE;

    }

    printf("get fmt...\n");

    if(ioctl(fd, VIDIOC_G_FMT, &fmt) == -1)

    {

    printf("Unable to get format\n");

    return FALSE;

    }

    {

    printf("fmt.type:\t\t%d\n",fmt.type);

    printf("pix.pixelformat:\t%c%c%c%c\n",fmt.fmt.pix.pixelformat & 0xFF, (fmt.fmt.pix.pixelformat >> 8) & 0xFF,(fmt.fmt.pix.pixelformat >> 16) & 0xFF, (fmt.fmt.pix.pixelformat >> 24) & 0xFF);

    printf("pix.height:\t\t%d\n",fmt.fmt.pix.height);

    printf("pix.width:\t\t%d\n",fmt.fmt.pix.width);

    printf("pix.field:\t\t%d\n",fmt.fmt.pix.field);

    }

    //设置及查看帧速率,这里只能是30帧,就是1秒采集30张图

    memset(&stream_para, 0, sizeof(struct v4l2_streamparm));

    stream_para.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;

    stream_para.parm.capture.timeperframe.denominator = 30;

    stream_para.parm.capture.timeperframe.numerator = 1;

    if(ioctl(fd, VIDIOC_S_PARM, &stream_para) == -1)

    {

    printf("Unable to set frame rate\n");

    return FALSE;

    }

    if(ioctl(fd, VIDIOC_G_PARM, &stream_para) == -1)

    {

    printf("Unable to get frame rate\n");

    return FALSE;

    }

    {

    printf("numerator:%d\ndenominator:%d\n",stream_para.parm.capture.timeperframe.numerator,stream_para.parm.capture.timeperframe.denominator);

    }

    return TRUE;

    }

    int v4l2_mem_ops()

    {

    unsigned int n_buffers;

    struct v4l2_requestbuffers req;

    //申请帧缓冲

    req.count=FRAME_NUM;

    req.type=V4L2_BUF_TYPE_VIDEO_CAPTURE;

    req.memory=V4L2_MEMORY_MMAP;

    if(ioctl(fd,VIDIOC_REQBUFS,&req)==-1)

    {

    printf("request for buffers error\n");

    return FALSE;

    }

    // 申请用户空间的地址列

    buffers = malloc(req.count*sizeof (*buffers));

    if (!buffers)

    {

    printf ("out of memory!\n");

    return FALSE;

    }

    // 进行内存映射

    for (n_buffers = 0; n_buffers < FRAME_NUM; n_buffers++)

    {

    buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;

    buf.memory = V4L2_MEMORY_MMAP;

    buf.index = n_buffers;

    //查询

    if (ioctl (fd, VIDIOC_QUERYBUF, &buf) == -1)

    {

    printf("query buffer error\n");

    return FALSE;

    }

    //映射

    buffers[n_buffers].length = buf.length;

    buffers[n_buffers].start = mmap(NULL,buf.length,PROT_READ|PROT_WRITE, MAP_SHARED, fd, buf.m.offset);

    if (buffers[n_buffers].start == MAP_FAILED)

    {

    printf("buffer map error\n");

    return FALSE;

    }

    }

    return TRUE;

    }

    int v4l2_frame_process()

    {

    unsigned int n_buffers;

    enum v4l2_buf_type type;

    char file_name[100];

    char index_str[10];

    long long int extra_time = 0;

    long long int cur_time = 0;

    long long int last_time = 0;

    //入队和开启采集

    for (n_buffers = 0; n_buffers < FRAME_NUM; n_buffers++)

    {

    buf.index = n_buffers;

    ioctl(fd, VIDIOC_QBUF, &buf);

    }

    type = V4L2_BUF_TYPE_VIDEO_CAPTURE;

    ioctl(fd, VIDIOC_STREAMON, &type);

    //出队,处理,写入yuv文件,入队,循环进行

    int loop = 0;

    while(loop < 15)

    {

    for(n_buffers = 0; n_buffers < FRAME_NUM; n_buffers++)

    {

    //出队

    buf.index = n_buffers;

    ioctl(fd, VIDIOC_DQBUF, &buf);

    //查看采集数据的时间戳之差,单位为微妙

    buffers[n_buffers].timestamp = buf.timestamp.tv_sec*1000000+buf.timestamp.tv_usec;

    cur_time = buffers[n_buffers].timestamp;

    extra_time = cur_time - last_time;

    last_time = cur_time;

    printf("time_deta:%lld\n\n",extra_time);

    printf("buf_len:%d\n",buffers[n_buffers].length);

    //处理数据只是简单写入文件,名字以loop的次数和帧缓冲数目有关

    printf("grab image data OK\n");

    memset(file_name,0,sizeof(file_name));

    memset(index_str,0,sizeof(index_str));

    sprintf(index_str,"%d",loop*4+n_buffers);

    strcpy(file_name,IMAGE);

    strcat(file_name,index_str);

    strcat(file_name,".jpg");

    //strcat(file_name,".yuv");

    FILE *fp2 = fopen(file_name, "wb");

    if(!fp2)

    {

    printf("open %s error\n",file_name);

    return(FALSE);

    }

    fwrite(buffers[n_buffers].start, IMAGEHEIGHT*IMAGEWIDTH*2,1,fp2);

    fclose(fp2);

    printf("save %s OK\n",file_name);

    //入队循环

    ioctl(fd, VIDIOC_QBUF, &buf);

    }

    loop++;

    }

    return TRUE;

    }

    int v4l2_release()

    {

    unsigned int n_buffers;

    enum v4l2_buf_type type;

    //关闭流

    type = V4L2_BUF_TYPE_VIDEO_CAPTURE;

    ioctl(fd, VIDIOC_STREAMON, &type);

    //关闭内存映射

    for(n_buffers=0;n_buffers

    {

    munmap(buffers[n_buffers].start,buffers[n_buffers].length);

    }

    //释放自己申请的内存

    free(buffers);

    //关闭设备

    close(fd);

    return TRUE;

    }

    /*int v4l2_video_input_output()

    {

    struct v4l2_input input;

    struct v4l2_standard standard;

    //首先获得当前输入的index,注意只是index,要获得具体的信息,就的调用列举操作

    memset (&input,0,sizeof(input));

    if (-1 == ioctl (fd, VIDIOC_G_INPUT, &input.index)) {

    printf("VIDIOC_G_INPUT\n");

    return FALSE;

    }

    //调用列举操作,获得 input.index 对应的输入的具体信息

    if (-1 == ioctl (fd, VIDIOC_ENUMINPUT, &input)) {

    printf("VIDIOC_ENUM_INPUT \n");

    return FALSE;

    }

    printf("Current input %s supports:\n", input.name);

    //列举所有的所支持的 standard,如果 standard.id 与当前 input 的 input.std 有共同的

    //bit flag,意味着当前的输入支持这个 standard,这样将所有驱动所支持的 standard 列举一个

    //遍,就可以找到该输入所支持的所有 standard 了。

    memset(&standard,0,sizeof (standard));

    standard.index = 0;

    while(0 == ioctl(fd, VIDIOC_ENUMSTD, &standard)) {

    if (standard.id & input.std){

    printf ("%s\n", standard.name);

    }

    standard.index++;

    }

    // EINVAL indicates the end of the enumeration, which cannot be empty unless this device falls under the USB exception.

    if (errno != EINVAL || standard.index == 0) {

    printf("VIDIOC_ENUMSTD\n");

    return FALSE;

    }

    }*/

    int main(int argc, char const *argv[])

    {

    printf("begin....\n");

    sleep(10);

    v4l2_init();

    printf("init....\n");

    sleep(10);

    v4l2_mem_ops();

    printf("malloc....\n");

    sleep(10);

    v4l2_frame_process();

    printf("process....\n");

    sleep(10);

    v4l2_release();

    printf("release\n");

    sleep(20);

    return TRUE;

    }

    #Makefile

    CC = gcc

    CFLAGS = -g -Wall -O2

    TARGET = v4l2_example.bin

    SRCS = v4l2_example.c

    C_OBJS = v4l2_example.o

    all:$(TARGET)

    $(TARGET):$(C_OBJS)

    $(CC) $(CFLAGS) -o $@ $^

    %.o:%.c

    $(CC) $(CFLAGS) -c -o $@ $<

    .PHONY:clean

    clean:

    rm -rf *.o $(TARGET) $(CXX_OBJS) $(C_OBJS)

    rm ./img/*

    v4l2_example.c,Makefile所在目录新建img目录用于存放图像文件,可以编译运行,如下:

    begin....

    driver: uvcvideo

    card: HP Wide Vision HD Camera

    bus_info: usb-0000:03:00.0-2

    version: 201480

    capabilities: 84200001

    Device /dev/video0: supports capture.

    Device /dev/video0: supports streaming.

    Support format:

    1.MJPEG

    2.YUV 4:2:2 (YUYV)

    support format RGB32

    set fmt...

    fmt.type: 1

    pix.pixelformat: RGB4

    pix.height: 480

    pix.width: 640

    pix.field: 4

    get fmt...

    fmt.type: 1

    pix.pixelformat: MJPG

    pix.height: 480

    pix.width: 640

    pix.field: 1

    numerator:1

    denominator:30

    init....

    malloc....

    time_deta:17741459080

    buf_len:614400

    grab image data OK

    save ./img/demo0.jpg OK

    time_deta:33239

    buf_len:614400

    grab image data OK

    save ./img/demo1.jpg OK

    time_deta:33466

    buf_len:614400

    grab image data OK

    save ./img/demo2.jpg OK

    time_deta:33139

    buf_len:614400

    grab image data OK

    save ./img/demo3.jpg OK

    time_deta:33239

    buf_len:614400

    grab image data OK

    save ./img/demo4.jpg OK

    time_deta:33382

    buf_len:614400

    grab image data OK

    save ./img/demo5.jpg OK

    time_deta:33044

    buf_len:614400

    grab image data OK

    save ./img/demo6.jpg OK

    time_deta:33225

    buf_len:614400

    grab image data OK

    save ./img/demo7.jpg OK

    time_deta:33369

    buf_len:614400

    grab image data OK

    save ./img/demo8.jpg OK

    time_deta:33091

    buf_len:614400

    grab image data OK

    save ./img/demo9.jpg OK

    time_deta:32418

    buf_len:614400

    grab image data OK

    save ./img/demo10.jpg OK

    time_deta:34051

    buf_len:614400

    grab image data OK

    save ./img/demo11.jpg OK

    time_deta:33189

    buf_len:614400

    grab image data OK

    save ./img/demo12.jpg OK

    time_deta:32841

    buf_len:614400

    grab image data OK

    save ./img/demo13.jpg OK

    time_deta:33599

    buf_len:614400

    grab image data OK

    save ./img/demo14.jpg OK

    time_deta:32628

    buf_len:614400

    grab image data OK

    save ./img/demo15.jpg OK

    time_deta:33783

    buf_len:614400

    grab image data OK

    save ./img/demo16.jpg OK

    time_deta:32628

    buf_len:614400

    grab image data OK

    save ./img/demo17.jpg OK

    time_deta:33105

    buf_len:614400

    grab image data OK

    save ./img/demo18.jpg OK

    time_deta:34140

    buf_len:614400

    grab image data OK

    save ./img/demo19.jpg OK

    time_deta:33219

    buf_len:614400

    grab image data OK

    save ./img/demo20.jpg OK

    time_deta:33334

    buf_len:614400

    grab image data OK

    save ./img/demo21.jpg OK

    time_deta:33245

    buf_len:614400

    grab image data OK

    save ./img/demo22.jpg OK

    time_deta:33276

    buf_len:614400

    grab image data OK

    save ./img/demo23.jpg OK

    time_deta:33133

    buf_len:614400

    grab image data OK

    save ./img/demo24.jpg OK

    time_deta:32533

    buf_len:614400

    grab image data OK

    save ./img/demo25.jpg OK

    time_deta:34036

    buf_len:614400

    grab image data OK

    save ./img/demo26.jpg OK

    time_deta:33093

    buf_len:614400

    grab image data OK

    save ./img/demo27.jpg OK

    time_deta:33374

    buf_len:614400

    grab image data OK

    save ./img/demo28.jpg OK

    time_deta:33432

    buf_len:614400

    grab image data OK

    save ./img/demo29.jpg OK

    time_deta:32969

    buf_len:614400

    grab image data OK

    save ./img/demo30.jpg OK

    time_deta:32412

    buf_len:614400

    grab image data OK

    save ./img/demo31.jpg OK

    time_deta:34113

    buf_len:614400

    grab image data OK

    save ./img/demo32.jpg OK

    time_deta:33193

    buf_len:614400

    grab image data OK

    save ./img/demo33.jpg OK

    time_deta:33654

    buf_len:614400

    grab image data OK

    save ./img/demo34.jpg OK

    time_deta:33330

    buf_len:614400

    grab image data OK

    save ./img/demo35.jpg OK

    time_deta:32732

    buf_len:614400

    grab image data OK

    save ./img/demo36.jpg OK

    time_deta:33140

    buf_len:614400

    grab image data OK

    save ./img/demo37.jpg OK

    time_deta:33969

    buf_len:614400

    grab image data OK

    save ./img/demo38.jpg OK

    time_deta:33044

    buf_len:614400

    grab image data OK

    save ./img/demo39.jpg OK

    time_deta:32326

    buf_len:614400

    grab image data OK

    save ./img/demo40.jpg OK

    time_deta:333137

    buf_len:614400

    grab image data OK

    save ./img/demo41.jpg OK

    time_deta:31840

    buf_len:614400

    grab image data OK

    save ./img/demo42.jpg OK

    time_deta:33712

    buf_len:614400

    grab image data OK

    save ./img/demo43.jpg OK

    time_deta:32597

    buf_len:614400

    grab image data OK

    save ./img/demo44.jpg OK

    time_deta:67859

    buf_len:614400

    grab image data OK

    save ./img/demo45.jpg OK

    time_deta:33813

    buf_len:614400

    grab image data OK

    save ./img/demo46.jpg OK

    time_deta:33024

    buf_len:614400

    grab image data OK

    save ./img/demo47.jpg OK

    time_deta:33146

    buf_len:614400

    grab image data OK

    save ./img/demo48.jpg OK

    time_deta:33227

    buf_len:614400

    grab image data OK

    save ./img/demo49.jpg OK

    time_deta:33007

    buf_len:614400

    grab image data OK

    save ./img/demo50.jpg OK

    time_deta:33680

    buf_len:614400

    grab image data OK

    save ./img/demo51.jpg OK

    time_deta:32909

    buf_len:614400

    grab image data OK

    save ./img/demo52.jpg OK

    time_deta:32553

    buf_len:614400

    grab image data OK

    save ./img/demo53.jpg OK

    time_deta:33578

    buf_len:614400

    grab image data OK

    save ./img/demo54.jpg OK

    time_deta:33308

    buf_len:614400

    grab image data OK

    save ./img/demo55.jpg OK

    time_deta:33421

    buf_len:614400

    grab image data OK

    save ./img/demo56.jpg OK

    time_deta:32596

    buf_len:614400

    grab image data OK

    save ./img/demo57.jpg OK

    time_deta:33463

    buf_len:614400

    grab image data OK

    save ./img/demo58.jpg OK

    time_deta:33100

    buf_len:614400

    grab image data OK

    save ./img/demo59.jpg OK

    process....

    release

    相关的说明注释已经很明显了,关于被注释的函数int v4l2_video_input_output()目前有些问题,你也可以研究一下,其他应该没问题。

    通过结果,可以看出,图片大小640*480,格式是MJPG,帧率为30,采集了60张图片,每次缓冲区的时间戳之差为33463us左右,1s/33463us = 29.88,约为30。主函数中设置一些sleep 为了在运行中新开终端去测试此程序所占内存,结果为

    begin ->4204Kbyte

    init->4204Kbyte

    malloc ->6736Kbyte

    process->6736Kbyte

    release->4336Kbyte

    申请的一个缓冲区为614400byte,4个一共是2457.6Kbyte,而6736-4204=2532,还要考虑程序自己申请的堆空间及一些其他情况,可以接受

    这边是使用ps -aux | grep 程序名,进行分析的,可能会有更好的工具,更细致的分析,希望你可以评论。

    另外你可以把格式改成yuv的,需要专门yuv的查看器,去看采集的图片

    ,然后还可以把文件打开的方式改成追加方式,把数据写成一个yuv文件,可以进行图像帧的播放,感觉就像是动画。

    展开全文
  • 为了兼容更多的硬件,Linux内核抽象了V4L2(Video for Linux Two)子系统。V4L2子系统是Linux内核中关于Video(视频)设备的API接口,是V4L(Video for Linux)子系统的升级版本。V4L2子系统向上为虚拟文件系统提供...

    1.概述

    Linux系统上的Video设备多种多样,如通过Camera Host控制器接口连接的摄像头,通过USB总线连接的摄像头等。为了兼容更多的硬件,Linux内核抽象了V4L2(Video for Linux Two)子系统。V4L2子系统是Linux内核中关于Video(视频)设备的API接口,是V4L(Video for Linux)子系统的升级版本。V4L2子系统向上为虚拟文件系统提供了统一的接口,应用程序可通过虚拟文件系统访问Video设备。V4L2子系统向下给Video设备提供接口,同时管理所有Video设备。Video设备又分为主设备和从设备,对于Camera来说,Camera Host控制器为主设备,负责图像数据的接收和传输,从设备为Camera Sensor,一般为I2C接口,可通过从设备控制Camera采集图像的行为,如图像的大小、图像的FPS等。主设备可通过v4l2_subdev_call的宏调用从设备提供的方法,反过来从设备可以调用主设备的notify方法通知主设备某些事件发生了。

    V4L2框架

    2.V4L2子系统

    V4L(Video for Linux)是Linux内核中关于视频设备的API接口,涉及视频设备的音频和视频信息采集及处理、视频设备的控制。V4L出现于Linux内核2.1版本,经过修改bug和添加功能,Linux内核2.5版本推出了V4L2(Video for Linux Two)子系统,功能更多且更稳定。V4L2的主设备号是81,次设备号范围0~255,这些次设备号又分为多类设备,如视频设备(次设备号范围0-63)、Radio(收音机)设备(次设备号范围64-127)、Teletext设备(次设备号范围192-223)、VBI设备(次设备号范围224-255)。V4L2设备对应的设备节点有/dev/videoX、/dev/vbiX、/dev/radioX。这里只讨论视频设备,视频设备对应的设备节点是/dev/videoX。视频设备以高频头或Camera为输入源,Linux内核驱动该类设备,接收相应的视频信息并处理。

    2.1.V4L2主设备数据结构

    V4L2主设备实例使用struct v4l2_device结构体表示,v4l2_device是V4L2子系统的入口,管理着V4L2子系统的主设备和从设备。简单设备可以仅分配这个结构体,但在大多数情况下,都会将这个结构体嵌入到一个更大的结构体中。需要与媒体框架整合的驱动必须手动设置dev->driver_data,指向包含v4l2_device结构体实例的驱动特定设备结构体。这可以在注册V4L2设备实例前通过dev_set_drvdata()函数完成。同时必须设置v4l2_device结构体的mdev域,指向适当的初始化并注册过的media_device实例。
    对于视频设备,Camera控制器可以视为主设备,接在Camera控制器上的摄像头可以视为从设备。V4L2子系统使用v4l2_device结构体管理设备,设备的具体操作方法根据设备类型决定,若是视频设备,则需要注册video_device结构体,并提供相应的操作方法。

        [include/media/v4l2-device.h]
        struct v4l2_device {
            struct device *dev;  // 父设备指针
        #if defined(CONFIG_MEDIA_CONTROLLER)  // 多媒体设备配置选项
            // 用于运行时数据流的管理,
            struct media_device *mdev;
        #endif
            // 注册的子设备的v4l2_subdev结构体都挂载此链表中
            struct list_head subdevs;
            // 同步用的自旋锁
            spinlock_t lock;
            // 独一无二的设备名称,默认使用driver name + bus ID
            char name[V4L2_DEVICE_NAME_SIZE];
            // 被一些子设备回调的通知函数,但这个设置与子设备相关。子设备支持的任何通知必须在
            // include/media/<subdevice>.h 中定义一个消息头。
            void (*notify)(struct v4l2_subdev *sd, unsigned int notification, void *arg);
            // 提供子设备(主要是video和ISP设备)在用户空间的特效操作接口,
            // 比如改变输出图像的亮度、对比度、饱和度等等
            struct v4l2_ctrl_handler *ctrl_handler;
            // 设备优先级状态
            struct v4l2_prio_state prio;
            /* BKL replacement mutex. Temporary solution only. */
            struct mutex ioctl_lock;
            // struct v4l2_device结构体的引用计数,等于0时才释放
            struct kref ref;
            // 引用计数ref为0时,调用release函数进行释放资源和清理工作
            void (*release)(struct v4l2_device *v4l2_dev);
        };
    

    使用v4l2_device_register注册v4l2_device结构体.如果v4l2_dev->name为空,则它将被设置为从dev中衍生出的值(为了更加精确,形式为驱动名后跟bus_id)。如果在调用v4l2_device_register前已经设置好了,则不会被修改。如果dev为NULL,则必须在调用v4l2_device_register前设置v4l2_dev->name。可以基于驱动名和驱动的全局atomic_t类型的实例编号,通过v4l2_device_set_name()设置name。这样会生成类似ivtv0ivtv1等名字。若驱动名以数字结尾,则会在编号和驱动名间插入一个破折号,如:cx18-0cx18-1等。dev参数通常是一个指向pci_devusb_interfaceplatform_device的指针,很少使其为NULL,除非是一个ISA设备或者当一个设备创建了多个PCI设备,使得v4l2_dev无法与一个特定的父设备关联。
    使用v4l2_device_unregister卸载v4l2_device结构体。如果dev->driver_data域指向 v4l2_dev,将会被重置为NULL。主设备注销的同时也会自动注销所有子设备。如果你有一个热插拔设备(如USB设备),则当断开发生时,父设备将无效。由于v4l2_device有一个指向父设备的指针必须被清除,同时标志父设备
    已消失,所以必须调用v4l2_device_disconnect函数清理v4l2_device中指向父设备的dev指针。v4l2_device_disconnect并不注销主设备,因此依然要调用v4l2_device_unregister函数注销主设备。

        [include/media/v4l2-device.h]
        // 注册v4l2_device结构体,并初始化v4l2_device结构体
        // dev-父设备结构体指针,若为NULL,在注册之前设备名称name必须被设置,
        // v4l2_dev-v4l2_device结构体指针
        // 返回值-0成功,小于0-失败
        int v4l2_device_register(struct device *dev, struct v4l2_device *v4l2_dev)
    
        // 卸载注册的v4l2_device结构体
        // v4l2_dev-v4l2_device结构体指针
        void v4l2_device_unregister(struct v4l2_device *v4l2_dev)
    
        // 设置设备名称,填充v4l2_device结构体中的name成员
        // v4l2_dev-v4l2_device结构体指针
        // basename-设备名称基本字符串
        // instance-设备计数,调用v4l2_device_set_name后会自加1
        // 返回值-返回设备计数自加1的值
        int v4l2_device_set_name(struct v4l2_device *v4l2_dev, 
                const char *basename, atomic_t *instance)
    
        // 热插拔设备断开时调用此函数
        // v4l2_dev-v4l2_device结构体指针
        void v4l2_device_disconnect(struct v4l2_device *v4l2_dev);
    

    同一个硬件的情况下。如ivtvfb驱动是一个使用ivtv硬件的帧缓冲驱动,同时alsa驱动也使用此硬件。可以使用如下例程遍历所有注册的设备:

        static int callback(struct device *dev, void *p)
        {
            struct v4l2_device *v4l2_dev = dev_get_drvdata(dev);
    
            /* 测试这个设备是否已经初始化 */
            if (v4l2_dev == NULL)
                return 0;
            ...
            return 0;
        }
    
        int iterate(void *p)
        {
            struct device_driver *drv;
            int err;
    
            /* 在PCI 总线上查找ivtv驱动。
            pci_bus_type是全局的. 对于USB总线使用usb_bus_type。 */
            drv = driver_find("ivtv", &pci_bus_type);
            /* 遍历所有的ivtv设备实例 */
            err = driver_for_each_device(drv, NULL, p, callback);
            put_driver(drv);
            return err;
        }
    

    有时你需要一个设备实例的运行计数。这个通常用于映射一个设备实例到一个模块选择数组的索引。推荐方法如下:

        static atomic_t drv_instance = ATOMIC_INIT(0);
        static int drv_probe(struct pci_dev *pdev, const struct pci_device_id *pci_id)
        {
            ...
            state->instance = atomic_inc_return(&drv_instance) - 1;
        }
    

    如果有多个设备节点,对于热插拔设备,知道何时注销v4l2_device结构体就比较困难。为此v4l2_device有引用计数支持。当调用video_register_device时增加引用计数,而设备节点释放时减小引用计数。当引用计数为零,则v4l2_devicerelease回调将被执行。可以在此时做最后的清理工作。如果创建了其他设备节点(比如ALSA),则你可以通过以下函数手动增减引用计数:

        [include/media/v4l2-device.h]
        // 增加引用计数
        void v4l2_device_get(struct v4l2_device *v4l2_dev);
        // 减少引用计数
        int v4l2_device_put(struct v4l2_device *v4l2_dev);
    

    由于引用技术初始化为1,需要在disconnect回调(对于USB设备)中调用v4l2_device_put,或者remove回调(例如对于PCI设备),否则引用计数将永远不会为0。

    2.2.V4L2从设备数据结构

    V4L2从设备使用struct v4l2_subdev结构体表示。一个V4L2主设备可能对应多个V4L2从设备,所有主设备对应的从设备都挂到v4l2_device结构体的subdevs链表中。对于视频设备,从设备就是摄像头,通常情况下是I2C设备,主设备可通过I2C总线控制从设备,例如控制摄像头的焦距、闪光灯等。struct v4l2_subdev结构体重要成员示意图如下图所示。struct v4l2_subdev_video_ops将在《Linux V4L2子系统-Video设备框架分析(二)》中介绍。

    v4l2_subdev结构体示意

        [include/media/v4l2-subdev.h]
        #define V4L2_SUBDEV_FL_IS_I2C		(1U << 0)  // 从设备是I2C设备
        #define V4L2_SUBDEV_FL_IS_SPI		(1U << 1)  // 从设备是SPI设备
        #define V4L2_SUBDEV_FL_HAS_DEVNODE	(1U << 2)  // 从设备需要设备节点
        #define V4L2_SUBDEV_FL_HAS_EVENTS	(1U << 3)  // 从设备会产生事件
    
        struct v4l2_subdev {
        #if defined(CONFIG_MEDIA_CONTROLLER)  // 多媒体配置选项
            struct media_entity entity;
        #endif
            struct list_head list;  // 子设备串联链表
            struct module *owner;  // 属于那个模块,一般指向i2c_lient驱动模块
            bool owner_v4l2_dev;
            // 标志位,确定该设备属于那种设备,由V4L2_SUBDEV_FL_IS_XX宏确定
            u32 flags;
            // 指向主设备的v4l2_device结构体
            struct v4l2_device *v4l2_dev;
            // v4l2子设备的操作函数集合
            const struct v4l2_subdev_ops *ops;
            // 提供给v4l2框架的操作函数,只有v4l2框架会调用,驱动不使用
            const struct v4l2_subdev_internal_ops *internal_ops;
            // 从设备的控制接口
            struct v4l2_ctrl_handler *ctrl_handler;
            // 从设备的名称,必须独一无二
            char name[V4L2_SUBDEV_NAME_SIZE];
            // 从设备组的ID,由驱动定义,相似的从设备可以编为一组,
            u32 grp_id;
            // 从设备私有数据指针,一般指向i2c_client的设备结构体dev
            void *dev_priv;
            // 主设备私有数据指针,一般指向v4l2_device嵌入的结构体
            void *host_priv;
            // 指向video设备结构体
            struct video_device *devnode;
            // 指向物理设备
            struct device *dev;
            // 将所有从设备连接到全局subdev_list链表或notifier->done链表
            struct list_head async_list;
            // 指向struct v4l2_async_subdev,用于异步事件
            struct v4l2_async_subdev *asd;
            // 指向管理的notifier,用于主设备和从设备的异步关联
            struct v4l2_async_notifier *notifier;
            /* common part of subdevice platform data */
            struct v4l2_subdev_platform_data *pdata;
        };
        // 提供给v4l2框架的操作函数,只有v4l2框架会调用,驱动不使用
        struct v4l2_subdev_internal_ops {
            // v4l2_subdev注册时回调此函数,使v4l2_dev指向主设备的v4l2_device结构体
            int (*registered)(struct v4l2_subdev *sd);
            // v4l2_subdev卸载时回调此函数
            void (*unregistered)(struct v4l2_subdev *sd);
            // 应用调用open打开从设备节点时调用此函数
            int (*open)(struct v4l2_subdev *sd, struct v4l2_subdev_fh *fh);
            // 应用调用close时调用此函数
            int (*close)(struct v4l2_subdev *sd, struct v4l2_subdev_fh *fh);
        };
    

    使用v4l2_subdev_init初始化v4l2_subdev结构体。然后必须用一个唯一的名字初始化subdev->name,同时初始化模块的owner域。若从设备是I2C设备,则可使用v4l2_i2c_subdev_init函数进行初始化,该函数内部会调用v4l2_subdev_init,同时设置flagsownerdevname等成员。

        [include/media/v4l2-subdev.h]
        // 初始化v4l2_subdev结构体
        // ops-v4l2子设备的操作函数集合指针,保存到v4l2_subdev结构体的ops成员中
        void v4l2_subdev_init(struct v4l2_subdev *sd,
    		    const struct v4l2_subdev_ops *ops);
    
        [include/media/v4l2-common.h]
        // 初始化V4L2从设备为I2C设备的v4l2_subdev结构体
        // sd-v4l2_subdev结构体指针
        // client-i2c_client结构体指针
        // ops-v4l2子设备的操作函数集合指针,保存到v4l2_subdev结构体的ops成员中
        void v4l2_i2c_subdev_init(struct v4l2_subdev *sd, 
            struct i2c_client *client,
    		const struct v4l2_subdev_ops *ops);
    

    若需同媒体框架整合,必须调用media_entity_init初始化v4l2_subdev结构体中的media_entity结构体(entity域)。pads数组必须预先初始化。无须手动设置media_entitytypename域,但如有必要,revision域必须初始化。当(任何)从设备节点被打开/关闭,对entity的引用将被自动获取/释放。在从设备被注销之后,使用media_entity_cleanup清理media_entity结构体。如果从设备驱动趋向于处理视频并整合进了媒体框架,必须使用v4l2_subdev_pad_ops替代v4l2_subdev_video_ops实现格式相关的功能。这种情况下,子设备驱动应该设置link_validate域,以提供它自身的链接验证函数。链接验证函数应对管道(两端链接的都是V4L2从设备)中的每个链接调用。驱动还要负责验证子设备和视频节点间格式配置的正确性。如果link_validate操作没有设置,默认的v4l2_subdev_link_validate_default函数将会被调用。这个函数保证宽、高和媒体总线像素格式在链接的收发两端都一致。子设备驱动除了它们自己的检测外,也可以自由使用这个函数以执行上面提到的检查。

        [include/media/media-entity.h]
        // 初始化v4l2_subdev结构体中的media_entity结构体
        // entity-要初始化的media_entity结构体指针
        // num_pads-源pad的数量,与驱动子设备结构相关
        // pads-media_pad结构体数组,通常pad被嵌入到驱动自定义的结构体里面,
        //      数组地址被传递给该参数,pad需提前初始化
        // extra_links-函数会根据num_pads分配media_link数目,该参数则指明除了预分
                       配的数量之外还需要多少额外的media_link数目
        // 返回值 0-成功,小于0-失败
        int media_entity_init(struct media_entity *entity, u16 num_pads,
                struct media_pad *pads, u16 extra_links)
    
        // 清理media_entity结构体
        // entity-media_entity结构体指针
        void media_entity_cleanup(struct media_entity *entity)
    
        // v4l2-subdev pad层级的操作函数
        [include/media/v4l2-subdev.h]
    	struct v4l2_subdev_pad_ops {
    		// ioctl VIDIOC_SUBDEV_ENUM_MBUS_CODE命令处理函数
    		int (*enum_mbus_code)(struct v4l2_subdev *sd,
    				      struct v4l2_subdev_pad_config *cfg,
    				      struct v4l2_subdev_mbus_code_enum *code);
    		// ioctl VIDIOC_SUBDEV_ENUM_FRAME_SIZE命令处理函数
    		int (*enum_frame_size)(struct v4l2_subdev *sd,
    				       struct v4l2_subdev_pad_config *cfg,
    				       struct v4l2_subdev_frame_size_enum *fse);
    		// ioctl VIDIOC_SUBDEV_ENUM_FRAME_INTERVAL命令处理函数
    		int (*enum_frame_interval)(struct v4l2_subdev *sd,
    					   struct v4l2_subdev_pad_config *cfg,
    					   struct v4l2_subdev_frame_interval_enum *fie);
    		// ioctl VIDIOC_SUBDEV_G_FMT命令处理函数
    		int (*get_fmt)(struct v4l2_subdev *sd,
    			       struct v4l2_subdev_pad_config *cfg,
    			       struct v4l2_subdev_format *format);
    		// ioctl VIDIOC_SUBDEV_S_FMT命令处理函数
    		int (*set_fmt)(struct v4l2_subdev *sd,
    			       struct v4l2_subdev_pad_config *cfg,
    			       struct v4l2_subdev_format *format);
    		// ioctl VIDIOC_SUBDEV_G_SELECTION命令处理函数
    		int (*get_selection)(struct v4l2_subdev *sd,
    				     struct v4l2_subdev_pad_config *cfg,
    				     struct v4l2_subdev_selection *sel);
    		// ioctl VIDIOC_SUBDEV_S_SELECTION命令处理函数
    		int (*set_selection)(struct v4l2_subdev *sd,
    				     struct v4l2_subdev_pad_config *cfg,
    				     struct v4l2_subdev_selection *sel);
    		// ioctl VIDIOC_SUBDEV_G_EDID命令处理函数
    		int (*get_edid)(struct v4l2_subdev *sd, struct v4l2_edid *edid);
    		// ioctl VIDIOC_SUBDEV_S_EDID命令处理函数
    		int (*set_edid)(struct v4l2_subdev *sd, struct v4l2_edid *edid);
    		// ioctl VIDIOC_SUBDEV_DV_TIMINGS_CAP命令处理函数
    		int (*dv_timings_cap)(struct v4l2_subdev *sd, struct v4l2_dv_timings_cap *cap);
    		// ioctl VIDIOC_SUBDEV_ENUM_DV_TIMINGS命令处理函数
    		int (*enum_dv_timings)(struct v4l2_subdev *sd, struct v4l2_enum_dv_timings *timings);
    	#ifdef CONFIG_MEDIA_CONTROLLER
    		// 用于多媒体控制器检查属于管道的链接是否可以用于流
    		int (*link_validate)(struct v4l2_subdev *sd, struct media_link *link,
    				     struct v4l2_subdev_format *source_fmt,
    				     struct v4l2_subdev_format *sink_fmt);
    	#endif /* CONFIG_MEDIA_CONTROLLER */
    		// 获取当前低级媒体总线帧参数
    		int (*get_frame_desc)(struct v4l2_subdev *sd, unsigned int pad,
    				      struct v4l2_mbus_frame_desc *fd);	      
    		// 设置低级媒体总线帧参数
    		int (*set_frame_desc)(struct v4l2_subdev *sd, unsigned int pad,
    				      struct v4l2_mbus_frame_desc *fd);
    	};
    

    从设备必须向V4L2子系统注册v4l2_subdev结构体,使用v4l2_device_register_subdev注册,使用v4l2_device_unregister_subdev注销。

        [include/media/v4l2-device.h]
        // 向V4L2子系统注册v4l2_subdev结构体
        // v4l2_dev-主设备v4l2_device结构体指针
        // sd-从设备v4l2_subdev结构体指针
        // 返回值 0-成功,小于0-失败
        int v4l2_device_register_subdev(struct v4l2_device *v4l2_dev,
                        struct v4l2_subdev *sd)
    
        // 从V4L2子系统注销v4l2_subdev结构体
        // sd-从设备v4l2_subdev结构体指针    
        void v4l2_device_unregister_subdev(struct v4l2_subdev *sd);
    

    V4L2从设备驱动都必须有一个v4l2_subdev结构体。这个结构体可以单独代表一个简单的从设备,也可以嵌入到一个更大的结构体中,与更多设备状态信息保存在一起。通常有一个下级设备结构体(比如:i2c_client)包含了内核创建的设备数据。建议使用v4l2_set_subdevdata()将这个结构体的指针保存在v4l2_subdev的私有数据域(dev_priv)中。这使得通过v4l2_subdev找到实际的低层总线特定设备数据变得容易。同时也需要一个从低层结构体获取v4l2_subdev指针的方法。对于常用的i2c_client结构体,i2c_set_clientdata函数可用于保存一个v4l2_subdev指针,i2c_get_clientdata可以获取一个v4l2_subdev指针;对于其他总线可能需要使用其他相关函数。

        [include/media/v4l2-subdev.h]
        // 将i2c_client的指针保存到v4l2_subdev结构体的dev_priv成员中
        static inline void v4l2_set_subdevdata(struct v4l2_subdev *sd, void *p)
        {
            sd->dev_priv = p;
        }
    
        [include/linux/i2c.h]
        // 可以将v4l2_subdev结构体指针保存到i2c_client中dev成员的driver_data中
        static inline void i2c_set_clientdata(struct i2c_client *dev, void *data)
        {
            dev_set_drvdata(&dev->dev, data);
        }
        // 获取i2c_client结构体中dev成员的driver_data,一般指向v4l2_subdev
        static inline void *i2c_get_clientdata(const struct i2c_client *dev)
        {
            return dev_get_drvdata(&dev->dev);
        }
    

    主设备驱动中也应保存每个子设备的私有数据,比如一个指向特定主设备的各设备私有数据的指针。为此v4l2_subdev结构体提供主设备私有数据域(host_priv),并可通过v4l2_get_subdev_hostdatav4l2_set_subdev_hostdata访问。

        [include/media/v4l2-subdev.h]
        static inline void *v4l2_get_subdev_hostdata(const struct v4l2_subdev *sd)
        {
            return sd->host_priv;
        }
        static inline void v4l2_set_subdev_hostdata(struct v4l2_subdev *sd, void *p)
        {
            sd->host_priv = p;
        }
    

    每个v4l2_subdev都包含子设备驱动需要实现的函数指针(如果对此设备不适用,可为NULL),具体在v4l2_subdev_ops结构体当中。由于子设备可完成许多不同的工作,而在一个庞大的函数指针结构体中通常仅有少数有用的函数实现其功能肯定不合适。所以,函数指针根据其实现的功能被分类,每一类都有自己的函数指针结构体,如v4l2_subdev_core_opsv4l2_subdev_audio_opsv4l2_subdev_video_ops等等。v4l2_subdev_video_ops在视频设备中(第二章中)详细介绍。顶层函数指针结构体包含了指向各类函数指针结构体的指针,如果子设备驱动不支持该类函数中的任何一个功能,则指向该类结构体的指针为NULL。

        [include/media/v4l2-subdev.h]
        /* v4l2从设备的操作函数集合,从设备根据自身设备类型选择实现,
           其中core函数集通常可用于所有子设备,其他类别的实现依赖于
           子设备。如视频设备可能不支持音频操作函数,反之亦然。这样的
           设置在限制了函数指针数量的同时,还使增加新的操作函数和分类
           变得较为容易。 */
        struct v4l2_subdev_ops {
            // 从设备的通用操作函数集合,进行初始化、reset、控制等操作
            const struct v4l2_subdev_core_ops	*core;
            const struct v4l2_subdev_tuner_ops	*tuner;
            const struct v4l2_subdev_audio_ops	*audio;  // 音频设备
            // 视频设备,后面详细描述
            const struct v4l2_subdev_video_ops	*video;  
            const struct v4l2_subdev_vbi_ops	*vbi;    // VBI设备
            const struct v4l2_subdev_ir_ops		*ir;
            const struct v4l2_subdev_sensor_ops	*sensor;
            const struct v4l2_subdev_pad_ops	*pad;
        };
        // 适用于所有v4l2从设备的操作函数集合
        struct v4l2_subdev_core_ops {
            // IO引脚复用配置
            int (*s_io_pin_config)(struct v4l2_subdev *sd, size_t n,
                            struct v4l2_subdev_io_pin_config *pincfg);
            // 初始化从设备的某些寄存器,使其恢复默认
            int (*init)(struct v4l2_subdev *sd, u32 val);
            // 加载固件
            int (*load_fw)(struct v4l2_subdev *sd);
            // 复位
            int (*reset)(struct v4l2_subdev *sd, u32 val);
            // 设置GPIO引脚输出值
            int (*s_gpio)(struct v4l2_subdev *sd, u32 val);
            // 设置从设备的电源状态,0-省电模式,1-正常操作模式
            int (*s_power)(struct v4l2_subdev *sd, int on);
            // 中断函数,被主设备的中断函数调用
            int (*interrupt_service_routine)(struct v4l2_subdev *sd,
                                u32 status, bool *handled);
            ......
        };
    

    使用v4l2_device_register_subdev注册从设备后,就可以调用v4l2_subdev_ops中的方法了。可以通过v4l2_subdev直接调用,也可以使用内核提供的宏定义v4l2_subdev_call间接调用某一个方法。若要调用多个从设备的同一个方法,则可使用v4l2_device_call_all宏定义。

        // 直接调用
        err = sd->ops->video->g_std(sd, &norm);
    
        // 使用宏定义调用,这个宏将会做NULL指针检查,如果su为NULL,则返回-ENODEV;
        // 如果sd->ops->video或sd->ops->video->g_std为NULL,则返回-ENOIOCTLCMD;
        // 否则将返回sd->ops->video->g_std的调用的实际结果
    	err = v4l2_subdev_call(sd, video, g_std, &norm);
    
        [include/media/v4l2-subdev.h]
        #define v4l2_subdev_call(sd, o, f, args...)				\
    	(!(sd) ? -ENODEV : (((sd)->ops->o && (sd)->ops->o->f) ?	\
    		(sd)->ops->o->f((sd) , ##args) : -ENOIOCTLCMD))
    
    
        v4l2_device_call_all(v4l2_dev, 0, video, g_std, &norm);
        [include/media/v4l2-device.h]
        #define v4l2_device_call_all(v4l2_dev, grpid, o, f, args...)		\
    	do {								\
    		struct v4l2_subdev *__sd;				\
    		__v4l2_device_call_subdevs_p(v4l2_dev, __sd,		\
    			!(grpid) || __sd->grp_id == (grpid), o, f ,	\
    			##args);					\
    	} while (0)
    

    如果子设备需要通知它的v4l2_device主设备一个事件,可以调用v4l2_subdev_notify(sd,notification, arg)。这个宏检查是否有一个notify回调被注册,如果没有,返回-ENODEV。否则返回 notify调用结果。notify回调函数由主设备提供。

        [include/media/v4l2-device.h]
        // 从设备通知主设备,最终回调到v4l2_device的notify函数
        static inline void v4l2_subdev_notify(struct v4l2_subdev *sd,
                            unsigned int notification, void *arg)
        {
            if (sd && sd->v4l2_dev && sd->v4l2_dev->notify)
                sd->v4l2_dev->notify(sd, notification, arg);
        }
    

    使用v4l2_subdev的好处在于它是一个通用结构体,且不包含任何底层硬件信息。所有驱动可以包含多个I2C总线的从设备,但也有从设备是通过GPIO控制。这个区别仅在配置设备时有关系,一旦子设备注册完成,对于v4l2子系统来说就完全透明了。

    2.3.V4L2主设备和从设备匹配过程分析

    V4L2主设备和从设备采用异步的匹配方法。首先介绍一下异步匹配用到的方法。主设备使用v4l2_async_notifier_register函数进行异步匹配,匹配到从设备,则调用v4l2_device_register_subdev函数注册从设备,使用v4l2_async_notifier_unregister函数异步取消匹配。从设备使用v4l2_async_register_subdev函数异步匹配主设备,若匹配到主设备,则调用v4l2_device_register_subdev函数注册从设备,使用v4l2_async_unregister_subdev函数异步取消匹配。
    匹配的方法由v4l2_async_subdev结构体决定,主设备可以有多个v4l2_async_subdev结构体,也说明主设备有多种匹配从设备的方法。match_type表示匹配方式,由枚举v4l2_async_match_type定义,具体有使用设备名称匹配-V4L2_ASYNC_MATCH_DEVNAME、使用I2C adapter ID and address进行匹配-V4L2_ASYNC_MATCH_I2C等。联合体match中包含了具体的匹配信息,根据匹配方式进行设置。v4l2_async_notifier管理整个匹配过程,未匹配的v4l2_async_subdev结构体被挂到waiting链表,匹配完成的挂到done链表同时调用bound函数进行绑定。

        [include/media/v4l2-async.h]
        // 主设备和从设备的匹配方式 
        enum v4l2_async_match_type {
            // 传统的匹配方式,使用v4l2_async_subdev的match方法进行匹配
            V4L2_ASYNC_MATCH_CUSTOM,  
            // 使用设备名称进行匹配
            V4L2_ASYNC_MATCH_DEVNAME,
            // 使用I2C adapter ID and address进行匹配
            V4L2_ASYNC_MATCH_I2C,
            // 使用firmware node 进行匹配
            V4L2_ASYNC_MATCH_OF,
        };
        struct v4l2_async_subdev {
            // 匹配方式
            enum v4l2_async_match_type match_type;
            union {
                struct {
                    // 设备树匹配方式
                    const struct device_node *node;
                } of;
                struct {
                    // 设备名称匹配方式
                    const char *name;
                } device_name;
                struct {
                    // 使用I2C adapter ID and address进行匹配
                    int adapter_id;
                    unsigned short address;
                } i2c;
                struct {
                    // 传统的匹配方式
                    bool (*match)(struct device *,
                            struct v4l2_async_subdev *);
                    void *priv;
                } custom;
            } match;
            // v4l2-async核心层使用,将此结构体挂入到notifier的waiting链表,驱动不可使用
            struct list_head list;
        };
    
        // 主设备注册一个notifier,用于异步的和从设备进行匹配
        int v4l2_async_notifier_register(struct v4l2_device *v4l2_dev,
                        struct v4l2_async_notifier *notifier);
        // 主设备注销notifier
        void v4l2_async_notifier_unregister(struct v4l2_async_notifier *notifier);
        
        // 从设备异步注册v4l2_subdev,用于异步的和主设备进行匹配
        int v4l2_async_register_subdev(struct v4l2_subdev *sd);
        // 从设备注销
        void v4l2_async_unregister_subdev(struct v4l2_subdev *sd);
    
        struct v4l2_async_notifier {
            unsigned int num_subdevs;  // subdevices的数量
            // 指针数组,指向subdevice descriptors
            struct v4l2_async_subdev **subdevs;  
            // 指向struct v4l2_device
            struct v4l2_device *v4l2_dev;
            // v4l2_async_subdev的链表,等待匹配drivers
            struct list_head waiting;
            // 已经probed的v4l2_subdev链表
            struct list_head done;
            // 挂在全局的notifiers链表上
            struct list_head list;
            // 驱动匹配到从设备后调用此函数
            int (*bound)(struct v4l2_async_notifier *notifier,
                    struct v4l2_subdev *subdev,
                    struct v4l2_async_subdev *asd);
            // 所有从设备被probed成功,调用此函数
            int (*complete)(struct v4l2_async_notifier *notifier);
            // 从设备注销时调用此函数
            void (*unbind)(struct v4l2_async_notifier *notifier,
                    struct v4l2_subdev *subdev,
                    struct v4l2_async_subdev *asd);
        };
    

    以imx6ull为例分析主设备和从设备的匹配过程。v4L2主设备和从设备的匹配过程可通过分析v4l2_async_notifier_register函数得到。可总结如下:
    (1)首先初始化需要匹配的v4l2_async_notifier结构体,主要设备匹配方式、bound函数和指向v4l2_async_subdev结构体的指针。
    (2)设置v4l2_async_notifierv4l2_dev指针指向主设备的v4l2_device结构体。
    (3)遍历subdevs指向的v4l2_async_subdev结构体,全部挂入waiting链表。
    (4)将v4l2_async_notifier结构体挂入到全局的notifier_list链表。
    (5)遍历subdev_list链表(所有的从设备都挂到subdev_list链表中),和waiting链表中的每一个v4l2_async_subdev结构体进行匹配。
    (6)若匹配成功,则将v4l2_async_subdevwaiting链表中删除,设置从设备V4l2_subdev结构体的notifierasd成员,使其指向mx6s_csi_dev结构体中的v4l2_async_notifierv4l2_async_subdev结构体。
    (7)调用bound函数,也就是使主设备mx6s_csi_dev结构体中的sd成员指向了从设备的v4l2_subdev结构体,从而完成了主设备和从设备进行绑定。
    (8)将匹配成功的从设备V4l2_subdev结构体从全局的subdev_list链表中移除,同时添加到v4l2_async_notifierdone链表中。
    (9)调用v4l2_device_register_subdev注册从设备。

    V4L2设备匹配过程

        v4l2_async_notifier_register
            // num_subdevs为0或num_subdevs大于128,返回错误
            if (!notifier->num_subdevs || notifier->num_subdevs > V4L2_MAX_SUBDEVS)
    		    return -EINVAL;
            // notifier->v4l2_dev指向v4l2_device结构体指针v4l2_dev
            notifier->v4l2_dev = v4l2_dev;
            
            for (i = 0; i < notifier->num_subdevs; i++) {
                asd = notifier->subdevs[i];
                switch (asd->match_type) {
                case V4L2_ASYNC_MATCH_CUSTOM:
                case V4L2_ASYNC_MATCH_DEVNAME:
                case V4L2_ASYNC_MATCH_I2C:
                case V4L2_ASYNC_MATCH_OF:
                    break;
                    ......
                }
                // 将指针数组subdevs指向的v4l2_async_subdev结构体挂入
                // notifier的waiting链表
                list_add_tail(&asd->list, &notifier->waiting);
            }
            mutex_lock(&list_lock);  // 加锁,保护全局链表
            // 将v4l2_async_notifier挂到全局链表notifier_list
            list_add(&notifier->list, &notifier_list);   
    
            // 遍历subdev_list链表,所有从设备的v4l2_subdev结构体都挂到subdev_list链表
            list_for_each_entry_safe(sd, tmp, &subdev_list, async_list) {
                int ret;
                // 判断子设备的v4l2_subdev是否和主设备的notifier匹配,
                // 匹配则返回v4l2_async_subdev结构体
                asd = v4l2_async_belongs(notifier, sd);
                if (!asd)
                    continue;
                // 匹配成功,调用bound函数,注册从设备的v4l2_subdev结构体
                ret = v4l2_async_test_notify(notifier, sd, asd);
                if (ret < 0) {
                    mutex_unlock(&list_lock);
                    return ret;
                }
            }
            mutex_unlock(&list_lock);
            return 0;
    
        v4l2_async_belongs
            // 定义匹配的函数指针
            bool (*match)(struct device *, struct v4l2_async_subdev *);
            // 遍历waiting链表,取出v4l2_async_subdev结构体
            list_for_each_entry(asd, &notifier->waiting, list) {
                // 确认匹配方式
                switch (asd->match_type) {
                case V4L2_ASYNC_MATCH_CUSTOM:
                    match = asd->match.custom.match;
                    if (!match)
                        return asd;
                    break;
                case V4L2_ASYNC_MATCH_DEVNAME:
                    match = match_devname;  // 设备名称匹配方法
                    break;
                case V4L2_ASYNC_MATCH_I2C:  // I2C匹配方法
                    match = match_i2c;
                    break;
                case V4L2_ASYNC_MATCH_OF:  // 设备树的匹配方法
                    match = match_of;
                    break;
                    ......
                }
    
                // 根据匹配方式,调用相应的匹配方法
                if (match(sd->dev, asd))
                    return asd;
            }
            return NULL;
    
        // 根据设备名称匹配,
        static bool match_devname(struct device *dev, struct v4l2_async_subdev *asd)
        {
            return !strcmp(asd->match.device_name.name, dev_name(dev));
        }
        // 对比i2c_client的adapter_id和address
        static bool match_i2c(struct device *dev, struct v4l2_async_subdev *asd)
        {
        #if IS_ENABLED(CONFIG_I2C)
            struct i2c_client *client = i2c_verify_client(dev);
            return client &&
                asd->match.i2c.adapter_id == client->adapter->nr &&
                asd->match.i2c.address == client->addr;
        #else
            return false;
        #endif
        }
        // 对比设备树节点
        static bool match_of(struct device *dev, struct v4l2_async_subdev *asd)
        {
            return dev->of_node == asd->match.of.node;
        }
    
        v4l2_async_test_notify
            // 从waiting链表中移除
            list_del(&asd->list);
            sd->asd = asd;
            sd->notifier = notifier;
            // bound非空,则调用bound函数,bound函数的主要作用是设置主设备的v4l2_subdev指针,
            // 使其指向匹配的从设备的v4l2_subdev结构体,从而完成主设备到从设备的绑定
            if (notifier->bound) {
                ret = notifier->bound(notifier, sd, asd);
                ......
            }
            // 将subdevice从async_list链表中移除后挂到done链表中
            list_move(&sd->async_list, &notifier->done);
            // 注册从设备v4l2_subdev结构体
            v4l2_device_register_subdev(notifier->v4l2_dev, sd)
            // 如果waiting链表无等待的v4l2_async_subdev,且complete非空
            if (list_empty(&notifier->waiting) && notifier->complete)
                return notifier->complete(notifier);  // 调用完成函数
    

    参考资料

    1. https://blog.csdn.net/kickxxx/article/details/8484498
    2. https://blog.csdn.net/weixin_44139476/article/details/107021395
    3. 内核文档-v4l2-framework_zh_CN.txt
    4. Linux内核4.1版本源码
    5. Android驱动开发权威指南
    6. Android驱动开发与移植实战详解
    7. https://linuxtv.org/downloads/v4l-dvb-apis/driver-api/v4l2-subdev.html
    展开全文
  • Linux v4l2框架分析

    2021-10-29 00:17:06
    背景说明:Kernel版本:4.14ARM64处理器,Contex-A53,双核使用工具:Source Insight 3.5, Visio1. 概述V4L2(Video for Linu...

    背景

    说明:

    1. Kernel版本:4.14

    2. ARM64处理器,Contex-A53,双核

    3. 使用工具:Source Insight 3.5, Visio

    1. 概述

    • V4L2(Video for Linux 2):Linux内核中关于视频设备驱动的框架,对上向应用层提供统一的接口,对下支持各类复杂硬件的灵活扩展;

    • V4L2框架,主要包括v4l2-coremeida frameworkvideobuf2等模块,这也是本文将要展开的内容,仅提纲挈领;

    开始吧。

    2. v4l2-core

    2.1 应用视角

    先从应用的角度来看如何使用v4l2吧:

    e92e369a2acbd0950c20011a11828328.png


    假如要进行视频数据采集,大体的步骤如上图左侧所示:

    1. 打开设备文件/dev/videoX

    2. 根据打开的设备,查询设备能力集;

    3. 设置视频数据的格式、参数等;

    4. 分配buffer,这个buffer可以是用户态分配的,也可以是从内核中获取的;

    5. 开始视频流采集工作;

    6. 将buffer enqueue到v4l2框架,底层负责将视频数据填充后,应用层再将buffer dequeue以便获取数据,然后再将buffer enqueue,如此循环往复;

    上图右侧是v4l2-core的大体框架,右侧是对硬件的抽象,要想理解好它,可以先看一下较常见的硬件拓扑结构:

    408f029fb9491fd0f0a0ec4ae039e253.png


    • 通常一个camera的模组如图所示,通常包括Lens、Sensor、CSI接口等,其中CSI接口用于视频数据的传输;

    • SoC的Mipi接口对接Camera,并通过I2C/SPI控制camera模组;

    • Camera模组中也可以包含ISP模块,用于对图像进行处理,有的SoC中也集成了ISP的IP,接收camera的raw数据后,进行图像处理;

    2.2 数据结构

    如果以上图的硬件为例,对摄像头的硬件该怎么来抽象呢?没错,就是以v4l2_devicev4l2_subdev来进行抽象,以v4l2_device来代表整个输入设备,以v4l2_subdev来代表子模块,比如CSISensor等;

    0aafd312babc88d1b6576c45c32d1194.png


    • v4l2_device:对视频设备的整体进行抽象,可以看成是一个纽带,将各个子设备联系在一起,通常它会嵌入在其他结构体中以提供v4l2框架的功能,比如strcut isp_device

    • v4l2_subdev:对子设备进行抽象,该结构体中包含的struct v4l2_subdev_ops是一个完备的操作函数集,用于对接各种不同的子设备,比如video、audio、sensor等,同时还有一个核心的函数集struct v4l2_subdev_core_ops,提供更通用的功能。子设备驱动根据设备特点实现该函数集中的某些函数即可;

    • video_device:用于向系统注册字符设备节点,以便用户空间可以进行交互,包括各类设置以及数据buffer的获取等,在该结构体中也能看到struct v4l2_ioctl_opsstruct vb2_queue结构体字段,这些与上文中的应用层代码编写息息相关;

    • 如果子设备不需要与应用层交互,struct v4l2_subdev中内嵌的video_device也可以不向系统注册字符设备;

    • video_device结构体,可以内嵌在其他结构体中,以便提供用户层交互的功能,比如struct isp_video

    • 针对图中回调函数集,v4l2-core提供了一些实现,所以driver在实现时,非特殊情况下可以不用重复造轮子;

    2.3 流程分析

    来进一步看一下内部的注册,及调用流程吧:

    4024c479437a77b9a7ff68b189b32c75.png


    • 在驱动实现中,驱动结构体中内嵌struct video_device,同时实现struct v4l2_file_operations结构体中的函数,最终通过video_register_device向提供注册;

    • v4l2_register_device函数通过cdev_add向系统注册字符设备,并指定了file_operations,用户空间调用open/read/write/ioctl等接口,便可回调到驱动实现中;

    • v4l2_register_device函数中,通过device_register向系统注册设备,会在/sys文件系统下创建节点;

    完成注册后,用户空间便可通过文件描述符来进行访问,从应用层看,大部分都是通过ioctl接口来完成,流程如下:

    e6c35f059441bce425b8f87470c82d79.png


    • 用户层的ioctl回调到__video_do_ioctl中,该函数会对系统提供的struct v4l2_ioctl_info v4l2_ioctls[]表进行查询,找到对应的项后进行调用;

    • 驱动做的工作就是填空题,实现对应的回调,在合适的时候被调用;

    下一个小节,让我们看看更复杂一点的情况。

    3. media framework

    3.1 问题引入

    为了更好的描述,本节以omap3isp为例,先看一下它的硬件构成:

    cd34f89b605614f7f4b0477c15f323b9.png


    • CSI:camera接口,接收图像数据,RGB/YUV/JPEG等;

    • CCDC:视频处理前端,CCDC为图像传感器和数字视频源提供接口,并处理图像数据;

    • Preview/Resizer:视频处理后端,Preview提供预览功能,可针对不同类型的传感器进行定制,Resizer提供将输入图像数据按所需的显示或视频编码分辨率调整大小的方法;

    • H3A/HIST:静态统计模块,H3A支持AF、AWB、AE的回路控制,HIST根据输入数据,提供各种3A算法所需的统计数据;

    上述硬件模块,可以对应到驱动结构体struct isp_device中的各个字段。

    omap3isp的硬件模块,支持多种数据流通路,它并不是唯一的,以RGB为例,如下图:

    0cd424df7df5baeaa33a4466bd2501a4.png


    • Raw RGB数据进入ISP模块后,可以在运行过程中,根据实际的需求进行通路设置;

    • 所以,重点是:它需要动态设置路径!

    那么,软件该如何满足这种需求呢?

    3.2 框架

    没错,pipeline框架的引入可以解决这个问题。说来很巧,我曾经也实现过一个类似的框架,在阅读media framework时有一种似曾相识的感觉,核心的思想大体一致。

    590d9f19fca7b229bb5a65dcbc89e6af.png


    • 模块之间相互独立,通过struct media_entity来进行抽象,通常会将struct media_entity嵌入到其他结构中,以支持media framework功能;

    • entity模块包含struct media_pad,pad可以认为是端口,与其他模块进行联系的媒介,针对特定模块来说它是确定的;

    • pad通过struct media_link来建立连接,指定source和sink,即可将通路建立起来;

    • 各个模块之间最终建立一条数据流,便是一条pipeline了,同一条pipeline中的模块,可以根据前一个模块查找到下一个模块,因此也可以很方便进行遍历,并做进一步的设置操作;

    因此,只需要将struct media_entity嵌入到特定子模块中,最终便可以将子模块串联起来,构成数据流。所以,omap3isp的驱动中,数据流就如下图所示:

    f98b0103f2223703715021034c272f70.png


    • video devnode代表video device,也就是前文中提到的导出到用户空间的节点,用于与用户进行控制及数据交互;

    • 每个模块分别有source pad和sink pad,从连接图就可以看出,数据通路灵活多变;

    • 至于数据通路选择问题,可以在驱动初始化的时候进行链接创建,比如isp_create_links

    还是看一下数据结构吧:

    acd9f32bb1bfc36a08f289726e3b4a0d.png


    • media_device:与v4l2_device类似,也是负责将各个子模块集中进行管理,同时在注册的时候,会向系统注册设备节点,方便用户层进行操作;

    • media_entitymedia_padmedia_link等结构体的功能在上文中描述过,注意,这几个结构体会添加到media_device的链表中,同时它们结构体的开始字段都需是struct media_gobj,该结构中的mdev将会指向它所属的media_device。这种设计方便结构之间的查找;

    • media_entity中包含多个media_pad,同时media_pad又会指向它所属的media_entity

    • media_graphmedia_pipelinemedia_entity的集合,直观来理解,就是由一些模块构成的一条数据通路,由一个统一的数据结构来组织管理;

    罗列一下常见的几个接口吧,细节不表了:

    /* 初始化entity的pads */
    int media_entity_pads_init(struct media_entity *entity, u16 num_pads,
            struct media_pad *pads);
    
    /* 在两个entity之间创建link */
    int media_create_pad_links(const struct media_device *mdev,
          const u32 source_function,
          struct media_entity *source,
          const u16 source_pad,
          const u32 sink_function,
          struct media_entity *sink,
          const u16 sink_pad,
          u32 flags,
          const bool allow_both_undefined);
    
    /* 开始graph的遍历,从指定的entity开始 */
    void media_graph_walk_start(struct media_graph *graph,
           struct media_entity *entity);
    
    /* 启动pipeline */
    __must_check int media_pipeline_start(struct media_entity *entity,
              struct media_pipeline *pipe);

    media frameworkv4l2_devicev4l2_subdev结合起来,就可以将各个子设备构建pipeline,完美!

    4. videobuf2

    4.1 框架分析

    • 框架可以分成两个部分看:控制流+数据流,上文已经大概描述了控制流,数据流的部分就是video buffer了。

    • V4L2的buffer管理是通过videobuf2来完成的,它充当用户空间和驱动之间的中间层,并提供low-level,模块化的内存管理功能;

    d9556da37d223932356c3112072940e6.png


    • 上图大体包含了videobuf2的框架;

    • vb2_queue:核心的数据结构,用于描述buffer的队列,其中struct vb2_buffer *bufs[]是存放buffer节点的数组,该数组中的成员代表了vb2 buffer,并将在queued_listdone_list两个队列中进行流转;

    • struct vb2_buf_ops:buffer的操作函数集,由驱动来实现,并由框架通过call_bufop宏来对特定的函数进行调用;

    • struct vb2_mem_ops:内存buffer分配函数接口,buffer类型分为三种:1)虚拟地址和物理地址都分散,可以通过dma-sg来完成;2)物理地址分散,虚拟地址连续,可以通过vmalloc分配;3)物理地址连续,可以通过dma-contig来完成;三种类型也vb2框架中都有实现,框架可以通过call_memop来进行调用;

    • struct vb2_ops:vb2队列操作函数集,由驱动来实现对应的接口,并在框架中通过call_vb_qop宏被调用;

    4.2 流程分析

    本节以omap3isp为例进行简要分析,感觉直接看图就可以了:

    1. buffer申请

    ceb37f7d41fd423430eb1d7f1d7fdb2b.png


    1. buffer enqueue

    c88ea745284763fcc601b0fab76e2dd2.png


    1. buffer dequeue

    a4ba0dba556fe8f25af53735b2a73beb.png


    1. stream on

    863807a6a6d70899516b5172f5b1904b.png


    行文至此,主体讲完了,相信看完本文应该有个大概的轮廓了,还有一些细节未进一步描述,就此打住。

    参考

    https://lwn.net/Articles/416649/ 

    《OMAP35x Technical Reference Manual (Rev. Y).pdf》

    end


    推荐阅读:

    专辑|Linux文章汇总

    专辑|程序人生

    专辑|C语言

    我的知识小密圈

    关注公众号,后台回复「1024」获取学习资料网盘链接。

    欢迎点赞,关注,转发,在看,您的每一次鼓励,我都将铭记于心~

    c73fb91b5f45af5d6d321ed8d65b84a3.png

    嵌入式Linux

    微信扫描二维码,关注我的公众号

    展开全文
  • linux v4l2

    2018-03-06 11:09:30
    1.1 struct v4l2_device 1.2 struct nxp_v4l2 1.3 struct media_device 1.4 struct media_entity 2 平台特有结构体 2.1 3 v4l2注册流程 3.1 vb2_ion_create_context 3.2 media_device_register 3.3 create_nx...
  • 本资源利用v4l2打开摄像头,无需安装opencv库,直接在Linux下编译即可!可手动调节摄像头输出分辨率。
  • Linux V4l2驱动 – 框架概述
  • 使用V4L2进行视频采集,一般有下面的五个主要步骤: 1.打开设备(一般是/dev/video),进行初始化参数设置,包括分辨率,像素格式等; 2.申请图像帧缓冲,并进行内存映射mmap; 3.把帧缓冲进行入队操作,开始视频流...
  • 为了实现代码的重用,sensor驱动只需实现各种设备控制方法供上层调用并注册v4l2_subdev,而无需关心video_device和v4l2_dev。 struct v4l2_subdev结构体定义如下: struct v4l2_subdev { ...... struct list_head...
  • Linux v4l2 二 驱动和 usb 摄像头

    千次阅读 2019-04-04 23:54:20
    Android Camera 一 源码路径 Android Camera 二 JNI JAVA和C/CPP图像数据传输流程分析 Android Camera 三 CameraService 和 Client 链接...Linux v4l2 一 应用层 Linux v4l2 二 驱动和 usb 摄像头 源码目录 ...
  • 使用原始的RGB数据构造Opencv中的Mat对象。该资源使用Linux系统中的V4L2接口读取摄像头MJPEG图像数据,解码成RGB数据,再转换为Opencv中的Mat对象所使用的BGR格式
  • v4l2基本框架,以及将linux下摄像头采集数据YUYV转换为JPEG格式
  • linux v4l2 资料整理

    2021-06-08 15:00:54
    https://linuxtv.org/downloads/legacy/video4linux/API/V4L2_API/spec-single/v4l2.html 对应kernel新版本的v4l2-API,每个内核版本有对应的v4l2-api https://linuxtv.org/downloads/v4l-dv...
  • Video for Linux Two API Specification(V4l2 api接口用户说明书)
  • v4l2—嵌入式linux编程人员使用参考书,豆丁上弄的文档,英文版,v4l2结构及API相关介绍。
  • v4l2 - Where does v4l2_buffer->timestamp value starts counting? - Stack Overflow #include <time.h> #include <math.h> ////////////////////// //setup: long getEpochTimeShift(){ ...
  • Linux V4L2驱动架构解析与开发导引 众所周 知,linux中可以采用灵活的多层次的驱动架构来对接口进行统一与抽象,最低层次的驱动总是直接面向硬件的,而最高层次的驱动在linux中被划分为 “面向字符设备、面向块设备...
  • Linux V4L2框架源码分析

    2019-03-13 19:23:42
    V4L2 video_device 分析 目录 V4L2 video_device 分析 目录 video_device 结构体 v4l2_file_operations video_device 结构体 struct video_device 结构体是用来生成和...
  • linux V4L2编程拍照

    2019-05-21 15:48:12
    linux环境下基于V4L2实现的拍照程序,包括代码和说明文档,可编译通过
  • linuxv4l2视频采集

    2016-05-12 12:00:38
    linux下使用v4l2对音视频的采集,获取一帧数据
  • V4L2是video for Linux 2的缩写,是一套Linux内核视频设备的驱动框架,为上层的访问底层的视频设备提供了统一的接口。 V4L2支持多种类型设备:视频输入输出设备、VBI设备和radio设备等,分别会在/dev目录下产生...
  • ARM LinuxV4L2驱动摄像头拍照代码,分直接读取方式和MMAP方式
  • linux v4l2 应用代码

    2014-03-21 17:34:39
    关于linuxv4l2的开发 关于linuxv4l2的开发 关于linuxv4l2的开发
  • Linux v4l2 一 应用层

    2019-04-05 20:34:07
    Android Camera 一 源码路径 Android Camera 二 JNI JAVA和C/CPP图像数据传输流程分析 Android Camera 三 CameraService 和 Client 链接到 ...Linux v4l2 一 应用层 Linux v4l2 二 驱动和 usb 摄像头 v4l2 :...
  • linux V4L2编程

    2013-09-23 14:49:07
    linux系统下的 V4L2编程,大家共同学习,共同提高。
  • 从本章开始,我们开始分析V4L2子系统(Video for Linux two),主要用于音视频设备的框架。V4l2主要用于驱动视频输出设备(video outpt interface)、Video overlay interface、Video output overlay device、VBI ...
  • 嵌入式arm64位luvcview 工具,可以测试USB camera以及所有支持v4l2设备
  • LINUX下采集V4L2摄像头数据,并保存成文件,为确保读取摄像头数据不丢失,采样多线程,队列缓冲方式,实现,保存的文件可以直接作为H264编码的源,百分百OK

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 11,899
精华内容 4,759
关键字:

linuxv4l2

linux 订阅