linux V4L2子系统——v4l2架构(6)之videobuf2

linux V4L2子系统——v4l2架构(6)之videobuf2

备注:
  1. Kernel版本:5.4
  2. 使用工具:Source Insight 4.0
  3. 参考博客:
(1)Linux V4L2子系统-videobuf2架构分析(三)
(2)V4L2架构-videobuf2

概述

Video设备产生的数据较多,传统的缓冲机制已不能满足需求。为此,Linux内核抽象出了videobuf2机制,用于管理存放视频图像的帧缓冲。videobuf2抽象层像一座桥梁,将用户空间和V4L2 driver连接起来。videobuf2抽象层向用户空间提供了标准POSIX I/O系统调用,包括read、poll及mmap等,同时还提供了大量与流式I/O相关的V4L2 ioctl调用,包括缓冲区分配、缓冲区入队、缓冲区出队及流控制。虽然使用videobuf2会给驱动程序强加一些设计决策,但是使用它的收益是videobuf2可以减少驱动程序代码和保持V4L2子系统在用户空间API的一致性,显然使用videobuf2更为合理。

为什么要有videobuf2?

  1. 不完善的以及错误的内存管理
    a. 不能停止 streaming(在 streamoff 的时候,buffer 被释放,从而无法灵活地再次开启 stream);
    b. VIDIOC_REQBUFS(0) 不会释放内存,这个主要就是使用上的方便与否;
    c. 不能够使用 VIDIOC_REQBUFS 重复申请内存,道理同上;
    d. video 内存在 mmapqbuf 甚至 page fault 中分配;在 unmapstreamoff 或者驱动自定义的地方被释放,也就是说不够标准化、统一化;
    e. 每一个buffer都有一个等待队列,太过繁琐,会导致 buffer 轮转的时间较长;

  2. 扩展性不足,尤其是嵌入式多媒体设备
    a. 难以添加新的内存处理函数以及自定义的内存分配函数,比如现在 videobuf2 里面已有的三种-「dma-contigsg-dmavmalloc」;
    b. 对 cache 以及 IOMMU 的支持非常弱;
    c. 不够灵活。只有一个通用的函数来处理 cache、sg-list 创建等等事情,这些应该分为不同的抽象方法类进行差异化管理;

  3. 无用的成员,代码,变量

分类

不是所有的Video设备都使用同一种类型的videobuf2。实时上,Linux内核中有3中不同类型的videobuf2。

#path:drivers/media/common/videobuf2
├── vb2-trace.c
├── videobuf2-core.c
├── videobuf2-dma-contig.c
├── videobuf2-dma-sg.c
├── videobuf2-dvb.c
├── videobuf2-memops.c
├── videobuf2-v4l2.c
└── videobuf2-vmalloc.c
#path:drivers/media/v4l2-core
├── videobuf-core.c
├── videobuf-dma-contig.c
├── videobuf-dma-sg.c
└── videobuf-vmalloc.c

(1)缓冲区物理地址和虚拟地址不连续。
大多数用户空间缓冲区就属于这种情况,在可能的情况下,内核空间以这种方式分配缓冲区也是有意义的。然而在有些情况下,则不适用。对于不连续的缓冲区,需要硬件上支持scatter/gather DMA operations。使用该缓冲区,需要包含头文件 <include/media/videobuf-dma-sg.h>(适用于V4L)或 <include/media/videobuf2-dma-sg.h> (适用于V4L2)。

(2)缓冲区物理地址不连续但虚拟地址连续。
缓冲区分配使用vmalloc。这种缓冲区无法使用DMA进行传输。但在不使用DMA的情形下,虚拟地址连续的缓冲器很便利。使用该缓冲区,需要包含头文件 <include/media/videobuf-vmalloc.h>(适用于V4L)或 <include/media/videobuf2-vmalloc.h>(适用于V4L2)。

(3)缓冲区物理地址和虚拟地址都连续。
在页式内存管理系统中,分配物理地址和虚拟地址都连续的缓冲区是不可靠的,因为这种分配方式容易造成更多的内存碎片,某些情况下内存碎片过多会造成内存分配失败,从而导致系统无法正常功能工作。但是对于DMA传输来说却很方便。使用该缓冲区,需要包含头文件 <include/media/videobuf-dma-contig.h>(适用于V4L)或 <include/media/videobuf2-dma-contig.h>(适用于V4L2)。这中类型的videobuf2比较常用。

除此之外,还存在一种overlay缓冲区,其位于系统的显存中。目前overlay缓冲区已被弃用,但在一些片上系统的驱动中偶尔还能看到。
至于使用哪一种videobuf2,需要驱动工程师根据实际的使用环境进行评估。

数据结构

详见:linux V4L2子系统——v4l2的结构体(5)之videobuf2(vb2)

vb2_queue初始化

以sun6i-csi为例,示例说明如下:

static const struct vb2_ops sun6i_csi_vb2_ops = {
	.queue_setup		= sun6i_video_queue_setup,
	.wait_prepare		= vb2_ops_wait_prepare,
	.wait_finish		= vb2_ops_wait_finish,
	.buf_prepare		= sun6i_video_buffer_prepare,
	.start_streaming	= sun6i_video_start_streaming,
	.stop_streaming		= sun6i_video_stop_streaming,
	.buf_queue			= sun6i_video_buffer_queue,
};

int sun6i_video_init(struct sun6i_video *video, struct sun6i_csi *csi,
		     const char *name)
{
    ......
	/* Initialize videobuf2 queue */
	vidq->type			= V4L2_BUF_TYPE_VIDEO_CAPTURE;
	vidq->io_modes			= VB2_MMAP | VB2_DMABUF;
	vidq->drv_priv			= video;
	vidq->buf_struct_size		= sizeof(struct sun6i_csi_buffer);
	vidq->ops			= &sun6i_csi_vb2_ops;
	vidq->mem_ops			= &vb2_dma_contig_memops;
	vidq->timestamp_flags		= V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC;
	vidq->lock			= &video->lock;
    
    
	/* Make sure non-dropped frame */
	vidq->min_buffers_needed	= 3;
	vidq->dev			= csi->dev;

	ret = vb2_queue_init(vidq);
	if (ret) {
		v4l2_err(&csi->v4l2_dev, "vb2_queue_init failed: %d\n", ret);
		goto clean_entity;
	}
    ......
}
//源码: drivers/media/common/videobuf2/videobuf2-v4l2.c
int vb2_queue_init(struct vb2_queue *q)
{
	/*
	 * Sanity check
	 */
	if (WARN_ON(!q)			  ||
	    WARN_ON(q->timestamp_flags &
		    ~(V4L2_BUF_FLAG_TIMESTAMP_MASK |
		      V4L2_BUF_FLAG_TSTAMP_SRC_MASK)))
		return -EINVAL;

	/* Warn that the driver should choose an appropriate timestamp type */
	// 驱动必须选择合适的时间戳,否则内核会发出警告
	WARN_ON((q->timestamp_flags & V4L2_BUF_FLAG_TIMESTAMP_MASK) ==
		V4L2_BUF_FLAG_TIMESTAMP_UNKNOWN);

	/* Warn that vb2_memory should match with v4l2_memory */
	// 检查 vb2_memory 与 v4l2_memory 的枚举变量值是否一致
	if (WARN_ON(VB2_MEMORY_MMAP != (int)V4L2_MEMORY_MMAP)
		|| WARN_ON(VB2_MEMORY_USERPTR != (int)V4L2_MEMORY_USERPTR)
		|| WARN_ON(VB2_MEMORY_DMABUF != (int)V4L2_MEMORY_DMABUF))
		return -EINVAL;

	if (q->buf_struct_size == 0)
		q->buf_struct_size = sizeof(struct vb2_v4l2_buffer);

	q->buf_ops = &v4l2_buf_ops;
	q->is_multiplanar = V4L2_TYPE_IS_MULTIPLANAR(q->type);
	q->is_output = V4L2_TYPE_IS_OUTPUT(q->type);
	q->copy_timestamp = (q->timestamp_flags & V4L2_BUF_FLAG_TIMESTAMP_MASK)
			== V4L2_BUF_FLAG_TIMESTAMP_COPY;
	/*
	 * For compatibility with vb1: if QBUF hasn't been called yet, then
	 * return EPOLLERR as well. This only affects capture queues, output
	 * queues will always initialize waiting_for_buffers to false.
	 */
	q->quirk_poll_must_check_waiting_for_buffers = true;

	return vb2_core_queue_init(q);
}
EXPORT_SYMBOL_GPL(vb2_queue_init);
//源码: drivers/media/common/videobuf2/videobuf2-core.c
int vb2_core_queue_init(struct vb2_queue *q)
{
	/*
	 * Sanity check
	 */
	// 必须设置下面的成员,否则返回-EINVAL的错误
	if (WARN_ON(!q)			  ||
	    WARN_ON(!q->ops)		  ||
	    WARN_ON(!q->mem_ops)	  ||
	    WARN_ON(!q->type)		  ||
	    WARN_ON(!q->io_modes)	  ||
	    WARN_ON(!q->ops->queue_setup) ||
	    WARN_ON(!q->ops->buf_queue))
		return -EINVAL;

	if (WARN_ON(q->requires_requests && !q->supports_requests))
		return -EINVAL;

	INIT_LIST_HEAD(&q->queued_list);	// 初始化queued_list链表节点
	INIT_LIST_HEAD(&q->done_list);		// 初始化done_list链表节点
	spin_lock_init(&q->done_lock);		// 初始化自旋锁
	mutex_init(&q->mmap_lock);			// 初始化互斥锁
	init_waitqueue_head(&q->done_wq);	// 初始化等待队列头

	q->memory = VB2_MEMORY_UNKNOWN;

	// 若缓冲区大小驱动没有设置,则内核使用默认值初始化
	if (q->buf_struct_size == 0)
		q->buf_struct_size = sizeof(struct vb2_buffer);

	if (q->bidirectional)
		q->dma_dir = DMA_BIDIRECTIONAL;
	else
		q->dma_dir = q->is_output ? DMA_TO_DEVICE : DMA_FROM_DEVICE;

	return 0;
}
EXPORT_SYMBOL_GPL(vb2_core_queue_init);

