阅读原文
<http://www.yellowmax2001.com/2018/07/15/V4L2%E6%A1%86%E6%9E%B6-videobuf2/>

本文介绍在 v4l2 框架之下的数据流交互的实现与使用,主要目的是实现一个能够进行用户空间与内核空间进行数据交互、数据流格式设置、数据流 buffer
申请与释放、数据流开启与关闭的 video 设备驱动。

02 - V4L2框架-media-device
<https://blog.csdn.net/u013904227/article/details/80889947>
01 - V4L2框架-v4l2 device
<https://blog.csdn.net/u013904227/article/details/80782068>
00 - V4L2框架概述 <https://blog.csdn.net/u013904227/article/details/80718831>

<>简介

videobuf2 用于连接 V4L2 驱动层与用户空间层,提供数据交流的通道,它可以分配并管理视频帧数据。videobuf 层实现了很多 ioctl
函数,包括 buffer 分配、入队、出队和数据流控制。

* 为什么要有videobuf2?
因为 videobuf1 存在者下面的问题(原文收录于赫尔欣基峰会会议的 PPT 文档中,摘录如下)
* 不完善的以及错误的内存管理
a. 不能停止 streaming(在 streamoff 的时候,buffer 被释放,从而无法灵活地再次开启 stream);
b. VIDIOC_REQBUFS(0) 不会释放内存,这个主要就是使用上的方便与否;
c. 不能够使用 VIDIOC_REQBUFS 重复申请内存,道理同上;
d. video 内存在 mmap、qbuf 甚至 page fault 中分配;在 unmap、streamoff
或者驱动自定义的地方被释放,也就是说不够标准化、统一化;
e. 每一个buffer都有一个等待队列,太过繁琐,会导致 buffer 轮转的时间较长;
* 扩展性不足,尤其是嵌入式多媒体设备
a. 难以添加新的内存处理函数以及自定义的内存分配函数,比如现在 videobuf2
里面已有的三种-「dma-contig、sg-dma、vmalloc」;
b. 对 cache 以及 IOMMU 的支持非常弱;
c. 不够灵活。只有一个通用的函数来处理 cache、sg-list 创建等等事情,这些应该分为不同的抽象方法类进行差异化管理;
* 无用的成员,代码,变量(这个我就没有深入了解了)
* videobuf2要解决的问题
a. 更正 V4L2 的 API,修复 videobuf1 的问题与缺点「上面提到的问题」
b. 把队列管理与内存管理完全分离,采用分层的设计
c. 更多专门的驱动回调,不同类型驱动特定的,同时又满足可定制化的拆分,不必要的不使用,必要的按需使用
d. 支持新的V4L2 API扩展,比如多 planar 视频 buffer,比如 YUV 就是多 planer 的格式,它们的 Y、U、V
数据可能不在同一个连续的地址段内
<>数据类型「具体指的是视频数据的类型」

<>数据类型简介

不是所有的视频设备都使用同一种类型的 buffer,事实上,buffer 类型可以概括为三种:

* buffer 在虚拟地址以及物理地址上面都是分散的。几乎所有的用户空间 buffer 都是这种类型,在内核空间中,这种类型的 buffer
并不总是能够满足需要,因为这要求硬件可以进行分散的 DMA 操作。
* 物理地址分散,虚拟地址连续。也就是通过 vmalloc 分配的 buffer,换句话说很难使用 DMA 来操作这些buffer。
* 物理地址连续(虚拟地址不关心)。在分段式系统上面分配这种类型的 buffer
是不可靠的(分段式-也即页式系统上面有可能在长时间运行之后内存出现大量碎片,从而导致连续的内存空间很难获得,所以说不可靠),但是简单 DMA
控制器只能够适用于这种类型的 buffer。也有分段式的 DMA 操作,不过那种需要用到 IOMMU 来实现。
这三种类型的 buffer 对应的相关操作函数分别对应于 videobuf2-dma-sg.c,videobuf2-vmalloc.c,
videobuf2-dma-contig.c。