使用方法探析

videobuf2的使用方法复杂,需要结合具体的驱动实例进行说明,这样比较好理解。应用可以通过调用 opencloseioctlmmapread 系统调用访问Video设备,内核根据不同的系统调用采用相对应的方法访问 videobuf2。下面从这些系统调用入手,分析内核中videobuf2的使用方法。

在这里插入图片描述

open/close

应用调用 open/close 打开/关闭Video设备,获取设备的描述符。内核中首先调用 v4l2_open / v4l2_release,然后调用驱动提供的 sun6i_video_open / sun6i_video_close 函数。

ioctl

V4L2子系统定义了很多ioctl命令供应用程序使用。

  • VIDIOC_REQBUFS 命令用于向内核申请缓冲区
  • VIDIOC_QUERYBUF 命令用于获取缓冲区信息
  • VIDIOC_QBUF 命令将读取完数据的空缓存返还给驱动的缓存队列
  • VIDIOC_DQBUF 命令将填充满数据的缓存从驱动中返回给应用
  • VIDIOC_STREAMOFF 命令用于关闭流,即停止图像采集
  • VIDIOC_STREAMON 命令用于开启流,即开启图像采集。

内核中的调用流程为 v4l2_ioctl -> video_ioctl2 -> __video_do_ioctl ->根据不同的命令调用不同的驱动函数->调用对应的videobuf2处理函数,具体调用流程参考上图。

下面具体分析一下ioctl调用的videobuf2处理函数。

VIDIOC_REQBUFS

使用 VIDIOC_REQBUFS 命令调用ioctl,最终会调用到 vb2_reqbufs 函数,内核使用 vb2_reqbufs 函数创建缓冲区。调用流程如下:

v4l_reqbufs
    |--->vb2_ioctl_reqbufs(sun6i_video_ioctl_ops.vidioc_reqbufs)
            |--->vb2_core_reqbufs
                    |--->sun6i_video_queue_setup(sun6i_csi_vb2_ops.queue_setup)
                    |--->__vb2_queue_alloc
                            |--->__init_vb2_v4l2_buffer(v4l2_buf_ops.init_buffer)
                            |--->__vb2_buf_mem_alloc
                                    |--->vb2_dc_alloc(vb2queue->mem_ops->alloc, vb2_dma_contig_memops.alloc)
                                    

(1)验证缓冲区的 memory typebuffer type 是否正确。
__verify_memory_type 函数用于校验缓冲区类型和缓冲区内存类型。vb2_queue 中的缓冲区类型 typev4l2_requestbuffers 中的缓冲区类型 type 需一致。缓冲区内存类型必须是 V4L2_MEMORY_MMAPV4L2_MEMORY_USERPTRV4L2_MEMORY_DMABUF 其中之一。

若是 V4L2_MEMORY_MMAP 类型,则 q->io_modes 必须设置为 VB2_MMAPmem_ops->allocq->mem_ops->putq->mem_ops->mmap 的函数指针也必须设置。

若是 V4L2_MEMORY_USERPTR 类型,则 q->io_modes 必须设置为 VB2_USERPTRmem_ops->get_userptrq->mem_ops->put_userptr 的函数指针也必须设置。

若是 V4L2_MEMORY_DMABUF 类型,则 q->io_modes 必须设置为 VB2_DMABUFmem_ops->attach_dmabufq->mem_ops->detach_dmabufq->mem_ops->map_dmabufq->mem_ops->unmap_dmabuf 的函数指针也必须设置。

(2)判断缓冲区参数是否正确,若不正确,则需要做一些处理。
申请的 缓冲区数量为0 或 缓冲区队列中缓冲区数量不为0 或 申请的缓冲区内存类型 和 缓冲区队列中缓冲区内存类型不一致 ,则进入额外的处理逻辑。若内存类型为 V4L2_MEMORY_MMAP 且缓冲区正在使用,则直接返回错误。清理处于 PREPAREDQUEUED 状态的缓冲区并释放缓冲区内存。

(3)计算需要分配的缓冲区数量。

(4)调用驱动实现的函数 queue_setup,驱动函数需要设置 num_buffersnum_buffersq->plane_sizesq->alloc_ctx

(5)调用 __vb2_queue_alloc 分配缓冲区内存。此时缓冲区的状态为 VB2_BUF_STATE_DEQUEUED

(6)若分配的缓冲区数量小于需要分配的数量,需要再次调用驱动提供的 queue_setup 函数。
(7)设置分配的缓冲区数量并向应用返回分配的缓冲区数量。

vb2_ioctl_reqbufs() 函数

// 源码: drivers/media/common/videobuf2/videobuf2-v4l2.c

int vb2_ioctl_reqbufs(struct file *file, void *priv,
			  struct v4l2_requestbuffers *p)
{
	struct video_device *vdev = video_devdata(file);
	int res = vb2_verify_memory_type(vdev->queue, p->memory, p->type);

	// 根据 io_mode 填充 capabilities
	fill_buf_caps(vdev->queue, &p->capabilities);
	if (res)
		return res;
	if (vb2_queue_is_busy(vdev, file))
		return -EBUSY;

	// 调用核心函数进行处理
	res = vb2_core_reqbufs(vdev->queue, p->memory, &p->count);
	/* If count == 0, then the owner has released all buffers and he
	   is no longer owner of the queue. Otherwise we have a new owner. */
	if (res == 0)
		vdev->queue->owner = p->count ? file->private_data : NULL;
	return res;
}
EXPORT_SYMBOL_GPL(vb2_ioctl_reqbufs);

vb2_core_reqbufs() 函数

// 源码: drivers/media/common/videobuf2/videobuf2-core.c
int vb2_core_reqbufs(struct vb2_queue *q, enum vb2_memory memory,
		unsigned int *count)
{
	unsigned int num_buffers, allocated_buffers, num_planes = 0;
	unsigned plane_sizes[VB2_MAX_PLANES] = { };
	unsigned int i;
	int ret;

	// 已开启数据流,则不能再次申请buf
	if (q->streaming) {
		dprintk(1, "streaming active\n");
		return -EBUSY;
	}

	if (q->waiting_in_dqbuf && *count) {
		dprintk(1, "another dup()ped fd is waiting for a buffer\n");
		return -EBUSY;
	}

	// 申请的缓冲区数量为0 或
	// 缓冲区队列中缓冲区数量不为0 或
	// 申请的缓冲区内存类型 和 缓冲区队列中缓冲区内存类型不一致,
	// 则需要则额外的处理
	if (*count == 0 || q->num_buffers != 0 ||
	    (q->memory != VB2_MEMORY_UNKNOWN && q->memory != memory)) {
		/*
		 * We already have buffers allocated, so first check if they
		 * are not in use and can be freed.
		 */
		mutex_lock(&q->mmap_lock);
		if (debug && q->memory == VB2_MEMORY_MMAP &&
		    __buffers_in_use(q))
			dprintk(1, "memory in use, orphaning buffers\n");

		/*
		 * Call queue_cancel to clean up any buffers in the
		 * QUEUED state which is possible if buffers were prepared or
		 * queued without ever calling STREAMON.
		 */
		// 若缓冲区处于PREPARED或QUEUED状态,则需要清理,
		// 一般在申请了缓冲区,但没调用STREAMON,会出现这种情况
		__vb2_queue_cancel(q);

		// 释放已经分配内存的缓冲区
		ret = __vb2_queue_free(q, q->num_buffers);
		mutex_unlock(&q->mmap_lock);
		if (ret)
			return ret;

		/*
		 * In case of REQBUFS(0) return immediately without calling
		 * driver's queue_setup() callback and allocating resources.
		 */
		if (*count == 0)
			return 0;
	}

	/*
	 * Make sure the requested values and current defaults are sane.
	 */
	WARN_ON(q->min_buffers_needed > VB2_MAX_FRAME);

	// 缓冲区数量取num_buffers和需要最少的缓冲区数量的较大值
	num_buffers = max_t(unsigned int, *count, q->min_buffers_needed);
	
	// 缓冲区数量取应用申请的数量和最大数量中的较小值,VIDEO_MAX_FRAME定义为32
	num_buffers = min_t(unsigned int, num_buffers, VB2_MAX_FRAME);
	memset(q->alloc_devs, 0, sizeof(q->alloc_devs));

	// 设置缓冲区队列中缓冲区的内存模型,内存模型应该和应用申请的模型保持一致
	q->memory = memory;

	/*
	 * Ask the driver how many buffers and planes per buffer it requires.
	 * Driver also sets the size and allocator context for each plane.
	 */
	// 回调用驱动提供的queue_setup函数,sun6i-csi 提供的函数为 sun6i_video_queue_setup
	// 驱动函数需要设置num_buffers、num_buffers、q->plane_sizes和q->alloc_ctx
	ret = call_qop(q, queue_setup, q, &num_buffers, &num_planes,
		       plane_sizes, q->alloc_devs);
	if (ret)
		return ret;

	/* Check that driver has set sane values */
	if (WARN_ON(!num_planes))
		return -EINVAL;

	for (i = 0; i < num_planes; i++)
		if (WARN_ON(!plane_sizes[i]))
			return -EINVAL;

	/* Finally, allocate buffers and video memory */
	allocated_buffers =
		__vb2_queue_alloc(q, memory, num_buffers, num_planes, plane_sizes);
	if (allocated_buffers == 0) {
		dprintk(1, "memory allocation failed\n");
		return -ENOMEM;
	}

	......

	mutex_lock(&q->mmap_lock);
	q->num_buffers = allocated_buffers;

	......
    
	return 0;
}

queue_setup() 函数
queue_setup函数 需要驱动提供。sun6i-csi 提供的函数为 sun6i_video_queue_setup,主要的作用是设置 __reqbufs函数 中的缓冲区数量 num_buffers、plane的数量 num_planes、plane的大小 q->plane_sizes 。上述设置的变量都是调用函数以指针的形式传入。

//sun6i-csi平台需由驱动自行实现,实为 sun6i_video_queue_setup
static int sun6i_video_queue_setup(struct vb2_queue *vq,
				   unsigned int *nbuffers,
				   unsigned int *nplanes,
				   unsigned int sizes[],
				   struct device *alloc_devs[])
{
	struct sun6i_video *video = vb2_get_drv_priv(vq);
	unsigned int size = video->fmt.fmt.pix.sizeimage;

	if (*nplanes)
		return sizes[0] < size ? -EINVAL : 0;

	*nplanes = 1;
	sizes[0] = size;

	return 0;
}

__vb2_queue_alloc() 函数
缓冲区分配主要由 __vb2_queue_alloc 函数实现。分配 num_buffers 个缓冲区,即分配 num_buffersstruct vb2_buffer 结构体。所有的缓冲区内存地址都保存到 vb2_queue 结构体中的bufs数组中。

若是缓冲区内存是 V4L2_MEMORY_MMAP 类型,则还需要额外分配保存图像的缓冲区,一个缓冲区分配 num_planes 个保存图像的缓冲区。此缓冲区由 vb2_dc_alloc 分配。分配完后缓冲器的结构示意如下图所示。

V4L2_MEMORY_MMAP 类型的缓冲区需要分配额外的内存空间用于存储图像数据,如图中绿框所属,首选分配一个管理的结构体 struct vb2_dc_buf ,再分配真正存储图像数据的缓冲区,存储图像的缓冲区物理地址和虚拟地址一致,其虚拟地址保存到管理结构体的 vaddr 成员中,虚拟地址保存到管理结构体的 dma_addr 成员中,缓冲区大小保存到管理结构体的 size成员 中。

在这里插入图片描述

// 源码: drivers/media/common/videobuf2/videobuf2-core.c

/*
 * __vb2_queue_alloc() - allocate videobuf buffer structures and (for MMAP type)
 * video buffer memory for all buffers/planes on the queue and initializes the
 * queue
 *
 * Returns the number of buffers successfully allocated.
 */
static int __vb2_queue_alloc(struct vb2_queue *q, enum vb2_memory memory,
			     unsigned int num_buffers, unsigned int num_planes,
			     const unsigned plane_sizes[VB2_MAX_PLANES])
{
	unsigned int buffer, plane;
	struct vb2_buffer *vb;
	int ret;

	/* Ensure that q->num_buffers+num_buffers is below VB2_MAX_FRAME */
	num_buffers = min_t(unsigned int, num_buffers,
			    VB2_MAX_FRAME - q->num_buffers);

	for (buffer = 0; buffer < num_buffers; ++buffer) {
		/* Allocate videobuf buffer structures */
		// 申请 vb2_buffer 内存,在初始化vb2_queue时会配置 buf_struct_size 大小
		// 注意:驱动程序一般会有个大结构体包含此结构体
		vb = kzalloc(q->buf_struct_size, GFP_KERNEL);
		if (!vb) {
			dprintk(1, "memory alloc for buffer struct failed\n");
			break;
		}

		// 初始化 vb2_buffer 
		vb->state = VB2_BUF_STATE_DEQUEUED;
		vb->vb2_queue = q;
		vb->num_planes = num_planes;
		vb->index = q->num_buffers + buffer;
		vb->type = q->type;
		vb->memory = memory;
		for (plane = 0; plane < num_planes; ++plane) {
			vb->planes[plane].length = plane_sizes[plane];
			vb->planes[plane].min_length = plane_sizes[plane];
		}

		// 回调 vb2_queue 中 buf_ops 的 init_buffer 函数
		// 此处实为:v4l2_buf_ops 中的 __init_vb2_v4l2_buffer
		call_void_bufop(q, init_buffer, vb);

		// 将新申请的 vb2_buffer 添加到 vb2_queue bufs 中
		q->bufs[vb->index] = vb;

		/* Allocate video buffer memory for the MMAP type */
		// 对于V4L2_MEMORY_MMAP类型,则还需要分配额外的内存用于保存图像数据
		// 然后映射到用户空间,用户可以直接读取额外内存中的数据
		if (memory == VB2_MEMORY_MMAP) {
			ret = __vb2_buf_mem_alloc(vb);
			if (ret) {
				dprintk(1, "failed allocating memory for buffer %d\n",
					buffer);
				q->bufs[vb->index] = NULL;
				kfree(vb);
				break;
			}
			__setup_offsets(vb);
			/*
			 * Call the driver-provided buffer initialization
			 * callback, if given. An error in initialization
			 * results in queue setup failure.
			 */
			// 回调 vb2_queue 中 vb2_ops 的 buf_init 函数初始化内存,
			// 此函数一般在驱动实现,也可不实现
			// sun6i-csi 没有实现
			ret = call_vb_qop(vb, buf_init, vb);
			if (ret) {
				dprintk(1, "buffer %d %p initialization failed\n",
					buffer, vb);
				__vb2_buf_mem_free(vb);
				q->bufs[vb->index] = NULL;
				kfree(vb);
				break;
			}
		}
	}

	dprintk(1, "allocated %d buffers, %d plane(s) each\n",
			buffer, num_planes);

	return buffer;
}