<>结构体、回调函数、初始化

根据buffer的不同,,需要用到的内核头文件也不同:
<media/videobuf-dma-sg.h> /* 物理地址分散 */ <media/videobuf-vmalloc.h> /* vmalloc()
分配的buffer */ <media/videobuf-dma-contig.h> /* 物理地址连续 */
需要实现以下几个回调函数来管理buffer:
struct vb2_ops { int (*queue_setup)(struct vb2_queue *q, const void *parg,
unsigned int *num_buffers, unsigned int *num_planes, unsigned int sizes[], void
*alloc_ctxs[]); void (*wait_prepare)(struct vb2_queue *q); void
(*wait_finish)(struct vb2_queue *q); int (*buf_init)(struct vb2_buffer *vb);
int (*buf_prepare)(struct vb2_buffer *vb); void (*buf_finish)(struct vb2_buffer
*vb); void (*buf_cleanup)(struct vb2_buffer *vb); int (*start_streaming)(struct
vb2_queue *q, unsigned int count); void (*stop_streaming)(struct vb2_queue *q);
void (*buf_queue)(struct vb2_buffer *vb); };
* queue_setup:
该回调函数在两个地方会被调用:
* 在真正的 memory 分配之前,由 VIDIOC_REQBUFS 和 VIDIOC_CREATE_BUFS 两个 ioctl
调用,一般情况下我们会使用VIDIOC_REQBUFS 这个 ioctl,后者不常使用。
* 如果不能够分配 num_buffers 指定的原始数量的的 buffer,那么该函数就会再次被调用来尽最大努力去分配 buffer 并返回实际被分配的
buffer 数量,返回值依然存到num_buffers,返回值这部分应该有驱动编写者来完成。
* num_planes 里面返回申请的 buffer 的 planes 的数量,比如对于 YUV420SP 格式的数据来说,它的 planes 就是
2「具体为什么是 2 就需要自己去网上找找 YUV 格式的解析啦,本文重点不在这里」。
* 每个 plane 的大小被存放在该回调函数的 sizes 数组成员里面,可选的,alloc_ctxs 数组可以存放每一个 plane
的特定数据,不过这种应用场景暂时没有遇到过,alloc_ctxs 没用过。
* 需要返回的值:驱动需要返回实际分配的 buffer 数量,存放在 num_buffers 中。返回 plane 数量,存放在 num_planes
中。每个 plane 的大小按照顺序存放在sizes[] 中,每个 plane 的分配者特定数据存放在 alloc_ctxs[] 「之前的版本是在
alloc_devs[]」中,这一步是可选的,因为并不总是用得到。
* wait_prepare:释放所有在 ioctl 操作函数执行时被持有的锁;该回调在 ioctl 需要等待一个新的 buffer
到达的时候被调用;需要在阻塞访问的时候避免死锁。
* wait_finish:请求所有的在上面 wait_prepare 回调锁释放的锁;需要在睡眠等待新的 buffer 到来之后继续运行。
上面两个回调比较通用的做法是:将 wait_prepare 赋值为 vb2_ops_wait_prepare,将 wait_finish 赋值为
vb2_ops_wait_finish,这两个是 videobuf2 为我们实现的两个回调,直接使用即可。上面两个函数在 vb2_internal_dqbuf
内部会被使用到,通常情况下会在用户 DQBUF 的时候使用,内核里面会判断是否是阻塞的情况,如果是阻塞调用并且没有准备好的数据,内核就会调用
wait_prepare 释放锁并进行休眠等待,直到有数据到达被唤醒之后才调用 wait_finish 重新持有锁。

*
buf_init:在 MMAP 方式下,分配完 buffer 或者 USERPTR 情况下请求完 buffer 之后被调用一次,(一个 buffer
调用一次)。如果该 ioctl 返回失败,将会导致queue_setup 执行失败。该函数的主要目的是让我们在 buffer 申请完毕之后对 buffer
做一些初始化工作,但是实际上好像并不是经常用到。在该函数里面可以获取到 buffer 的 type,memory 类型,index,planesnum 等