__vb2_buf_mem_alloc() 函数

// 源码: drivers/media/common/videobuf2/videobuf2-core.c

/*
 * __vb2_buf_mem_alloc() - allocate video memory for the given buffer
 */
static int __vb2_buf_mem_alloc(struct vb2_buffer *vb)
{
	struct vb2_queue *q = vb->vb2_queue;
	void *mem_priv;
	int plane;
	int ret = -ENOMEM;

	/*
	 * Allocate memory for all planes in this buffer
	 * NOTE: mmapped areas should be page aligned
	 */
	for (plane = 0; plane < vb->num_planes; ++plane) {
		/* Memops alloc requires size to be page aligned. */
		// 分配的内存大小按页对齐
		unsigned long size = PAGE_ALIGN(vb->planes[plane].length);

		/* Did it wrap around? */
		if (size < vb->planes[plane].length)
			goto free;

		// 回调 vb2_queue 中 mem_ops 的 alloc
		// sun6i-csi平台实为:vb2_dma_contig_memops.alloc
		// 即是:vb2_dc_alloc
		mem_priv = call_ptr_memop(vb, alloc,
				q->alloc_devs[plane] ? : q->dev,
				q->dma_attrs, size, q->dma_dir, q->gfp_flags);
		if (IS_ERR_OR_NULL(mem_priv)) {
			if (mem_priv)
				ret = PTR_ERR(mem_priv);
			goto free;
		}

		/* Associate allocator private data with this plane */
		// 将额外分配的内存保存到mem_priv成员中
		vb->planes[plane].mem_priv = mem_priv;
	}

	return 0;
free:
	/* Free already allocated memory if one of the allocations failed */
	for (; plane > 0; --plane) {
		call_void_memop(vb, put, vb->planes[plane - 1].mem_priv);
		vb->planes[plane - 1].mem_priv = NULL;
	}

	return ret;
}

mem_ops->alloc()函数
此函数依据场景不同,有三种,sun6i-csi平台使用的的事 缓冲区虚拟地址和物理地址都连续,属于第三种类型的 videobuf2,故该函数为 vb2_dc_alloc

// 源码:drivers/media/common/videobuf2/videobuf2-dma-contig.c
struct vb2_dc_buf {
	struct device			*dev;
	void				*vaddr;		// 内存虚拟地址
	unsigned long			size;	// 内存大小
	void				*cookie;
	dma_addr_t			dma_addr;	// 内存物理地址
	unsigned long			attrs;	// dma 内存属性
	enum dma_data_direction		dma_dir;//dma传输方向
	struct sg_table			*dma_sgt;	// SG DMA相关
	struct frame_vector		*vec;

	/* MMAP related */
	struct vb2_vmarea_handler	handler;
	refcount_t			refcount;
	struct sg_table			*sgt_base;

	/* DMABUF related */
	struct dma_buf_attachment	*db_attach;// DMABUF相关变量
};

static void *vb2_dc_alloc(struct device *dev, unsigned long attrs,
			  unsigned long size, enum dma_data_direction dma_dir,
			  gfp_t gfp_flags)
{
	struct vb2_dc_buf *buf;

	if (WARN_ON(!dev))
		return ERR_PTR(-EINVAL);

	// 首先分配一个struct vb2_dc_buf结构体
	buf = kzalloc(sizeof *buf, GFP_KERNEL);
	if (!buf)
		return ERR_PTR(-ENOMEM);

	if (attrs)
		buf->attrs = attrs;

	// 然后分配存储图像数据的缓冲区,此缓冲区的物理地址和虚拟地址都连续
	buf->cookie = dma_alloc_attrs(dev, size, &buf->dma_addr,
					GFP_KERNEL | gfp_flags, buf->attrs);
	if (!buf->cookie) {
		dev_err(dev, "dma_alloc_coherent of size %ld failed\n", size);
		kfree(buf);
		return ERR_PTR(-ENOMEM);
	}

	if ((buf->attrs & DMA_ATTR_NO_KERNEL_MAPPING) == 0)
		buf->vaddr = buf->cookie;

	/* Prevent the device from being released while the buffer is used */
	buf->dev = get_device(dev);	// 设置父设备指针
	buf->size = size;			// 保存图像数据缓冲区的大小
	buf->dma_dir = dma_dir;		// 记录DMA传输方向

	// 设置struct vb2_vmarea_handler结构体
	buf->handler.refcount = &buf->refcount;
	buf->handler.put = vb2_dc_put;	// 回调函数
	buf->handler.arg = buf;			// 回调函数参数

	refcount_set(&buf->refcount, 1); // 增加引用计数,引用计数为0时释放缓冲区

	return buf;	// 返回分配的 vb2_dc_buf 地址
}

VIDIOC_QUERYBUF

使用 VIDIOC_QUERYBUF 命令调用ioctl,最终会调用到 vb2_querybuf 函数,内核使用 vb2_querybuf 函数将缓冲区信息拷贝到用户空间,主要有时间戳 timestamp、标志 flags、缓冲区长度 length、缓冲区偏移 offset 等信息。

函数调用流程如下:

v4l_querybuf
    |--->vb2_ioctl_querybuf(sun6i_video_ioctl_ops.vidioc_querybuf)
            |--->vb2_querybuf
                    |--->vb2_core_querybuf
                            |--->__fill_v4l2_buffer(v4l2_buf_ops.fill_user_buffer)
// 源码: drivers/media/common/videobuf2/videobuf2-v4l2.c
/*
 * __fill_v4l2_buffer() - fill in a struct v4l2_buffer with information to be
 * returned to userspace
 */
static void __fill_v4l2_buffer(struct vb2_buffer *vb, void *pb)
{
	struct v4l2_buffer *b = pb;
	struct vb2_v4l2_buffer *vbuf = to_vb2_v4l2_buffer(vb);
	struct vb2_queue *q = vb->vb2_queue;
	unsigned int plane;

	/* Copy back data such as timestamp, flags, etc. */
	b->index = vb->index;
	b->type = vb->type;
	b->memory = vb->memory;
	b->bytesused = 0;

	b->flags = vbuf->flags;
	b->field = vbuf->field;
	b->timestamp = ns_to_timeval(vb->timestamp);
	b->timecode = vbuf->timecode;
	b->sequence = vbuf->sequence;
	b->reserved2 = 0;
	b->request_fd = 0;

	if (q->is_multiplanar) {
		/*
		 * Fill in plane-related data if userspace provided an array
		 * for it. The caller has already verified memory and size.
		 */
		b->length = vb->num_planes;
		for (plane = 0; plane < vb->num_planes; ++plane) {
			struct v4l2_plane *pdst = &b->m.planes[plane];
			struct vb2_plane *psrc = &vb->planes[plane];

			pdst->bytesused = psrc->bytesused;
			pdst->length = psrc->length;
			if (q->memory == VB2_MEMORY_MMAP)
				pdst->m.mem_offset = psrc->m.offset;
			else if (q->memory == VB2_MEMORY_USERPTR)
				pdst->m.userptr = psrc->m.userptr;
			else if (q->memory == VB2_MEMORY_DMABUF)
				pdst->m.fd = psrc->m.fd;
			pdst->data_offset = psrc->data_offset;
			memset(pdst->reserved, 0, sizeof(pdst->reserved));
		}
	} else {
		/*
		 * We use length and offset in v4l2_planes array even for
		 * single-planar buffers, but userspace does not.
		 */
		b->length = vb->planes[0].length;
		b->bytesused = vb->planes[0].bytesused;
		if (q->memory == VB2_MEMORY_MMAP)
			b->m.offset = vb->planes[0].m.offset;
		else if (q->memory == VB2_MEMORY_USERPTR)
			b->m.userptr = vb->planes[0].m.userptr;
		else if (q->memory == VB2_MEMORY_DMABUF)
			b->m.fd = vb->planes[0].m.fd;
	}

	/*
	 * Clear any buffer state related flags.
	 */
	b->flags &= ~V4L2_BUFFER_MASK_FLAGS;
	b->flags |= q->timestamp_flags & V4L2_BUF_FLAG_TIMESTAMP_MASK;
	if (!q->copy_timestamp) {
		/*
		 * For non-COPY timestamps, drop timestamp source bits
		 * and obtain the timestamp source from the queue.
		 */
		b->flags &= ~V4L2_BUF_FLAG_TSTAMP_SRC_MASK;
		b->flags |= q->timestamp_flags & V4L2_BUF_FLAG_TSTAMP_SRC_MASK;
	}

	switch (vb->state) {
	case VB2_BUF_STATE_QUEUED:
	case VB2_BUF_STATE_ACTIVE:
		b->flags |= V4L2_BUF_FLAG_QUEUED;
		break;
	case VB2_BUF_STATE_IN_REQUEST:
		b->flags |= V4L2_BUF_FLAG_IN_REQUEST;
		break;
	case VB2_BUF_STATE_ERROR:
		b->flags |= V4L2_BUF_FLAG_ERROR;
		/* fall through */
	case VB2_BUF_STATE_DONE:
		b->flags |= V4L2_BUF_FLAG_DONE;
		break;
	case VB2_BUF_STATE_PREPARING:
	case VB2_BUF_STATE_DEQUEUED:
		/* nothing */
		break;
	}

	if ((vb->state == VB2_BUF_STATE_DEQUEUED ||
	     vb->state == VB2_BUF_STATE_IN_REQUEST) &&
	    vb->synced && vb->prepared)
		b->flags |= V4L2_BUF_FLAG_PREPARED;

	if (vb2_buffer_in_use(q, vb))
		b->flags |= V4L2_BUF_FLAG_MAPPED;
	if (vbuf->request_fd >= 0) {
		b->flags |= V4L2_BUF_FLAG_REQUEST_FD;
		b->request_fd = vbuf->request_fd;
	}
}

VIDIOC_QBUF

使用 VIDIOC_QBUF 命令调用ioctl,最终会调用到 vb2_qbuf函数,内核使用 vb2_qbuf函数 将读取完数据的空缓存返还给驱动的缓存队列。调用流程如下:

v4l_qbuf
    |--->vb2_ioctl_qbuf(sun6i_video_ioctl_ops.vidioc_qbuf)
            |--->vb2_qbuf
                    |--->vb2_core_qbuf

vb2_qbuf 的主要工作如下:
(1)检查当前 v4l2_buffer 参数是否合法——vb2_queue_or_prepare_buf

(2)根据 v4l2_buffer index 获取 bufs数组 获取对应编号的缓冲区地址。

(3)根据缓冲区的不同状态做不同的处理。
VB2_BUF_STATE_DEQUEUED / VB2_BUF_STATE_IN_REQUEST 状态的缓冲区需要调用 __buf_prepare函数 执行一些准备工作。
首先将缓冲区状态设置为 VB2_BUF_STATE_PREPARINGV4L2_MEMORY_MMAP 调用__prepare_mmap,sun6i-csi 平台调用 sun6i_video_buffer_prepare。其他状态的缓冲区直接返回错误,不能进行 VIDIOC_QBUF 操作。

(4)将缓冲区挂到 queued_list 链表中。

(5)设置缓冲区状态为 VB2_BUF_STATE_QUEUED

(6)若 VIDIOC_STREAMON 已经被调用,start_streaming 未成功调用,当前 buf 数量 多余 最小,此时应再次调用 start_streaming

vb2_qbuf函数:

// 源码: drivers/media/common/videobuf2/videobuf2-v4l2.c
int vb2_qbuf(struct vb2_queue *q, struct media_device *mdev,
	     struct v4l2_buffer *b)
{
	struct media_request *req = NULL;
	int ret;

	if (vb2_fileio_is_active(q)) {
		dprintk(1, "file io in progress\n");
		return -EBUSY;
	}

	// 参数合法性检查,主要有以下几个:
	// 1. v4l2_buffer type 与 vb2_queue type
	// 2. v4l2_buffer index 是否越界
	// 3. vb2_queue bufs 所对应的 vb2_buffer 是否存在
	// 4. v4l2_buffer memory 与 vb2_queue memory 类型是否一致
	ret = vb2_queue_or_prepare_buf(q, mdev, b, false, &req);
	if (ret)
		return ret;

	ret = vb2_core_qbuf(q, b->index, b, req);
	if (req)
		media_request_put(req);
	return ret;
}
EXPORT_SYMBOL_GPL(vb2_qbuf);

vb2_core_qbuf函数:

// 源码: drivers/media/common/videobuf2/videobuf2-core.c

int vb2_core_qbuf(struct vb2_queue *q, unsigned int index, void *pb,
		  struct media_request *req)
{
	struct vb2_buffer *vb;
	int ret;

	if (q->error) {
		dprintk(1, "fatal error occurred on queue\n");
		return -EIO;
	}

	// 获取对应编号的缓冲区地址
	vb = q->bufs[index];

	if (!req && vb->state != VB2_BUF_STATE_IN_REQUEST &&
	    q->requires_requests) {
		dprintk(1, "qbuf requires a request\n");
		return -EBADR;
	}

	if ((req && q->uses_qbuf) ||
	    (!req && vb->state != VB2_BUF_STATE_IN_REQUEST &&
	     q->uses_requests)) {
		dprintk(1, "queue in wrong mode (qbuf vs requests)\n");
		return -EBUSY;
	}

	if (req) {
		int ret;

		q->uses_requests = 1;
		// 验证 vb 的状态是否为缓冲区出队
		if (vb->state != VB2_BUF_STATE_DEQUEUED) {
			dprintk(1, "buffer %d not in dequeued state\n",
				vb->index);
			return -EINVAL;
		}

		if (q->is_output && !vb->prepared) {
			ret = call_vb_qop(vb, buf_out_validate, vb);
			if (ret) {
				dprintk(1, "buffer validation failed\n");
				return ret;
			}
		}

		media_request_object_init(&vb->req_obj);

......

		// 更改 vb 的状态为缓冲区排队中
		vb->state = VB2_BUF_STATE_IN_REQUEST;

		/*
		 * Increment the refcount and store the request.
		 * The request refcount is decremented again when the
		 * buffer is dequeued. This is to prevent vb2_buffer_done()
		 * from freeing the request from interrupt context, which can
		 * happen if the application closed the request fd after
		 * queueing the request.
		 */
		media_request_get(req);
		vb->request = req;

		/* Fill buffer information for the userspace */
		if (pb) {
			// 此pb,实为v4l2_buffer,copy 时间戳
			call_void_bufop(q, copy_timestamp, vb, pb);

			// 此pb,实为v4l2_buffer,flag、memory类型等
			call_void_bufop(q, fill_user_buffer, vb, pb);
		}

		dprintk(2, "qbuf of buffer %d succeeded\n", vb->index);
		return 0;
	}

	if (vb->state != VB2_BUF_STATE_IN_REQUEST)
		q->uses_qbuf = 1;