*
buf_prepare:每次 buffer 重新入队「就是在用户调用 QBUF 的时候」以及 VIDIOC_PREPARE_BUF
操作的时候被调用,驱动可以做一些硬件操作「通常数据都是由硬件产生的」之前的初始化工作。如果该回调返回失败,那么 buffer 将不会执行 queue
动作。一般在这里需要设置 plane 的 payload,也就是每个 plane 的使用内存长度,单位为 Byte,可以使用
vb2_set_plane_payload 来帮助完成「在 omap3isp 例程当中该回调函数里面获取 buffer 的 dma 地址,然后赋值给
buffer->dma 成员」。

*
buf_finish:每次 buffer 被取出的时候被调用,并且是在 buffer 到达用户空间之前,所以驱动可以访问/修改 buffer
的内容。buffer 的状态可以是:
VB2_BUF_STATE_DONE 或者 VB2_BUF_STATE_ERROR:这两种状态只在 streaming 的时候才会出现,前者是在驱动把
buffer 传递给 videobuf2 的时候被置位,此时还没有被传递给用户空间,后者与前者类似,也会最终被传递给用户,但是表明对 buffer
的操作最终以失败告终
VB2_BUF_STATE_PARPARED:videobuf2 准备好了 buffer,并且驱动持有 buffer。注意,驱动与 videobuf2
看作两个使用者
VB2_BUF_STATE_DEQUEUED:默认状态,表明 buffer 处于被用户使用的状态

*
buf_cleanup:buffer 被释放之前调用一次,每个 buffer 仅有一次,驱动可以在这里面做一些额外的操作。

*
start_streaming:进入 streaming 状态时被调用一次,一般情况下,驱动在该回调函数执行之前,通过 buf_queue 回调来接收
buffer,这里驱动需要把 buffer 放到驱动自己维护的一个 buffer 队列里面。count 参数存放已经被 queue 的 buffer
数量,驱动可以由此获取它。如果发生硬件错误,驱动可以通过该回调返回一个错误,此时所有的buffer都会被归还给 videobuf2(调用
vb2_buffer_done(VB2_BUF_STATE_QUEUED))。如果需要设置开始 start_streaming 需要的 buffer
最小数量,比如驱动只有在满足 buffer 数量大于等于 3 的时候才能够开启数据流,那么就可以在该函数被调用之前设置
vb2_queue->min_buffers_needed 成员为自己想要的值,此时该回调函数会在满足最小 buffer 数量之后才被调用。

*
stop_streaming:在 streaming 被禁止的时候调用,驱动需要关掉 DMA 或者等待 DMA 结束,调用 vb2_buffer_done
来归还所有驱动持有的 buffers(参数使用VB2_BUF_STATE_DONE 或者 VB2_BUF_STATE_ERR),可能需要用到
vb2_wait_for_all_buffers 来等待所有的 buffer,该函数是用来等待所有的 buffer 被归还给 videobuf2 的。

*
buf_queue:用来传递 vb(vb2_buffer 结构体) 给驱动,驱动可以在这里开启硬件操作(DMA 等等)。驱动填充 buffer
完毕之后需要调用vb2_buffer_done 归还 buffer,该函数总是在 VIDIOC_STREAMON 操作之后调用。但是也有可能在
VIDIOC_STREAMON 之前被调用,比如用户空间中 querybuf 之后的 queue 操作。videobuf2 允许自定义一个 buffer
结构体而不是直接使用vb2_buffer,但是自定义的结构体的第一个成员必须是 vb2_v4l2_buffer 结构体,该结构体的大小需要在 vb2_queue
的buf_struct_size 成员中指定。