	// 检查缓冲区状态,只有 VB2_BUF_STATE_DEQUEUED 和
	// VB2_BUF_STATE_IN_REQUEST 状态才能被加入到驱动的缓存队列

	switch (vb->state) {
	case VB2_BUF_STATE_DEQUEUED:
	case VB2_BUF_STATE_IN_REQUEST:
		if (!vb->prepared) {
			ret = __buf_prepare(vb);
			if (ret)
				return ret;
		}
		break;
	case VB2_BUF_STATE_PREPARING:
		dprintk(1, "buffer still being prepared\n");
		return -EINVAL;
	default:
		dprintk(1, "invalid buffer state %d\n", vb->state);
		return -EINVAL;
	}

	/*
	 * Add to the queued buffers list, a buffer will stay on it until
	 * dequeued in dqbuf.
	 */
	// 将应用传入的缓冲区挂到queued_list链表中
	list_add_tail(&vb->queued_entry, &q->queued_list);

	// queued_list链表中的缓冲区数量加1
	q->queued_count++;
	
	q->waiting_for_buffers = false;

	// 将该 vb 的状态 缓冲区入队中
	vb->state = VB2_BUF_STATE_QUEUED;

	if (pb)
		call_void_bufop(q, copy_timestamp, vb, pb);

	trace_vb2_qbuf(q, vb);

	/*
	 * If already streaming, give the buffer to driver for processing.
	 * If not, the buffer will be given to driver on next streamon.
	 */
	if (q->start_streaming_called)
		__enqueue_in_driver(vb);

	/* Fill buffer information for the userspace */
	if (pb) // 向用户空间填充信息
		call_void_bufop(q, fill_user_buffer, vb, pb);

	/*
	 * If streamon has been called, and we haven't yet called
	 * start_streaming() since not enough buffers were queued, and
	 * we now have reached the minimum number of queued buffers,
	 * then we can finally call start_streaming().
	 */
	// 若 VIDIOC_STREAMON已经被调用,start_streaming未成功调用,
	// 当前 buf 数量 多余 最小,此时应再次调用 start_streaming
	if (q->streaming && !q->start_streaming_called &&
	    q->queued_count >= q->min_buffers_needed) {
		ret = vb2_start_streaming(q);
		if (ret)
			return ret;
	}

	dprintk(2, "qbuf of buffer %d succeeded\n", vb->index);
	return 0;
}

// 源码: drivers/media/common/videobuf2/videobuf2-core.c

static int __buf_prepare(struct vb2_buffer *vb)
{
	struct vb2_queue *q = vb->vb2_queue;
	enum vb2_buffer_state orig_state = vb->state;
	unsigned int plane;
	int ret;

......

	// 将该 vb 的状态改为 正在准备缓存中
	vb->state = VB2_BUF_STATE_PREPARING;

	// 根据 vb2_memory memory 类型初始化内存
	switch (q->memory) {
	case VB2_MEMORY_MMAP:
		ret = __prepare_mmap(vb);
		break;
	case VB2_MEMORY_USERPTR:
		ret = __prepare_userptr(vb);
		break;
	case VB2_MEMORY_DMABUF:
		ret = __prepare_dmabuf(vb);
		break;
	default:
		WARN(1, "Invalid queue type\n");
		ret = -EINVAL;
		break;
	}

......

	return 0;
}
// 源码: drivers/media/common/videobuf2/videobuf2-core.c
/*
 * __prepare_mmap() - prepare an MMAP buffer
 */
static int __prepare_mmap(struct vb2_buffer *vb)
{
	int ret = 0;

	// 回调   vb2_queue 的buf_ops 中的 fill_vb2_buffer,
	// 实为 __fill_vb2_buffer, 即:v4l2_buf_ops 的 __fill_vb2_buffer
	ret = call_bufop(vb->vb2_queue, fill_vb2_buffer,
			 vb, vb->planes);

	// 上面正常,则继续回调 vb2_queue 中的 vb2_ops 的 buf_prepare
	// sun6i-csi平台实为:sun6i_video_buffer_prepare
	return ret ? ret : call_vb_qop(vb, buf_prepare, vb);
}

sun6i-csi 平台 buf_prepare 的回调函数为 sun6i_video_buffer_prepare,主要的作用是 检查buf大小,设置 payload,获取 dma_addr 并设置 struct sun6i_csi_buffer

static int sun6i_video_buffer_prepare(struct vb2_buffer *vb)
{
	struct vb2_v4l2_buffer *vbuf = to_vb2_v4l2_buffer(vb);
	struct sun6i_csi_buffer *buf =
			container_of(vbuf, struct sun6i_csi_buffer, vb);
	struct sun6i_video *video = vb2_get_drv_priv(vb->vb2_queue);
	unsigned long size = video->fmt.fmt.pix.sizeimage;

	if (vb2_plane_size(vb, 0) < size) {
		v4l2_err(video->vdev.v4l2_dev, "buffer too small (%lu < %lu)\n",
			 vb2_plane_size(vb, 0), size);
		return -EINVAL;
	}

	vb2_set_plane_payload(vb, 0, size);

	buf->dma_addr = vb2_dma_contig_plane_dma_addr(vb, 0);

	vbuf->field = video->fmt.fmt.pix.field;

	return 0;
}
VIDIOC_STREAMON

使用 VIDIOC_STREAMON 命令调用ioctl,最终会调用到 vb2_streamon 函数,内核使用 vb2_streamon 函数将开启视频流,对于图像采集设备而言,则设备开始采集图像,并将图像数据保存到缓冲区中。

v4l_streamon
    |--->vb2_ioctl_streamon(sun6i_video_ioctl_ops.vidioc_streamon)
            |--->vb2_streamon
                    |--->vb2_core_streamon
                            |--->vb2_start_streaming
                                    |--->sun6i_video_start_streaming(sun6i_csi_vb2_ops.start_streaming)
                                            |--->v4l2_subdev_call(subdev, video, s_stream, 1);

vb2_streamon 主要的工作如下:
(1)遍历 queued_list链表,首先将缓冲区状态设置为 VB2_BUF_STATE_ACTIVE,然后将入队的缓冲区都添加到驱动的队列中。sun6i-csi平台 调用 sun6i_video_buffer_queue 将缓冲区添加到 dma_queue 链表中。

(2)调用 start_streaming 开始视频流,sun6i-csi平台 调用 sun6i_video_start_streaming 函数使能设备,开始图像采集。

(3)流开启标志设置 streaming 设置为1。

// 源码: drivers/media/common/videobuf2/videobuf2-core.c
/*
 * vb2_start_streaming() - Attempt to start streaming.
 * @q:		videobuf2 queue
 *
 * Attempt to start streaming. When this function is called there must be
 * at least q->min_buffers_needed buffers queued up (i.e. the minimum
 * number of buffers required for the DMA engine to function). If the
 * @start_streaming op fails it is supposed to return all the driver-owned
 * buffers back to vb2 in state QUEUED. Check if that happened and if
 * not warn and reclaim them forcefully.
 */
static int vb2_start_streaming(struct vb2_queue *q)
{
	struct vb2_buffer *vb;
	int ret;

	/*
	 * If any buffers were queued before streamon,
	 * we can now pass them to driver for processing.
	 */
	// 将queued_list上的所有缓冲区添加到驱动中
	// 1. 设置缓冲区状态
	// 2. 增加驱动使用缓冲区的计数
	// 3. 回调驱动 vb2_queue 中 vb2_ops的 buf_queue,实为 sun6i_video_buffer_queue
	list_for_each_entry(vb, &q->queued_list, queued_entry)
		__enqueue_in_driver(vb);

	/* Tell the driver to start streaming */
	q->start_streaming_called = 1;

	// 回调驱动中的start_streaming函数,视频设备开始工作
	// 实为 sun6i_video_start_streaming
	ret = call_qop(q, start_streaming, q,
		       atomic_read(&q->owned_by_drv_count));
	if (!ret)
		return 0;

	q->start_streaming_called = 0;

	dprintk(1, "driver refused to start streaming\n");
	/*
	 * If you see this warning, then the driver isn't cleaning up properly
	 * after a failed start_streaming(). See the start_streaming()
	 * documentation in videobuf2-core.h for more information how buffers
	 * should be returned to vb2 in start_streaming().
	 */
	if (WARN_ON(atomic_read(&q->owned_by_drv_count))) {
		unsigned i;

		/*
		 * Forcefully reclaim buffers if the driver did not
		 * correctly return them to vb2.
		 */
		for (i = 0; i < q->num_buffers; ++i) {
			vb = q->bufs[i];
			if (vb->state == VB2_BUF_STATE_ACTIVE)
				vb2_buffer_done(vb, VB2_BUF_STATE_QUEUED);
		}
		/* Must be zero now */
		WARN_ON(atomic_read(&q->owned_by_drv_count));
	}
	/*
	 * If done_list is not empty, then start_streaming() didn't call
	 * vb2_buffer_done(vb, VB2_BUF_STATE_QUEUED) but STATE_ERROR or
	 * STATE_DONE.
	 */
	WARN_ON(!list_empty(&q->done_list));
	return ret;
}

VIDIOC_DQBUF

使用 VIDIOC_DQBUF 命令调用ioctl,最终会调用到 vb2_dqbuf函数,内核使用 vb2_dqbuf函数 将填充满数据的缓存从驱动中返回给应用。

v4l_dqbuf
    |--->vb2_ioctl_dqbuf(sun6i_video_ioctl_ops.vidioc_dqbuf)
            |--->vidioc_dqbuf
                    |--->vb2_core_dqbuf

vb2_dqbuf 主要的工作如下:
(1)检查缓冲区是否可用。
图像采集模式下,需要等待图像数据填充到缓冲区中才能被使用。非阻塞且无缓冲区可用,则直接返回 -EAGAIN 错误。阻塞且无缓冲区可用,睡眠等待。

(2)缓冲区可用,则获取一个可用的缓冲区并将其从 done_list链表 中删除。

(3)将可用的缓冲区信息拷贝到用户空间。

(4)将可用的缓冲区从 queued_list链表 中删除。

(5)设置缓冲区状态为 VB2_BUF_STATE_DEQUEUED

// 源码: drivers/media/common/videobuf2/videobuf2-core.c

int vb2_core_dqbuf(struct vb2_queue *q, unsigned int *pindex, void *pb,
		   bool nonblocking)
{
	struct vb2_buffer *vb = NULL;
	int ret;
	
    // 获取一个 done 的 vb2_buffer
	ret = __vb2_get_done_vb(q, &vb, pb, nonblocking);
	if (ret < 0)
		return ret;

	switch (vb->state) {
	case VB2_BUF_STATE_DONE:
		dprintk(3, "returning done buffer\n");
		break;
	case VB2_BUF_STATE_ERROR:
		dprintk(3, "returning done buffer with errors\n");
		break;
	default:
		dprintk(1, "invalid buffer state\n");
		return -EINVAL;
	}

	call_void_vb_qop(vb, buf_finish, vb);
	vb->prepared = 0;

	if (pindex)
		*pindex = vb->index;

	/* Fill buffer information for the userspace */
	if (pb)
		call_void_bufop(q, fill_user_buffer, vb, pb);

	/* Remove from videobuf queue */
	list_del(&vb->queued_entry);
	q->queued_count--;	// 入队缓冲区减1

	trace_vb2_dqbuf(q, vb);

	/* go back to dequeued state */
	// 设置缓冲器为VB2_BUF_STATE_DEQUEUED状态
	__vb2_dqbuf(vb);

......

	return 0;

}
// 源码: drivers/media/common/videobuf2/videobuf2-core.c
/*
 * __vb2_get_done_vb() - get a buffer ready for dequeuing
 *
 * Will sleep if required for nonblocking == false.
 */
static int __vb2_get_done_vb(struct vb2_queue *q, struct vb2_buffer **vb,
			     void *pb, int nonblocking)
{
	unsigned long flags;
	int ret = 0;

	/*
	 * Wait for at least one buffer to become available on the done_list.
	 */
	// 等待缓冲器是否可用,缓冲区可用,返回0
	// 非阻塞且无缓冲区可用,则直接返回-EAGAIN错误
	// 阻塞且无缓冲区可用,睡眠等待

	// wait_prepare 最终调用 vb2_ops_wait_prepare 函数
	ret = __vb2_wait_for_done_vb(q, nonblocking);
	if (ret)
		return ret;

	/*
	 * Driver's lock has been held since we last verified that done_list
	 * is not empty, so no need for another list_empty(done_list) check.
	 */
	spin_lock_irqsave(&q->done_lock, flags);
	// 获取done_list链表中一个可用的缓冲区
	*vb = list_first_entry(&q->done_list, struct vb2_buffer, done_entry);
	/*
	 * Only remove the buffer from done_list if all planes can be
	 * handled. Some cases such as V4L2 file I/O and DVB have pb
	 * == NULL; skip the check then as there's nothing to verify.
	 */
	// 确保取出的缓冲区所有planes可用
	if (pb)
		ret = call_bufop(q, verify_planes_array, *vb, pb);

	// 缓冲区所有planes可用,则将缓冲区从done_list链表移除
	if (!ret)
		list_del(&(*vb)->done_entry);
	spin_unlock_irqrestore(&q->done_lock, flags);

	return ret;
}

VIDIOC_STREAMOFF

使用 VIDIOC_STREAMOFF 命令调用ioctl,最终会调用到 vb2_streamoff函数,内核使用 vb2_streamoff函数 关闭流。调用流程如下:

v4l_streamoff
    |--->vb2_ioctl_streamoff(sun6i_video_ioctl_ops.vidioc_streamoff)
            |--->vb2_streamoff
                    |--->vb2_core_streamoff
                            |--->__vb2_queue_cancel(核心处理函数)
                                    |--->sun6i_video_stop_streaming(sun6i_csi_vb2_ops.stop_streaming)
                                            |--->v4l2_subdev_call(subdev, video, s_stream, 0);

vb2_streamoff 的主要工作如下:
(1)调用驱动提供的函数 stop_streaming 停止流,sun6i-csi平台 调用 sun6i_video_stop_streaming 函数关闭视频采集设备。

(2)清空入队链表和清空完成链表。

(3)将所有缓冲区出队到用户空间并设置 VB2_BUF_STATE_DEQUEUED 状态。

// 源码: drivers/media/common/videobuf2/videobuf2-core.c

/*
 * __vb2_queue_cancel() - cancel and stop (pause) streaming
 *
 * Removes all queued buffers from driver's queue and all buffers queued by
 * userspace from videobuf's queue. Returns to state after reqbufs.
 */
static void __vb2_queue_cancel(struct vb2_queue *q)
{
	unsigned int i;

	/*
	 * Tell driver to stop all transactions and release all queued
	 * buffers.
	 */
	// 调用驱动提供的停止流的函数 sun6i_video_stop_streaming
	if (q->start_streaming_called)
		call_void_qop(q, stop_streaming, q);

	/*
	 * If you see this warning, then the driver isn't cleaning up properly
	 * in stop_streaming(). See the stop_streaming() documentation in
	 * videobuf2-core.h for more information how buffers should be returned
	 * to vb2 in stop_streaming().
	 */
	if (WARN_ON(atomic_read(&q->owned_by_drv_count))) {
		for (i = 0; i < q->num_buffers; ++i)
			if (q->bufs[i]->state == VB2_BUF_STATE_ACTIVE) {
				pr_warn("driver bug: stop_streaming operation is leaving buf %p in active state\n",
					q->bufs[i]);
				vb2_buffer_done(q->bufs[i], VB2_BUF_STATE_ERROR);
			}
		/* Must be zero now */
		WARN_ON(atomic_read(&q->owned_by_drv_count));
	}

	q->streaming = 0;				// 清除流开启标志
	q->start_streaming_called = 0;	// 清除VIDIOC_STREAMON被调用标志
	q->queued_count = 0;			// 清除入队缓冲区计数
	q->error = 0;					// 清楚错误计数
	q->uses_requests = 0;			
	q->uses_qbuf = 0;

	/*
	 * Remove all buffers from videobuf's list...
	 */
	INIT_LIST_HEAD(&q->queued_list);	// 清空入队链表
	/*
	 * ...and done list; userspace will not receive any buffers it
	 * has not already dequeued before initiating cancel.
	 */
	INIT_LIST_HEAD(&q->done_list);		// 清空完成链表
	atomic_set(&q->owned_by_drv_count, 0); // 设置驱动引用计数为0
	wake_up_all(&q->done_wq);	// 唤醒等待在缓冲区队列的线程

	......
}