每一个申请到的 buffer 都会被传递给 buf_prepare,该回调函数需要设置 buffer 的 size、width、height 与 field
成员,如果 buffer 的 state 成员为VIDEOBUF_NEEDS_INIT,驱动需要传递 buffer 给以下函数:
/* 该函数调用通常会为buffer分配空间,最后buf_prepare()需要设置buffer的state为VIDEOBUF_PREPARED。 */
int videobuf_iolock(struct videobuf_queue* q, struct videobuf_buffer *vb,
struct v4l2_framebuffer *fbuf);
当一个 buffer 需要进行队列操作时,它会被传递给 buf_queue,该回调函数需要将 buffer 放到驱动自行维护的一个 buffer
链表中,并且设置 buffer 状态为VIDEOBUF_QUEUED(这个状态位不需要驱动自己去设置,交给 videobuf2 就好了)。该函数会保持
spinlock(videobuf_queue 里面的 irqlock,新的 videobuf 不会保持锁,需要自己申请并在 buf_queue
里面使用),使用list_add_tail 来对 buffer 进行入队操作。

<>vb2_queue

该结构体可以当做是 videobuf2 的一个整体的抽象,要想使用 videobuf2 这个模块,就去初始化一个 vb2_queue
结构体吧,它会帮助我们建立起一个基于 videobuf2 的数据流管理模块的。

* vb2_queue 的初始化
vb2_queue 需要设置以下 field:
* type:v4l2_buf_type 枚举类型,表明设备的 buffer 类型
* io_modes:vb2_io_modes 枚举类型,表明驱动支持哪一种 streaming,实际应用中 VB2_MMAP 类型的居多
* drv_priv:一般指向驱动特定的结构体,也就是驱动自定义维护的结构体
* ops:指向 vb2_ops,也就是上面描述的的那一大坨
* mem_ops:根据 buffer 类型「物理连续、vmalloc、物理分散的 sg-dma」选择 vb2_dma_contig_memops,
vb2_dma_sg_memops,vb2_vmalloc_memops
三种之一,与我来说,最常用的就是第一个啦,当然也可以自己动手丰衣足食,不过常规使用那三个就能够满足了
* timestamp_flags:V4L2_BUF_FLAG_TIMESTAMP_MASK 或者
V4L2_BUF_FLAG_TSTAMP_SRC_MASK 类型的 flag 之一,两种 flag 可以是或的关系同时存在,表明时间戳的类型,一般使用
V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC 即可,表明是递增类型的,它是在内核的 monotonic 时钟时间轴上面生成的时间戳
* lock:可以与 video_device 共用一个 lock,更推荐使用独立的 lock,独立的 lock 一定程度上可以减少锁竞争
* buf_struct_size:自定义的 buffer 结构体大小,自定义的 buffer 结构体必须将 vb2_v4l2_buffer
成员放在第一个。可以在 vb2 的 queue 回调中通过container_of 来获取自定义的buffer结构体数据。
上面一顿操作之后要记得使用 vb2_queue_init 对 vb2_queue 进一步初始化,里面主要是一大堆的 WARN_ON
来对我们的设置进行检查,获取v4l2_buf_ops 这个 vb2_buf_ops 类型的 buffer
操作函数,并且初始化一些锁与一个等待队列,该等待队列主要是用来等待 buffer 变为可 dequeue 状态。

<>用户空间的操作


用户空间 stream 操作 ioctl 调用流程
通常用户空间的操作顺序如下:

* VIDIOC_S_PARAM::参数为 struct v4l2_streamparm,主要设置帧率,type 与
capturemode/outputmode 等。内核的 ioctl 需要保存
capturemode(mode需自定义)帧率等数据,可以保存到自定义的结构体里面。也可以在内核里面通过v4l2_subdev_call 来调用
* VIDIOC_S_FMT::参数为 struct v4l2_format,主要设置长宽,数据格式等。内核的 ioctl 需要保存长宽,像素格式,所在的
field 等。必要时需要调用相关的子设备的结构体进行子设备的格式设置。
* VIDIOC_REQBUFS::参数为 struct v4l2_requestbuffers,设置 count,type 与 memory 请求数据,在
queue_setup 回调函数里面需要设置一些东西。参见上面关于 queue_setup 的描述。
* VIDIOC_QUERYBUF::参数为 struct v4l2_buffer,需设置 index,type,memory,length(plane
length) 等参数
* VIDIOC_QBUF,该操作与上一个不断循环,直到获取所有的 buf 并将 buf 入队到内核驱动的的 buf 管理列表中
* VIDIOC_STREAMON::参数为 enum v4l2_buf_type,开启指定类型的 stream。
* select 等待 buf 可读。在内核驱动里面需要获取 plane 的地址,vb2_plane_vaddr,在数据填充完毕之后需要调用
vb2_buffer_done 来标注该 buffer 已经成功填充完毕,以便 v4l2 把数据放入 done_list,以待用户空间进行读取。
* VIDIOC_DQBUF,该操作与上一个操作不断循环,直到停止数据采集
* VIDIOC_STREAMOFF,结束数据采集工作。
超大示意图如下所示,下图展示了用户空间到内核驱动的 ioctl 调用顺序与调用过程中所涉及到的文件「图片比较大,电脑选择新标签页打开图片,或者保存下来看」:


<>如何把 /dev/video 节点与 videobuf2 联系起来

其实这个联系的过程上面一张图就能够说明了,这里再说明一些代码层面的联系方式。

首先 video_device 需要有自己的 open,release,unlocked_ioctl 等等,一个常见的初始化操作如下「此处参照了
omap3isp 里面的代码」:
static struct v4l2_file_operations isp_video_fops = { .owner = THIS_MODULE,
.unlocked_ioctl = video_ioctl2, .open = isp_video_open, .release =
isp_video_release, .poll = isp_video_poll, .mmap = isp_video_mmap, };
其中那几个函数几乎都是与 videobuf2 是挂钩的,如果去看下内核里面的代码就可以了然了,其中实用频率最高的就是 .unlocked_ioctl =
video_ioctl2, 这个回调函数了,这个回调函数是用户空间通过 /dev/videoX 节点通往 videobuf2
的不二路径,所以干脆直接就把这个成员赋值为video_ioctl2 了,这个是 videobuf2
为我们提供的操作函数,那就拿起来并使用它吧。从用户空间的角度看,只有一个 ioctl
的入口,但是我们穿过重重关卡,来到内核之后,再往前走两步,就可谓之「初极狭,才通人。复行数十步,豁然开朗」。里面还有一大坨分门别类的 ioctl
回调函数,截取如下:
static const struct v4l2_ioctl_ops isp_video_ioctl_ops = { .vidioc_querycap =
isp_video_querycap, .vidioc_g_fmt_vid_cap = isp_video_get_format,
.vidioc_s_fmt_vid_cap = isp_video_set_format, .vidioc_try_fmt_vid_cap =
isp_video_try_format, .vidioc_g_fmt_vid_out = isp_video_get_format,
.vidioc_s_fmt_vid_out = isp_video_set_format, .vidioc_try_fmt_vid_out =
isp_video_try_format, .vidioc_cropcap = isp_video_cropcap, .vidioc_g_crop =
isp_video_get_crop, .vidioc_s_crop = isp_video_set_crop, .vidioc_g_parm =
isp_video_get_param, .vidioc_s_parm = isp_video_set_param, .vidioc_reqbufs =
isp_video_reqbufs, .vidioc_querybuf = isp_video_querybuf, .vidioc_qbuf =
isp_video_qbuf, .vidioc_dqbuf = isp_video_dqbuf, .vidioc_streamon =
isp_video_streamon, .vidioc_streamoff = isp_video_streamoff, .vidioc_enum_input
= isp_video_enum_input, .vidioc_g_input = isp_video_g_input, .vidioc_s_input =
isp_video_s_input, };
有了上面两个,我们只需要在初始化 video_device 的时候做以下的操作,此时便可以把整个串联起来了:
video->video.fops = &isp_video_fops; /* 重点 */ video->video.vfl_type =
VFL_TYPE_GRABBER; video->video.release = video_device_release_empty; /* 重点 */
video->video.ioctl_ops = &isp_video_ioctl_ops; /* 重点 */
video->pipe.stream_state = ISP_PIPELINE_STREAM_STOPPED;
看到这里是不是就把 /dev/videoX,video_device,vb2_queue,videobuf2
这几个全部贯通起来了,现在再参照上面的那一个大图来重新梳理一遍:首先用户open 一个 /dev/videoX,获取其句柄,同时触发内核的 open
函数内部对 videobuf2 的vb2_queue 进行初始化;然后进行一系列的 ioctl 操作,入口是
isp_video_fops->unlocked_ioctl 成员,再往后会细分为 isp_video_ioctl_ops 里面的一个个回调,这一个个回调与
vb2 众多的 ops 深度结合起来共同完成了数据流的管理工作。