缓冲区状态变化

通过分析关于缓冲区的ioctl命令执行流程,可以总结出缓冲区的状态变化过程,如下图所示。黄色表示缓冲区属于用户空间,绿色表示缓冲区属于 videobuf2,青色表示缓冲区属于驱动。

(1)通过 VIDIOC_REQBUFS 命令申请缓冲区后,缓冲区的状态为 VB2_BUF_STATE_DEQUEUED ,缓冲区属于用户空间。

(2)通过 VIDIOC_QBUF 命令将缓冲区添加到内核空间,缓冲区的状态先变为 VB2_BUF_STATE_PREPARING,然后再变为 VB2_BUF_STATE_QUEUED ,缓冲区属于 videobuf2。若 VIDIOC_STREAMON 被调用,则 start_streaming_called=1,则 VIDIOC_QBUF 还会把缓冲区添加到驱动缓冲区队列中,此时缓冲区属于驱动。

(3)通过 VIDIOC_STREAMON 命令开启流后,缓冲区被添加到驱动缓冲区队列中,缓冲区的状态为 VB2_BUF_STATE_ACTIVE,此时缓冲区属于驱动。

(4)若DMA数据传输完成,则缓冲区中已被填充数据,则驱动调用 vb2_buffer_done 将缓冲区状态更改为 VB2_BUF_STATE_DONE,此时缓冲区属于 videobuf2

在这里插入图片描述

mmap

使用 mmap系统调用 映射Video设备,最终会调用到 vb2_mmap,内核使用 vb2_mmap 函数将Video设备的缓冲映射到用户空间。要使用 mmap,则缓冲区内存类型必须为 V4L2_MEMORY_MMAP,虚拟内存区域必须是共享的 VM_SHARED,图像输出模式时虚拟内存区域必须是可写的 VM_WRITE,图像采集模式时虚拟内存区域必须是可读的 VM_READ。调用流程如下:

v4l2_mmap
    |--->vb2_fop_mmap(sun6i_video_fops.mmap)
            |--->vb2_mmap(核心函数)
                    |--->vb2_dc_mmap(vb2_queue->mem_ops->mmap)
// 源码: drivers/media/common/videobuf2/videobuf2-core.c
int vb2_mmap(struct vb2_queue *q, struct vm_area_struct *vma)
{
	unsigned long off = vma->vm_pgoff << PAGE_SHIFT;
	struct vb2_buffer *vb;
	unsigned int buffer = 0, plane = 0;
	int ret;
	unsigned long length;

	// 判断 vb2_memory_type 是否为 VB2_MEMORY_MMAP
	if (q->memory != VB2_MEMORY_MMAP) {
		dprintk(1, "queue is not currently set up for mmap\n");
		return -EINVAL;
	}

	/*
	 * Check memory area access mode.
	 */
	// 根据类型 检查 vm_flags标志
	if (!(vma->vm_flags & VM_SHARED)) {
		dprintk(1, "invalid vma flags, VM_SHARED needed\n");
		return -EINVAL;
	}
	if (q->is_output) {
		if (!(vma->vm_flags & VM_WRITE)) {
			dprintk(1, "invalid vma flags, VM_WRITE needed\n");
			return -EINVAL;
		}
	} else {
		if (!(vma->vm_flags & VM_READ)) {
			dprintk(1, "invalid vma flags, VM_READ needed\n");
			return -EINVAL;
		}
	}

	mutex_lock(&q->mmap_lock);

	if (vb2_fileio_is_active(q)) {
		dprintk(1, "mmap: file io in progress\n");
		ret = -EBUSY;
		goto unlock;
	}

	/*
	 * Find the plane corresponding to the offset passed by userspace.
	 */
	ret = __find_plane_by_offset(q, off, &buffer, &plane);
	if (ret)
		goto unlock;

	vb = q->bufs[buffer];

	/*
	 * MMAP requires page_aligned buffers.
	 * The buffer length was page_aligned at __vb2_buf_mem_alloc(),
	 * so, we need to do the same here.
	 */
	// mmap要求内存按页对齐
	length = PAGE_ALIGN(vb->planes[plane].length);
	if (length < (vma->vm_end - vma->vm_start)) {
		dprintk(1,
			"MMAP invalid, as it would overflow buffer length\n");
		ret = -EINVAL;
		goto unlock;
	}

	/*
	 * vm_pgoff is treated in V4L2 API as a 'cookie' to select a buffer,
	 * not as a in-buffer offset. We always want to mmap a whole buffer
	 * from its beginning.
	 */
	vma->vm_pgoff = 0;

	// 调用驱动提供的mmap函数,实为 vb2_dc_mmap(vb2_dma_contig_memops.mmap)
	ret = call_memop(vb, mmap, vb->planes[plane].mem_priv, vma);

unlock:
	mutex_unlock(&q->mmap_lock);
	if (ret)
		return ret;

	dprintk(3, "buffer %d, plane %d successfully mapped\n", buffer, plane);
	return 0;
}

//源码:drivers/media/common/videobuf2/videobuf2-dma-contig.c

static int vb2_dc_mmap(void *buf_priv, struct vm_area_struct *vma)
{
	struct vb2_dc_buf *buf = buf_priv;
	int ret;

	if (!buf) {
		printk(KERN_ERR "No buffer to map\n");
		return -EINVAL;
	}

	// DMA内存一致性映射
	ret = dma_mmap_attrs(buf->dev, vma, buf->cookie,
		buf->dma_addr, buf->size, buf->attrs);

	if (ret) {
		pr_err("Remapping memory failed, error: %d\n", ret);
		return ret;
	}

	// 设置虚拟内存不能扩大和core dump标志
	vma->vm_flags		|= VM_DONTEXPAND | VM_DONTDUMP;

	// 设置私有数据
	vma->vm_private_data	= &buf->handler;

	// 设置虚拟内存的操作函数
	vma->vm_ops		= &vb2_common_vm_ops;

	// 打开虚拟内存
	vma->vm_ops->open(vma);

	pr_debug("%s: mapped dma addr 0x%08lx at 0x%08lx, size %ld\n",
		__func__, (unsigned long)buf->dma_addr, vma->vm_start,
		buf->size);

	return 0;
}

close

使用close系统关闭Video设备,最终会调用到 vb2_queue_release,内核使用 vb2_queue_release 关闭Video设备、清理缓冲区队列和释放缓冲区占用的内存。调用流程如下:

v4l2_release
    |--->sun6i_video_close(sun6i_video_fops.release)
            |--->_vb2_fop_release
                    |--->vb2_queue_release
                            |--->vb2_core_queue_release
                                    |--->__vb2_cleanup_fileio
                                    |--->__vb2_queue_cancel
                                    |--->__vb2_queue_free

vb2_core_queue_release 的主要工作如下:
(1)释放文件I/O模拟器占用的资源

(2)停止流,关闭Video设备,清空入队链表和清空完成链表,将所有缓冲区出队到用户空间并设置 VB2_BUF_STATE_DEQUEUED 状态。

(3)释放缓冲区内存。

// 源码: drivers/media/common/videobuf2/videobuf2-core.c

void vb2_core_queue_release(struct vb2_queue *q)
{
	__vb2_cleanup_fileio(q);	// 释放文件I/O模拟器占用的资源
	__vb2_queue_cancel(q);		// 退出和停止流
	mutex_lock(&q->mmap_lock);	
	__vb2_queue_free(q, q->num_buffers);// 释放缓冲区内存
	mutex_unlock(&q->mmap_lock);
}
EXPORT_SYMBOL_GPL(vb2_core_queue_release);
  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值