至于内部的一些细节,比如怎么从 unlocked_ioctl 到细分的 isp_video_ioctl_ops
这里不再细讲,否则占用篇幅过大,这一过程的部分的代码可以在v4l2-ioctl.c/__video_do_ioctl 函数中看到,快去看吧。

<>编程注意事项

* 要特别注意 plane 与 buffer 索引的区别,一个 buffer 下面有多个 plane,应该是这样的循环方式: for (buffer)
{ for (plane) { ... ... } }
* 像下面的 ioctl 尽量可以使用 vb2 提供的回调函数,如果需要自己实现的话也需要显式调用 vb2 提供的回调函数: V4L24L2 ioctls
VIDIOC_REQBUFS VIDIOC_QUERYBUF VIDIOC_QBUF VIDIOC_DQBUF VB2 ioctls
.start_streaming .stop_streaming
*
需要自己实现一个自旋锁用于 buffer
队列管理,关于为什么用自旋锁,原因是当驱动要获取数据的时候,可以加锁,然后从该队列删除,释放锁,填充数据可以等到释放锁之后进行,所以没必要用别的,自旋锁轻量,在这种情况下自旋锁的使用成本较低。通常在驱动里面维护一个
list 队列,存放激活的 buffer 数据,使用自旋锁自行管理,在 queue 的时候入队,数据填充完毕之后出队,调用vb2_buffer_done
来放入 vb2 的 buffer 队列等待用户拿取。在vb2_queue 初始化的时候可以设置 buf_struct_size
成员为自定义大小的帧结构体(自定义的结构体要把vb2_v4l2_buffer 放在第一个成员的位置,在里面可以添加帧的属性等等),要用到的时候就使用
container_of 来获取自定义的帧结构体。

*
使用wait队列来定时产生数据:
init_waitqueue_head kernel thread set_freezable DECLARE_WAITQUEUE
add_wait_queue generate datas ... schedule_timeout_interruptible
remove_wait_queue try_to_freeze wake_up_interruptible
*
在 streamoff 的时候需要调用 vb2_buffer_done(, ERR) 来归还所有的 buffer 到 vb2 里面。只要调用
vb2_buffer_done,该 buffer 就会被放入到 vb2 的各种队列里面,剩下的事情就是调用 vb2_ioctl_streamoff 让 vb2
来完成数据的释放啦。

*
如何获取 vb2_buffer 的虚拟地址与物理地址
dma-contig, scatter/gather-dma,vmalloc(只有虚拟地址) 虚拟地址:vb2_plane_vaddr() 物理地址:
dma-contig:vb2_dma_contig_plane_dma_addr
scatter/gather-dma:vb2_dma_sg_plane_desc
* 编程步骤
* 自定义一个 video 设备结构体,最简单的里面需要包含,video_device、vb2_queue 以及自定义的 buffer 帧结构体描述。
* 对上面提到的两个结构体 video_device、vb2_queue 进行初始化。
* 实现 vb2_ops 结构体,具体的实现以及注意事项参见上面的描述。 想做的事情就去做吧

友情链接
KaDraw流程图
API参考文档
OK工具箱
云服务器优惠
阿里云优惠券
腾讯云优惠券
华为云优惠券
站点信息
问题反馈
邮箱:[email protected]
QQ群:637538335
关注微信