linux v4l2 摄像头驱动 - video 视频流处理框架

    V4L2(Video for Linux 2) 是 Linux 操作系统中的视频驱动框架, 它为视频设备提供了统一的处理接口. 本文基于内核版本 Linux 4.4.94+ 分析 linux 视频驱动框架. 它包括以下内容.

  • /dev/videoX 视频流处理框架
  • /dev/v4l-subdevX 的子设备框架
  • V4L2 异步通知系统
  • Media Controller 框架

    /dev/videoX 节点对应的设备后续会称为 video 设备和 video 框架. /dev/v4l-subdevX 节点对应的设备则称为 subdev 设备和 subdev 框架. 无论是 video 设备还是 subdev 设备, 都是通过 v4l2_device 进行统一管理. video 设备subdev 设备 的整体框架如下所示.

注意: video 指代的是 /dev/videoX 节点对应的设备, 而不是内核中的 video_deivce 数据结构. 这个数据结构无论是 video 设备还是 subdev 设备都用到了.

一、/dev/videoX

    /dev/videoX 设备节点用于摄像头视频流的管理, 视频流数据传输, 摄像头到内存的 dma 传输, 视频流的控制, 启动停止, 内存分配, 自定义操作等. 它的整体框架如图所示.

    可以看出 video 设备本质就是字符设备. 主要操作就是一堆 ioctl, 主要的宏如下.

ioctl 功能含义
VIDIOC_QUERYCAP 查询设备能力(如是否支持视频输入/输出、驱动信息等)
VIDIOC_G_FMT 获取当前视频格式(分辨率、像素格式等)
VIDIOC_S_FMT 设置视频格式(分辨率、像素格式等)
VIDIOC_REQBUFS 申请缓冲区(用于流式 I/O 传输)
VIDIOC_QUERYBUF 查询缓冲区信息(如内存地址、大小、状态等)
VIDIOC_QBUF 将缓冲区放入队列(用于数据采集)
VIDIOC_DQBUF 从队列中取出缓冲区(获取已采集的数据)
VIDIOC_STREAMON 启动视频流(开始采集数据)
VIDIOC_STREAMOFF 停止视频流(停止采集数据)
VIDIOC_DEFAULT 处理未定义的 ioctl 请求(默认操作)

    对于以上的操作, 对应驱动需要提供三个操作函数 v4l2_ioctl_ops , vb2_ops, 以及 vb2_mem_ops.

1. 查询设备能力

应用需要返回的数据结构如下.

1
2
3
4
5
6
7
8
9
struct v4l2_capability {
__u8 driver[16]; // 驱动模块的名称
__u8 card[32]; // 视频设备描述信息, 厂商型号等
__u8 bus_info[32]; // 总线信息
__u32 version; // 内核版本号, 通过 KERNEL_VERSION 宏获取
__u32 capabilities; // 设备的整体功能
__u32 device_caps; // 设备节点的访问能力
__u32 reserved[3]; // 保留
};

对应的 ioctl 宏为 VIDIOC_QUERYCAP

1
2
3
4
5
6
7
8
9
10
11
12
#define VIDEO_DEVICE "/dev/video0"

int main() {
int fd = open(VIDEO_DEVICE, O_RDWR);

// 1. 查询设备能力
struct v4l2_capability cap;
ioctl(fd, VIDIOC_QUERYCAP, &cap)

...... // 省略部分代码
close(fd);
};

调用流程如下图

对于该接口, 驱动需要实现 vidioc_querycap 接口. 主要用于提供 cap->driver, cap->card, cap->bus_info

1
2
3
const struct v4l2_ioctl_ops xxx_ioctl_ops = {
.vidioc_querycap = xxx_querycap,
};

2. 获取当前视频格式

应用需要返回的数据结构如下.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/*
* 使用时,根据 `type` 字段的值来选择合适的 `fmt` 字段格式:
* - 如果 `type` 是 `V4L2_BUF_TYPE_VIDEO_CAPTURE`,则使用 `pix`
* - 如果 `type` 是 `V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE`,则使用 `pix_mp`
* - 如果 `type` 是 `V4L2_BUF_TYPE_VIDEO_OVERLAY`,则使用 `win`
* - 如果 `type` 是 `V4L2_BUF_TYPE_VBI_CAPTURE`,则使用 `vbi`
* - 如果 `type` 是 `V4L2_BUF_TYPE_SLICED_VBI_CAPTURE`,则使用 `sliced`
* - 如果 `type` 是 `V4L2_BUF_TYPE_SDR_CAPTURE`,则使用 `sdr`
* - 如果 `type` 是 `V4L2_BUF_TYPE_META_CAPTURE`,则使用 `meta`
* - 如果需要自定义格式,可以使用 `raw_data`
*/
struct v4l2_format {
__u32 type;
union {
// 单平面视频图像格式(适用于大多数普通摄像头)
struct v4l2_pix_format pix;

// 多平面视频图像格式(适用于 YUV420 等多平面格式,如现代摄像头或视频编解码器)
struct v4l2_pix_format_mplane pix_mp;

// 视频叠加层格式(定义视频叠加窗口的位置、混合方式等)
struct v4l2_window win;

// 原始 VBI (Vertical Blanking Interval) 格式
// 用于捕获模拟视频信号中的垂直消隐期数据(如图文电视)
struct v4l2_vbi_format vbi;

// 分片 VBI 格式(结构化的 VBI 数据,按行分片)
struct v4l2_sliced_vbi_format sliced;

// 软件定义无线电 (SDR) 格式
// 用于定义 SDR 设备的采样率、调制方式等参数
struct v4l2_sdr_format sdr;

// 原始数据缓冲区(保留字段,用于未来扩展或自定义格式)
struct v4l2_meta_format meta;
__u8 raw_data[200];
} fmt;
};

对于普通的摄像头是会用到 v4l2_pix_format. 这里只以这个为例进行说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// 描述单平面视频帧的格式(适用于传统摄像头)
struct v4l2_pix_format {
// 图像宽度(像素),例如 1920
__u32 width;

// 图像高度(像素),例如 1080
__u32 height;

// 像素格式(四字符编码,如 V4L2_PIX_FMT_YUYV 表示 YUV422 格式)
__u32 pixelformat;

// 扫描模式(枚举 v4l2_field)
// V4L2_FIELD_NONE = 逐行扫描
// V4L2_FIELD_INTERLACED = 隔行扫描
__u32 field;

// 每行像素数据的字节数(包含内存对齐填充)
// 计算公式:width * 每像素字节数 + 填充字节
// 若为 0 表示无填充(连续存储)
__u32 bytesperline;

// 整个图像数据的总大小(单位:字节)
// 典型计算:bytesperline * height
__u32 sizeimage;

// 颜色空间(枚举 v4l2_colorspace)
// 例如:V4L2_COLORSPACE_SRGB(sRGB 颜色空间)
__u32 colorspace;

// 私有数据,含义取决于 pixelformat(通常保留使用)
__u32 priv;

// 格式标志位(V4L2_PIX_FMT_FLAG_* 的位掩码组合)
// 例如:V4L2_PIX_FMT_FLAG_PREMUL_ALPHA(预乘 Alpha 通道)
__u32 flags;

// 颜色编码联合体(根据场景选择成员)
union {
// Y'CbCr 编码标准(如 ITU-R BT.601/709/2020)
__u32 ycbcr_enc;

// HSV 编码方式(极少使用场景)
__u32 hsv_enc;
};

// 量化范围(枚举 v4l2_quantization)
// V4L2_QUANTIZATION_FULL_RANGE = 0-255(PC 范围)
// V4L2_QUANTIZATION_LIM_RANGE = 16-235(TV 范围,YUV 默认)
__u32 quantization;

// 传输函数(枚举 v4l2_xfer_func)
// 定义光信号到电信号的转换曲线(如 sRGB 伽马曲线)
__u32 xfer_func;
};

同样的应用调用如下.

1
2
3
4
5
...... // 省略无关代码
struct v4l2_format fmt;
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ioctl(fd, VIDIOC_G_FMT, &fmt)
......

调用到驱动的调用链如下

也就是驱动要实现一个 vidioc_g_fmt_vid_cap 用于填充 v4l2_pix_format 数据结构.

1
2
3
const struct v4l2_ioctl_ops xxx_ioctl_ops = {
.vidioc_g_fmt_vid_cap = xxx_fmt_vid_cap,
};

3. 设置视频格式

1
2
3
4
5
6
7
8
struct v4l2_format new_fmt;
new_fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
new_fmt.fmt.pix.width = 640;
new_fmt.fmt.pix.height = 480;
new_fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
new_fmt.fmt.pix.field = V4L2_FIELD_INTERLACED;

ioctl(fd, VIDIOC_S_FMT, &new_fmt)

调用流程如下

驱动需要实现 vidioc_s_fmt_vid_cap 函数, 用于设置摄像头格式.

1
2
3
const struct v4l2_ioctl_ops xxx_ioctl_ops = {
.vidioc_s_fmt_vid_cap = xxx_fmt_vid_cap,
};

4. 申请缓冲区

    v4l2 支持 4 种分配内存的方式 mmap, user, overlay, dma-buf. 本文只讲前两种常用的, 在内核中使用 vb2_queue 来管理 buffer. 在内核中如下图所示

  • 每一个 vb2_buffer 用于描述一帧图像
  • 一帧图像可以由一个或者多个 vb2_plane 组成, (例如可以将 YUV 数据分别存储于三个 vb2_plane), 大多数情况一个 vb2_plane 里面存放一帧数据
  • 每一个 vb2_plane 有一个 mem_priv 指向一个数据结构, 该结构用于保存实际分配的内存, videobuf2 为我们提供了一个数据结构 vb2_vmalloc_buf.
  • 由于每一帧图像的数据量是确定的, 分辨率一般是不会变的, 所以为了减少代码冗余, 使用 plane_sizes 数组用于保存每个 vb2_plane 的数据大小

1) mmap 方式

    用于空间需要用到 v4l2_requestbuffers 用来要分配的内存类型.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct v4l2_requestbuffers {
__u32 count; // 缓冲区的数量
__u32 type; // 缓冲区的类型, 由 v4l2_buf_type 进行描述
__u32 memory; // 内存分配的方式
__u32 capabilities;
__u32 reserved[1];
};

// 可用的 type , 常用的就两个
enum v4l2_buf_type {
V4L2_BUF_TYPE_VIDEO_CAPTURE = 1, // 捕获视频流数据
V4L2_BUF_TYPE_VIDEO_OUTPUT = 2, // 输出视频流数据
.....// 省略
/* Deprecated, do not use */
V4L2_BUF_TYPE_PRIVATE = 0x80,
};

// memory 的类型有四种
enum v4l2_memory {
V4L2_MEMORY_MMAP = 1, // mmap 内核空间分配
V4L2_MEMORY_USERPTR = 2, // user 用户空间分配
V4L2_MEMORY_OVERLAY = 3, // 覆盖模式, 将数据直接映射到显存
V4L2_MEMORY_DMABUF = 4, // dma-buf
};

应用的调用方式如下所示

1
2
3
4
5
6
struct v4l2_requestbuffers req;
req.count = 4;
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
req.memory = V4L2_MEMORY_MMAP;

ioctl(fd, VIDIOC_REQBUFS, &req);

驱动的调用流程如下.

    从调用流程可以知道驱动要实现的接口有 queue_setup

1
2
3
4
static const struct vb2_ops uvc_queue_qops = {
.queue_setup = uvc_queue_setup,
......
};

    传入的 count 被转换为 num_buffers, 但具体的数量由驱动决定, 以及每一个 vb2_buffer 的数量也是由驱动决定的. 因此 uvc_queue_setup 包含以下内容.

  • 根据传入的 num_buffers 重新计算 vb2_buffer 的数量.
  • 指定 vb2_plane 的数量, 大部分情况为 1
  • 根据 vb2_plane 的数量, 将每个 vb2_plane 的大小填充到 vb2_queue->plane_sizes 数组中

    可以参考 uvc 驱动的实现 drivers/usb/gadget/function/uvc_queue.c, 这里就不搬代码了.从上面的图可以看出 __vb2_queue_alloc 函数也是非常重要的函数我这里就直接附上源码.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
static int __vb2_queue_alloc(struct vb2_queue *q, enum vb2_memory memory,
unsigned int num_buffers, unsigned int num_planes)
{
unsigned int buffer;
struct vb2_buffer *vb;
int ret;

// 分配 num_buffers 个 vb2_buffer
for (buffer = 0; buffer < num_buffers; ++buffer) {
vb = kzalloc(q->buf_struct_size, GFP_KERNEL); // 分配 vb2_buffer
if (!vb) {
dprintk(1, "memory alloc for buffer struct failed\n");
break;
}

// 设置 buffer 的 state 为已出队状态
// VB2_BUF_STATE_DEQUEUED: 已出队, 代表缓冲区已被用户空间取出,当前由用户应用程序控制
// VB2_BUF_STATE_PREPARING: 准备中, 缓冲还未完全初始化
// VB2_BUF_STATE_PREPARED: videobuf 和驱动程序已完成缓冲区的准备,缓冲区可用于硬件操作
// VB2_BUF_STATE_QUEUED: 缓冲区已加入 videobuf 队列,但尚未送至驱动程序处理
// VB2_BUF_STATE_REQUEUEING: 缓冲区正在重新提交给驱动程序进行处理
// VB2_BUF_STATE_ACTIVE: 缓冲区已经被驱动程序接收,并可能正在用于硬件操作(如视频采集、编码等)
// VB2_BUF_STATE_DONE: 硬件或驱动已处理完缓冲区,并返回到 videobuf,但尚未被用户空间取出, 操作完成,等待用户获取
// VB2_BUF_STATE_ERROR: 缓冲区处理过程中发生错误
vb->state = VB2_BUF_STATE_DEQUEUED;
vb->vb2_queue = q; // 设置所属的 vb2_queue
vb->num_planes = num_planes; // 设置 vb2_plane 的数量
vb->index = q->num_buffers + buffer; // 设置 index , 该 index 也是数组标
vb->type = q->type; // 设置 type
vb->memory = memory; // 设置内存分配方式

if (memory == VB2_MEMORY_MMAP) {
// 遍历 vb2_buffer 的 vb2_plane 为每一个 vb2_plane 分配内存
// 分配的内存保存在 mem_priv 数据结构中, 具体内存的分配方式由b->vb2_queue->mem_ops->alloc 提供, vb2_plane 的大小更新到 length 成员变量
ret = __vb2_buf_mem_alloc(vb);
if (ret) {
dprintk(1, "failed allocating memory for "
"buffer %d\n", buffer);
kfree(vb);
break;
}

// 如果 ops 设置了 buf_init 则调用该函数对 buffer 进一步初始化.
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);
kfree(vb);
break;
}
}

// 将 vb2_buffer 保存到 vb2_queue->bufs[]数组中 中
q->bufs[q->num_buffers + buffer] = vb;
}

// 更新 vb2_buffer 中 vb2_plane 的大小
__setup_lengths(q, buffer);
// 更新新分配的 vb2_plane.m.offset 的值, 它等于之前分配的 plane 的数据长度加上自己的数据长度.
if (memory == VB2_MEMORY_MMAP)
__setup_offsets(q, buffer);

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

return buffer;
}

这个函数的功能如下

  • 分配 num_buffers 个 vb2_buffer , 设置所属的 vb2_queue, 更新 vb2_plane 的数量 num_planes, 设置数组索引 index, 设置内存分配方式 memory.
  • 如果内存分配方式是 VB2_MEMORY_MMAP, 则遍历 vb2_buffer 的 vb2_plane 为每一个 vb2_plane 分配内存, 配的内存保存在 mem_priv 数据结构中, 具体内存的分配方式由 b->vb2_queue->mem_ops->alloc 提供
  • 更新 vb2_buffer 中 vb2_plane 的大小即 length 成员变量.
  • 如果内存分配的方式是 VB2_MEMORY_MMAP , 更新新分配的 vb2_plane.m.offset 的值, 它等于之前分配的 vb2_plane 的数据长度加上自己的数据长度. 我们只需要知道 vb2_plane.m.offset 每一个 vb2_plane 都是唯一的, 它也是作为 plane 的索引.

其中驱动需要提供 alloc 函数, 用于分配实现内存分配接口.

1
2
3
4
const struct vb2_mem_ops vb2_vmalloc_memops = {
.alloc = vb2_vmalloc_alloc,
......
}

对于 vb2_mem_ops 我们一般使用默认的 vb2_buffer 提供的默认接口, 这里 alloc 则对应 vb2_vmalloc_alloc.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
static void *vb2_vmalloc_alloc(void *alloc_ctx, unsigned long size,
enum dma_data_direction dma_dir, gfp_t gfp_flags)
{
struct vb2_vmalloc_buf *buf;

buf = kzalloc(sizeof(*buf), GFP_KERNEL | gfp_flags);
if (!buf)
return NULL;

buf->size = size; // 设置 buffer 的大小
buf->vaddr = vmalloc_user(buf->size); // 分配虚拟内存
buf->dma_dir = dma_dir; // 设置 dma_dir
buf->handler.refcount = &buf->refcount;
buf->handler.put = vb2_vmalloc_put;
buf->handler.arg = buf;

if (!buf->vaddr) {
pr_debug("vmalloc of size %ld failed\n", buf->size);
kfree(buf);
return NULL;
}

atomic_inc(&buf->refcount);
return buf;
}

    这个函数就是用于分配内存的, vb2 提供了一个默认的数据结构. 这个数据结构被保存到了 vb2_buffer->planes[plane].mem_priv.

1
2
3
4
5
6
7
8
9
struct vb2_vmalloc_buf {
void *vaddr; // 用于保存分配的内存的虚拟地址.
struct frame_vector *vec;
enum dma_data_direction dma_dir;
unsigned long size;
refcount_t refcount;
struct vb2_vmarea_handler handler;
struct dma_buf *dbuf; // 如果是 dma_buf 则放到这里
};

总结

  1. 驱动需要实现 vb2_ops->queue_setup 用于提供 vb2_buffer 的数量, vb2_plane 的数量和大小.
  2. 驱动需要还需要实现另一个接口 vb2_mem_ops->alloc 用于分配实际的内存空间, 大多数情况, 使用 videobuf2-vmalloc.c 中提供的 vb2_vmalloc_alloc 函数, 当然也可以根据需求自己实现.

2) user 分配

    user 分配和 mmap 分配完全一样只是不会调用 __vb2_buf_mem_alloc 分配触发真正的内存分配而已. 因为真正的内存由用户空间分配, 在 VIDIOC_QBUF 提交 buffer 到内核操作的时候会分配该 __vb2_buf_mem_alloc, 并将转换后的虚拟地址填充到该结构.

5. 查询缓冲区信息

    查询缓冲区的作用就是返回前面分配的内存信息, 即返回内核里面的 v4l2_buffer.

1
2
3
4
5
6
7
......
struct v4l2_buffer buf;
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = 0;
ioctl(fd, VIDIOC_QUERYBUF, &buf);
......

调用流程如下所示

    根据 index 返回分配的对应的 v4l2_buffer , 不同内存分配的方式也会返回不同的信息.

  • mmap 分配的内存则会返回 b->m.offset 这个则是 mmap 的索引
  • user 方式则返回 b.m->userptr 指向用户空间分配的内存
  • dma-buf 方式则返回对应 b.m.fd

6. 将缓冲区放入队

1
2
3
4
5
6
7
8
9
10
11
struct v4l2_buffer buf;
buf.index = 0;
buf.length = dev->mem[i].length;
buf.m.userptr = (unsigned long)dev->mem[i].start;
buf.type = V4L2_BUF_TYPE_VIDEO_OUTPUT;

if (ioctl(fd, VIDIOC_QBUF, &buf) == -1) {
perror("将缓冲区放入队列失败");
close(fd);
return 1;
}

调用流程如下所示

    从调用链可以看出, 对于 VIDIOC_QBUF 操作就是一些回调函数的实现, 主要需要实现的函数如下所示.

1) fill_vb2_buffer

非必须实现, 该接口用于更新用户传入的 vb2_buffer , 即用内核里面的 vb2_buffer 更新用户空间传入的 vb2_buffer. 内核为我们提供了一个默认的实现 __fill_vb2_buffer. 当调用内核的 vb2_queue_init 初始化 vb2_queue 时设置.

1
2
3
4
5
6
7
8
9
10
11
//drivers/media/common/videobuf2/videobuf2-v4l2.c
static const struct vb2_buf_ops v4l2_buf_ops = {
.fill_vb2_buffer = __fill_vb2_buffer,
};

int vb2_queue_init(struct vb2_queue *q)
{
......
q->buf_ops = &v4l2_buf_ops;
......
};

2) buf_prepare

该接口用于驱动对 buffer 在放入链表 vb2_queue->queued_lis 前作处理.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
static struct vb2_ops uvc_queue_qops = {
......
.buf_prepare = uvc_buffer_prepare,
......
};

static int uvc_buffer_prepare(struct vb2_buffer *vb)
{
struct uvc_video_queue *queue = vb2_get_drv_priv(vb->vb2_queue);
struct vb2_v4l2_buffer *vbuf = to_vb2_v4l2_buffer(vb);
struct uvc_buffer *buf = container_of(vbuf, struct uvc_buffer, buf);

....

// 更新 buffer 状态
buf->state = UVC_BUF_STATE_QUEUED;
buf->mem = vb2_plane_vaddr(vb, 0); // 获取 buffer 的虚拟地址
buf->length = vb2_plane_size(vb, 0); // 获取 buffer 的大小
if (vb->type == V4L2_BUF_TYPE_VIDEO_CAPTURE)
buf->bytesused = 0;
else
buf->bytesused = vb2_get_plane_payload(vb, 0);

return 0;
}

    对于 uvc 来说就是更新私有 uvc_buffer 的虚拟地址和 buffer 的大小 buf_prepare 会对 vb2_buffer 做预处理. 它和后面 buf_queue 是两个相互配合的接口. uvc 和一些常规的驱动, 会在这里拿到 vb2_buffer 的虚拟地址. 还有一些例如 rk 的驱动会在 buf_queue 中拿走虚拟地址. 就是 camer 控制器的图像传输用到的内存地址是从 buf_prepare或者 buf_queue 获取的. 具体取决于驱动的选择.

3) get_userptr

    如果需要支持用户空间分配内存必须实现, 该接口用于将用户空间的分配的内存地址, 转换为内核空间的虚拟地址. 内核也提供了默认的实现 vb2_vmalloc_get_userptr, 一般情况初始化 vb2_queue 时设置, 参考 uvc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const struct vb2_mem_ops vb2_vmalloc_memops = {
......
.get_userptr = vb2_vmalloc_get_userptr,
......
};

int uvcg_queue_init(struct uvc_video_queue *queue, enum v4l2_buf_type type,
struct mutex *lock)
{
int ret;

// ...... 省略部分代码
queue->queue.ops = &uvc_queue_qops; // 初始化 vb2_ops

//......

queue->queue.mem_ops = &vb2_vmalloc_memops; // 初始化 vb2_mem_ops

//......

ret = vb2_queue_init(&queue->queue);

// ......

return 0;
}

    vb2_vmalloc_get_userptr 会创建 vb2_vmalloc_buf 用来保存内存的内核虚拟地址, 如果是 mmap 则在申请内存操作 VIDIOC_REQBUFS 时分配, 用户分配则挪到了这里.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
static void *vb2_vmalloc_get_userptr(struct device *dev, unsigned long vaddr,
unsigned long size,
enum dma_data_direction dma_dir)
{
struct vb2_vmalloc_buf *buf;
struct frame_vector *vec;
int n_pages, offset, i;
int ret = -ENOMEM;

buf = kzalloc(sizeof(*buf), GFP_KERNEL);
if (!buf)
return ERR_PTR(-ENOMEM);

buf->dma_dir = dma_dir;
offset = vaddr & ~PAGE_MASK;
buf->size = size;
// ...... 省略部分代码

if (frame_vector_to_pages(vec) < 0) {
buf->vaddr = (__force void *)ioremap_nocache(__pfn_to_phys(nums[0]), size + offset);
} else {
buf->vaddr = vm_map_ram(frame_vector_pages(vec), n_pages, -1, PAGE_KERNEL);
}

buf->vaddr += offset;

return buf;
}

4) buf_init

    如果一直没有分配 mem_priv 则会尝试调用 buf_init, 没看到有啥驱动用到, 不做分析.

5) attach_dmabuf 和 map_dmabuf

    dma-buf 中用获取 dma-buf 内存的回调接口. 这里也不详细描述. 后面可能会补充.

6) start_streaming

    尝试开始取流, 如果 q->streaming 设置了且检测到 queued_count 小于 min_buffers_needed, 则尝试触发取流操作.

7) 总结

  • VIDIOC_QBUF 参数用于将缓冲区放入队列, 分三种情况 mmap、 user 和 dma-buf. 分别对应三种内存分配的方式. 不同的方式需要实现的回调不同.
  • 用户空间只需要提供内存的分配方式, 以及 buf.index. 缓冲区的分配要通过 VIDIOC_REQBUFS 来实现. 简单的操作就是分配之后用 VIDIOC_QUERYBUF 拿到分配的 vb2_buffer. 用这个 buffer 来实现 VIDIOC_QBUF 操作
  • 内核会更新用户传入的 vb2_buffer 信息, 同步为内核的 vb2_buffer.
  • vb->queued_entry 链接到 &q->queued_list, 同时增加 q->queued_count 的引用计数. 链表 q->queued_list 就是等待处理 buffer 的链表.

7. 启动视频流

1
2
3
4
5
if (ioctl(fd, VIDIOC_STREAMON, &buf.type) == -1) {
perror("启动视频流失败");
close(fd);
return 1;
}

调用流程如下.

1) buf_queue

    在启动视频流前遍历需要处理的 buffer, 做启动前的预处理. 部分驱动在这里获取 vb2_buffer 的地址给到 carme 控制器.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static struct vb2_ops rkcif_vb2_ops = {
// ......
.wait_prepare = vb2_ops_wait_prepare,
// ......
};

static void rkcif_buf_queue(struct vb2_buffer *vb)
{

// ......
for (i = 0; i < fmt->mplanes; i++) {
void *addr = vb2_plane_vaddr(vb, i); // 获取 vb2_buffer 的虚拟地址

if (hw_dev->iommu_en) {
struct sg_table *sgt = vb2_dma_sg_plane_desc(vb, i);

// 填充到 carmera 控制器的 buffer
cifbuf->buff_addr[i] = sg_dma_address(sgt->sgl);
} else {
cifbuf->buff_addr[i] = vb2_dma_contig_plane_dma_addr(vb, i);
}

}
// ......

2) vidioc_streamon

    启动视频流的回调接口, 可以直接实现启动功能, 参考 uvc 驱动的实现

1
2
3
4
5
const struct v4l2_ioctl_ops uvc_v4l2_ioctl_ops = {
//...
.vidioc_streamon = uvc_v4l2_streamon,
// ...
};

    或者这里使用内核的默认实现 vb2_ioctl_streamon, 然后在实现 start_streaming 作为驱动真正的流启动接口. 以 vimc 为例, 如下所示.

1
2
3
4
5
6
7
8
9
10
11
static const struct v4l2_ioctl_ops vimc_cap_ioctl_ops = {
......
.vidioc_streamon = vb2_ioctl_streamon,
......
};

static const struct vb2_ops vimc_cap_qops = {
// ......
.start_streaming = vimc_cap_start_streaming,
// ......
};

    这里可以写寄存器启动 sensor 也可以通过 v4l2_subdev_call 接口调用 senor 的 subdev 对应的启动接口启动.

3. vb2_buffer_done

    如果 buffer 已经处理完了, 则唤醒 done_wq 将 buffer 返回给用户空间.

8. 从队列中取出缓冲

1
2
3
4
5
if (ioctl(fd, VIDIOC_DQBUF, &buf) == -1) {
perror("从队列中取出缓冲区失败");
close(fd);
return 1;
}

调用流程如下所示.

从缓冲区冲取出队列的关键函数是 __vb2_wait_for_done_vb, 他的功能如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
static int __vb2_wait_for_done_vb(struct vb2_queue *q, int nonblocking)
{

for (;;) {
int ret;

if (q->waiting_in_dqbuf) { // 当前有其他进程在等待缓冲区, 则直接返回
return -EBUSY;
}

if (!q->streaming) { // 视频流停止了也直接返回
return -EINVAL;
}

if (q->error) { // 队列中有错误也直接返回
return -EIO;
}

if (q->last_buffer_dequeued) { // 最后一个缓冲区出队返回
return -EPIPE;
}

if (!list_empty(&q->done_list)) { // 如果已经有完成的缓冲区退出
break;
}

if (nonblocking) { // 没有缓冲区直接返回
return -EAGAIN;
}

q->waiting_in_dqbuf = 1; // 设置占用该 vb2_queue


call_void_qop(q, wait_prepare, q); // 回调 vb2_queue->ops->wait_prepare 加锁

// 进入等待, 等待缓冲区准备好.
dprintk(3, "will sleep waiting for buffers\n");
ret = wait_event_interruptible(q->done_wq,
!list_empty(&q->done_list) || !q->streaming ||
q->error);

call_void_qop(q, wait_finish, q); // 回调 vb2_queue->ops->wait_finish 解锁
q->waiting_in_dqbuf = 0; // 释放占用
}
return 0;
}
  • 当前有其他进程在等待缓冲区 q->waiting_in_dqbuf视频流停止了 !q->streaming队列中有错误q->error最后一个缓冲区已经出队 q->last_buffer_dequeued都立即返回.
  • 调用 wait_prepare 默认实现为 vb2_ops_wait_prepare 为 vb2_queue 加锁. 设置 q->waiting_in_dqbuf 表示当前进程占用该 vb2_queue.
  • wait_event_interruptible 进入等待, 当 buffer 准备好会触发中断, 在中断中调用 vb2_buffer_done 唤醒 q->done_wq 队列返回.
  • 调用 wait_finish 默认实现为 vb2_ops_wait_finish 为 vb2_queue 解锁. 清除 q->waiting_in_dqbuf 释放前进程占用 vb2_queue.

9. mmap 映射内存

1
2
void *buffer_start;
buffer_start = mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, buf.m.offset);

    映射内存就是通过 buf.m.offset 找到对应的 vb2_plane , 然后映射其内存到用户空间.内核为我们提供了默认实现, 调用流程如下所示.

这里贴一下关键代码, 这部分内存主要是内存映射想关, 不再这篇文章过多赘述.

1
2
3
4
vb2_vmalloc_mmap() -->
remap_vmalloc_range() --> // 这里就把内核的虚拟内存映射到用户空间了
vma->vm_start = (unsigned long)(addr + (pgoff << PAGE_SHIFT));
vma->vm_end = vma->vm_start + size;

10. 停止视频流

1
2
3
4
5
if (ioctl(fd, VIDIOC_STREAMOFF, &buf.type) == -1) {
perror("停止视频流失败");
close(fd);
return 1;
}

调用流程如下.

和启动流相同, 可以在 ops->vidioc_streamoff 中就直接实现停止操作, 例如 uvc

1
2
3
4
5
const struct v4l2_ioctl_ops uvc_v4l2_ioctl_ops = {
// ......
.vidioc_streamoff = uvc_v4l2_streamoff,
// ......
};

也可以调用通用的接口 vb2_ioctl_streamoff, 然后在实现 vb2_queue->ops->stop_streaming 接口用于真正的停止操作. 如下所示

1
2
3
4
5
6
7
8
9
10
11
static const struct v4l2_ioctl_ops vpif_ioctl_ops = {
// ......
.vidioc_streamoff = vb2_ioctl_streamoff, // 调通通用接口
// ......
};

static const struct vb2_ops video_qops = {
// ......
.stop_streaming = vpif_stop_streaming, // 真正的停止接口
// ......
};

11. 总结

    完整的整理整个 /dev/videox 的操作流程可以发现, 其实就是对 vb2_buffer 的操作. 它聚焦于视频的流处理.下图完整的展示了一帧图像的处理流程(这图对应 mmap). 的这些步骤已近是很精简的步骤了.

知识回顾: 每一帧图像用一个 vb2_buffer 描述, 一帧图像可以由一个或者多个 vb2_plane 组成, 每一个 vb2_plane 有一个 mem_priv 指向一个数据结构, 该结构用于保存实际分配的内存, videobuf2 为我们提供了一个数据结构 vb2_vmalloc_buf

驱动需要实现的接口如下:

接口 说明
vb2_queue->ops->queue_setup 用于确认 vb2_plane 的数量和大小。
vb2_queue->mem_ops->alloc 用于分配一帧图像传输需要的内存。
vb2_queue->buf_ops->fill_user_buffer 用于将 vb2_buffer 返回给用户空间。
vb2_queue->ops->buf_prepare 获取前面分配的 vb2_buffer 的 vb2_plane 中虚拟地址,
把它传给 camera 控制器,或者对 plane 做预处理。
它和 vb2_queue->ops->buf_queue 是二选其一
如果在 buf_queue 中获取,这里就不用获取。
vb2_queue->ops->buf_queue 获取前面分配的 vb2_buffer 的 vb2_plane 中虚拟地址,
把它传给 camera 控制器。
它和 vb2_queue->ops->buf_prepare 是二选其一
如果在 buf_prepare 中获取,这里就不用获取。
vb2_queue->ops->start_streaming 启动摄像头传输。如果这里使用 vb2_ioctl_streamon
系统接口则需要实现 vb2_queue->ops->start_streaming
vb2_queue->ops->start_streaming 如果前面的 vb2_queue->ops->start_streaming
使用了 vb2_ioctl_streamon 接口则需要实现这个接口。
vb2_queue->mem_ops->mmap 用于映射内核虚拟地址到用户空间
一般使用默认实现 vb2_vmalloc_mmap

二、 实现一个虚拟摄像头

    有了前面的知识, 我们来逐步实现一个支持 mmap 的虚拟摄像头.为了减少代码的冗余, 除了第一个程序外, 后续的修改将以 patch 的形式给出. 这样也方便观察修改了哪些地方.

1. 最简单的虚拟摄像头

    首先是实现一个简单的摄像头驱动, 它只需要具备 /dev/videox 节点就行了.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#include <linux/videodev2.h>
#include <media/v4l2-device.h>

static struct v4l2_device v4l2_dev;
static struct video_device vcam_vdev;

static int vcam_open(struct file *file) {
printk(KERN_INFO "simple_vcam: device opened\n");
return 0;
}

static int vcam_release(struct file *file) {
printk(KERN_INFO "simple_vcam: device closed\n");
return 0;
}

static const struct v4l2_file_operations vcam_fops = {
.owner = THIS_MODULE,
.open = vcam_open,
.release = vcam_release,
};

static int __init vcam_init(void) {
int ret;

printk(KERN_INFO "simple_vcam: initializing\n");

// 当第一个参数为 NULL 时必须设置 name
snprintf(v4l2_dev.name, sizeof(v4l2_dev.name), "%s", "vcam v4l2 dev");
ret = v4l2_device_register(NULL, &v4l2_dev);
if (ret) {
printk(KERN_ERR "simple_vcam: v4l2_device_register failed\n");
return ret;
}

// 初始化 vcam_vdev
strscpy(vcam_vdev.name, "Simple Virtual Camera", sizeof(vcam_vdev.name));
vcam_vdev.v4l2_dev = &v4l2_dev; // 必须设置所属 v4l2_dev
vcam_vdev.fops = &vcam_fops; // 设置 fops
vcam_vdev.release = video_device_release_empty; // 必须设置 relase 接口
vcam_vdev.vfl_dir = VFL_DIR_RX;
vcam_vdev.device_caps = V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_READWRITE;

// 注册 video device.
ret = video_register_device(&vcam_vdev, VFL_TYPE_GRABBER, -1);
if (ret) {
printk(KERN_ERR "simple_vcam: video_register_device failed\n");
v4l2_device_unregister(&v4l2_dev);
return ret;
}

printk(KERN_INFO "simple_vcam: registered video device /dev/video%d\n", vcam_vdev.minor);
return 0;
}

static void __exit vcam_exit(void) {
printk(KERN_INFO "simple_vcam: exiting\n");
video_unregister_device(&vcam_vdev);
v4l2_device_unregister(&v4l2_dev);
}

module_init(vcam_init);
module_exit(vcam_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("baron");
MODULE_DESCRIPTION("Simple virtual camera driver example");

验证程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
int fd;

// 打开设备 /dev/video9(根据实际情况可能是 /dev/videoX)
fd = open("/dev/video9", O_RDWR);
if (fd == -1) {
perror("打开设备失败");
return -1;
}

printf("设备已打开成功!\n");

// 关闭设备
close(fd);
printf("设备已关闭。\n");

return 0;
}

验证结果:

1
2
3
4
5
console:/cache # ./mytest
[ 1777.977139] simple_vcam: device opened
camera open
camera close
console:/cache # [ 1777.977359] simple_vcam: device closed

2. 支持查询设备能力

驱动修改如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
Index: kernel/drivers/media/my_camera.c
===================================================================
--- kernel.orig/drivers/media/my_camera.c
+++ kernel/drivers/media/my_camera.c
@@ -6,6 +6,7 @@
#include <linux/uaccess.h>
#include <linux/videodev2.h>
#include <media/v4l2-device.h>
+#include <media/v4l2-ioctl.h>

static struct v4l2_device v4l2_dev;
static struct video_device vcam_vdev;
@@ -20,8 +21,34 @@ static int vcam_release(struct file *fil
return 0;
}

+// 驱动需要实现 querycap 用于返回 video 信息
+// cap->driver: 设备的驱动信息
+// cap->card: 设备的名称
+// cap->bus_info: 设备的总线信息
+static int vcam_v4l2_querycap(struct file *file, void *fh, struct v4l2_capability *cap)
+{
+ struct video_device *vdev = video_devdata(file);
+
+ strlcpy(cap->driver, "virtual_vcam", sizeof(cap->driver));
+ strlcpy(cap->card, "Virtual Camera", sizeof(cap->card));
+ strlcpy(cap->bus_info, vdev->name, sizeof(vdev->name));
+
+ cap->device_caps = V4L2_CAP_VIDEO_OUTPUT | V4L2_CAP_STREAMING;
+ cap->capabilities = cap->device_caps | V4L2_CAP_DEVICE_CAPS;
+
+ return 0;
+}
+
+const struct v4l2_ioctl_ops vcam_v4l2_ioctl_ops = {
+ .vidioc_querycap = vcam_v4l2_querycap, // 实现 vidioc_querycap 用于支持 VIDIOC_QUERYCAP
+};
+
static const struct v4l2_file_operations vcam_fops = {
.owner = THIS_MODULE,
+ .unlocked_ioctl = video_ioctl2, // 增加 ioctl 的支持
+#ifdef CONFIG_COMPAT
+ .compat_ioctl32 = video_ioctl2, // 增加 ioctl 的支持
+#endif
.open = vcam_open,
.release = vcam_release,
};
@@ -43,6 +70,7 @@ static int __init vcam_init(void) {
strscpy(vcam_vdev.name, "Simple Virtual Camera", sizeof(vcam_vdev.name));
vcam_vdev.v4l2_dev = &v4l2_dev; // 必须设置所属 v4l2_dev
vcam_vdev.fops = &vcam_fops; // 设置 fops
+ vcam_vdev.ioctl_ops = &vcam_v4l2_ioctl_ops;
vcam_vdev.release = video_device_release_empty; // 必须设置 relase 接口
vcam_vdev.vfl_dir = VFL_DIR_RX;
vcam_vdev.device_caps = V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_READWRITE;

应用修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Index: test/test.c
===================================================================
--- test.orig/test.c
+++ test/test.c
@@ -18,6 +18,17 @@ int main() {

printf("camera open\n");

+ // 1. 查询设备能力
+ struct v4l2_capability cap;
+ if (ioctl(fd, VIDIOC_QUERYCAP, &cap) == -1) {
+ perror("查询设备能力失败");
+ close(fd);
+ return 1;
+ }
+ printf("card: %s\n", cap.card);
+ printf("driver: %s\n", cap.driver);
+ printf("bus_info: %s\n", cap.bus_info);
+
// 关闭设备
close(fd);
printf("camera close\n");

验证结果:

1
2
3
4
5
6
7
8
console:/cache # ./mytest
[ 888.990438] simple_vcam: device opened
camera open
card: Virtual Camera // 打印card 信息
driver: virtual_vcam // 打印 drvier 信息
bus_info: Simple Virtual Camera // 打印总线信息
camera close
console:/cache # [ 888.996537] simple_vcam: device closed

3. 支持申请 buffer 的能力

从前面的文章可以知道 camera 的 buffer 通过 vb2_queue 来管理, 因此我们需要添加对它的支持.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
Index: kernel/drivers/media/my_camera.c
===================================================================
--- kernel.orig/drivers/media/my_camera.c
+++ kernel/drivers/media/my_camera.c
@@ -7,9 +7,15 @@
#include <linux/videodev2.h>
#include <media/v4l2-device.h>
#include <media/v4l2-ioctl.h>
+#include <linux/spinlock.h>
+#include <media/videobuf2-core.h>
+#include <media/videobuf2-v4l2.h>
+#include <media/videobuf2-vmalloc.h>

static struct v4l2_device v4l2_dev;
static struct video_device vcam_vdev;
+static struct vb2_queue vcam_queue;
+static struct mutex vcam_mutex;

static int vcam_open(struct file *file) {
printk(KERN_INFO "simple_vcam: device opened\n");
@@ -21,7 +27,7 @@ static int vcam_release(struct file *fil
return 0;
}

-// 驱动需要实现 querycap 用于返回 video 信息
+// 用于支持查询设备的能力
// cap->driver: 设备的驱动信息
// cap->card: 设备的名称
// cap->bus_info: 设备的总线信息
@@ -39,8 +45,41 @@ static int vcam_v4l2_querycap(struct fil
return 0;
}

+// 分配 buffer 时驱动的回调接口, 这里使用通用的 vb2_reqbufs 实现
+// 虚拟摄像头这里就随便填写了.
+static int vcam_v4l2_reqbufs(struct file *file, void *fh, struct v4l2_requestbuffers *b)
+{
+ int ret;
+
+ if (b->type != vcam_queue.type)
+ return -EINVAL;
+
+ printk("%s\n", __func__);
+
+ ret = vb2_reqbufs(&vcam_queue, b);
+
+ return ret ? ret : b->count;
+}
+
+// V4L2_BUF_TYPE_VIDEO_CAPTURE 必须实现这个接口用于返回支持的视频格式.
+static int vcam_v4l2_g_fmt_vid_cap(struct file *file, void *fh, struct v4l2_format *f)
+{
+ f->fmt.pix.width = 400;
+ f->fmt.pix.height = 800;
+ f->fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
+ f->fmt.pix.field = V4L2_FIELD_NONE;
+ f->fmt.pix.bytesperline = f->fmt.pix.width * 2;
+ f->fmt.pix.sizeimage = f->fmt.pix.height * f->fmt.pix.bytesperline;
+ f->fmt.pix.colorspace = V4L2_COLORSPACE_SRGB;
+ f->fmt.pix.priv = 0;
+
+ return 0;
+}
+
const struct v4l2_ioctl_ops vcam_v4l2_ioctl_ops = {
- .vidioc_querycap = vcam_v4l2_querycap, // 实现 vidioc_querycap 用于支持 VIDIOC_QUERYCAP
+ .vidioc_querycap = vcam_v4l2_querycap, // 用于返回设备信息
+ .vidioc_reqbufs = vcam_v4l2_reqbufs, // 用于支持分配 vb2_buffer
+ .vidioc_g_fmt_vid_cap = vcam_v4l2_g_fmt_vid_cap, // 用于支持返回驱动支持的视频格式
};

static const struct v4l2_file_operations vcam_fops = {
@@ -53,6 +92,48 @@ static const struct v4l2_file_operations
.release = vcam_release,
};

+// 用于设置 vb2_plane 的数量和大小
+static int vcam_queue_setup(struct vb2_queue *vq,
+ unsigned int *nbuffers, unsigned int *nplanes,
+ unsigned int sizes[], struct device *alloc_devs[])
+{
+ printk("%s\n", __func__);
+ *nplanes = 1; // 设置数量
+ sizes[0] = 800*400; // 设置大小
+ return 0;
+}
+
+static void vcam_buffer_queue(struct vb2_buffer *vb)
+{
+ printk("%s\n", __func__);
+}
+
+static struct vb2_ops vcam_queue_qops = {
+ .queue_setup = vcam_queue_setup, // 用于设置 vb2_plane 的数量和大小
+ .buf_queue = vcam_buffer_queue, // 必须实现
+};
+
+// 初始化 vb2_queue
+int vcam_queue_init(struct vb2_queue *queue, enum v4l2_buf_type type,
+ struct mutex *lock)
+{
+ int ret;
+
+ queue->type = type;
+ queue->io_modes = VB2_MMAP | VB2_USERPTR | VB2_DMABUF;
+ queue->ops = &vcam_queue_qops;
+ queue->lock = lock;
+ queue->mem_ops = &vb2_vmalloc_memops;
+ queue->timestamp_flags = V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC
+ | V4L2_BUF_FLAG_TSTAMP_SRC_EOF;
+
+ ret = vb2_queue_init(queue);
+ if (ret)
+ return ret;
+
+ return 0;
+}
+
static int __init vcam_init(void) {
int ret;

@@ -83,6 +164,9 @@ static int __init vcam_init(void) {
return ret;
}

+ // 初始化 vb2_queue
+ vcam_queue_init(&vcam_queue, V4L2_BUF_TYPE_VIDEO_CAPTURE, &vcam_mutex);
+
printk(KERN_INFO "simple_vcam: registered video device /dev/video%d\n", vcam_vdev.minor);
return 0;
}

应用程序修改.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Index: test/test.c
===================================================================
--- test.orig/test.c
+++ test/test.c
@@ -29,6 +29,19 @@ int main() {
printf("driver: %s\n", cap.driver);
printf("bus_info: %s\n", cap.bus_info);

+ // 2. 申请缓冲区
+ struct v4l2_requestbuffers req;
+ req.count = 3;
+ req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+ req.memory = V4L2_MEMORY_MMAP;
+ if (ioctl(fd, VIDIOC_REQBUFS, &req) == -1) {
+ perror("申请缓冲区失败");
+ close(fd);
+ return 1;
+ }
+
+ printf("alloc 3 buffer\n");
+
// 关闭设备
close(fd);
printf("camera close\n");

验证结果:

1
2
3
4
5
6
7
8
9
10
11
console:/cache # ./mytest
[ 785.535608] simple_vcam: device opened
camera open
card: Virtual Camera
driver: virtual_vcam
bus_info: Simple Virtual Camera
alloc 3 buffer
camera close
[ 785.541709] vcam_v4l2_reqbufs
[ 785.541882] vcam_queue_setup
[ 785.543306] simple_vcam: device closed

4. 支持查询缓冲区信息

这个驱动需要添加对 vidioc_querybuf 的支持.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Index: kernel/drivers/media/my_camera.c
===================================================================
--- kernel.orig/drivers/media/my_camera.c
+++ kernel/drivers/media/my_camera.c
@@ -76,10 +76,17 @@ static int vcam_v4l2_g_fmt_vid_cap(struc
return 0;
}

+// 添加驱动用于支持查询缓冲区信息的功能
+static int vcam_v4l2_querybuf(struct file *file, void *fh, struct v4l2_buffer *b)
+{
+ return vb2_querybuf(&vcam_queue, b);
+}
+
const struct v4l2_ioctl_ops vcam_v4l2_ioctl_ops = {
.vidioc_querycap = vcam_v4l2_querycap, // 用于返回设备信息
.vidioc_reqbufs = vcam_v4l2_reqbufs, // 用于支持分配 vb2_buffer
.vidioc_g_fmt_vid_cap = vcam_v4l2_g_fmt_vid_cap, // 用于支持返回驱动支持的视频格式
+ .vidioc_querybuf = vcam_v4l2_querybuf, // 添加驱动用于支持查询缓冲区信息的功能
};

static const struct v4l2_file_operations vcam_fops = {

应用程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Index: test/test.c
===================================================================
--- test.orig/test.c
+++ test/test.c
@@ -42,6 +42,18 @@ int main() {

printf("alloc 3 buffer\n");

+ // 3. 查询缓冲区信息
+ struct v4l2_buffer buf;
+ buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+ buf.memory = V4L2_MEMORY_MMAP;
+ buf.index = 0;
+ if (ioctl(fd, VIDIOC_QUERYBUF, &buf) == -1) {
+ perror("查询缓冲区信息失败");
+ close(fd);
+ return 1;
+ }
+ printf("缓冲区大小: %u bytes\n", buf.length);
+
// 关闭设备
close(fd);
printf("camera close\n");

验证结果

1
2
3
4
5
6
7
8
9
10
11
12
console:/cache # ./mytest
[ 182.823177] simple_vcam: device opened
camera open
card: Virtual Camera
driver: virtual_vcam
bus_info: Simple Virtual Camera
缓冲区大小: 320000 bytes // 计算一下 800x400 = 320000
alloc 3 buffer
camera close
[ 182.829223] vcam_v4l2_reqbufs
[ 182.829240] vcam_queue_setup
[ 182.833253] simple_vcam: device closed

5. 支持 mmap 功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Index: kernel/drivers/media/my_camera.c
===================================================================
--- kernel.orig/drivers/media/my_camera.c
+++ kernel/drivers/media/my_camera.c
@@ -89,13 +89,20 @@ const struct v4l2_ioctl_ops vcam_v4l2_io
.vidioc_querybuf = vcam_v4l2_querybuf, // 添加驱动用于支持查询缓冲区信息的功能
};

+// 添加对 mmap 的支持
+static int vcam_v4l2_mmap(struct file *file, struct vm_area_struct *vma)
+{
+ return vb2_mmap(&vcam_queue, vma);
+}
+
static const struct v4l2_file_operations vcam_fops = {
.owner = THIS_MODULE,
+ .open = vcam_open,
.unlocked_ioctl = video_ioctl2, // 增加 ioctl 的支持
#ifdef CONFIG_COMPAT
.compat_ioctl32 = video_ioctl2, // 增加 ioctl 的支持
#endif
- .open = vcam_open,
+ .mmap = vcam_v4l2_mmap, // 添加对 mmap 的支持
.release = vcam_release,
};

应用程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Index: test/test.c
===================================================================
--- test.orig/test.c
+++ test/test.c
@@ -5,6 +5,7 @@
#include <sys/ioctl.h>
#include <linux/videodev2.h>
#include <errno.h>
+#include <sys/mman.h>

int main() {
int fd;
@@ -54,6 +55,16 @@ int main() {
}
printf("缓冲区大小: %u bytes\n", buf.length);

+ // 4. mmap 映射内存
+ void *buffer_start;
+ buffer_start = mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, buf.m.offset);
+ if (buffer_start == MAP_FAILED) {
+ perror("mmap");
+ exit(EXIT_FAILURE);
+ }
+
+ printf("mmap camera bufer %p\n", buffer_start);
+
// 关闭设备
close(fd);
printf("camera close\n");

验证结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
console:/cache # ./mytest
[ 55.868859] simple_vcam: device opened
camera open
card: Virtual Camera
driver: virtual_vcam
bus_info: Simple Virtual Camera
alloc 3 buffer
缓冲区大小: 320000 bytes
mmap camera bufer 0x78780ac000 // 映射内存
camera close
[ 55.874932] vcam_v4l2_reqbufs
[ 55.874953] vcam_queue_setup
[ 55.884138] simple_vcam: device closed

6. 支持将队列放入缓冲区

我们在将 buffer 放入缓冲区前, 先给他写入 “hello vcam this is user” 然后在内核中打印出我们的修改.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
Index: kernel/drivers/media/my_camera.c
===================================================================
--- kernel.orig/drivers/media/my_camera.c
+++ kernel/drivers/media/my_camera.c
@@ -16,6 +16,8 @@ static struct v4l2_device v4l2_dev;
static struct video_device vcam_vdev;
static struct vb2_queue vcam_queue;
static struct mutex vcam_mutex;
+static void *vcam_mem;
+static unsigned int length;

static int vcam_open(struct file *file) {
printk(KERN_INFO "simple_vcam: device opened\n");
@@ -87,6 +89,7 @@ const struct v4l2_ioctl_ops vcam_v4l2_io
.vidioc_reqbufs = vcam_v4l2_reqbufs, // 用于支持分配 vb2_buffer
.vidioc_g_fmt_vid_cap = vcam_v4l2_g_fmt_vid_cap, // 用于支持返回驱动支持的视频格式
.vidioc_querybuf = vcam_v4l2_querybuf, // 添加驱动用于支持查询缓冲区信息的功能
+ .vidioc_qbuf = vb2_ioctl_qbuf, // 用于支持将缓冲区放入队列
};

// 添加对 mmap 的支持
@@ -122,9 +125,23 @@ static void vcam_buffer_queue(struct vb2
printk("%s\n", __func__);
}

+
+// 获取前面分配的 buffer 的虚拟地址
+// 打印出用户的修改, 这里应该能打印 "hello vcam this is user"
+static int vcam_buffer_prepare(struct vb2_buffer *vb)
+{
+ vcam_mem = vb2_plane_vaddr(vb, 0);
+ length = vb2_plane_size(vb, 0);
+
+ printk("vcam_mem: %s, length:%d\n", (char*)vcam_mem, length);
+
+ return 0;
+}
+
static struct vb2_ops vcam_queue_qops = {
.queue_setup = vcam_queue_setup, // 用于设置 vb2_plane 的数量和大小
.buf_queue = vcam_buffer_queue, // 必须实现
+ .buf_prepare = vcam_buffer_prepare, // 用于获取前面分配的 buffer 的虚拟地址
};

// 初始化 vb2_queue
@@ -179,6 +196,7 @@ static int __init vcam_init(void) {
}

// 初始化 vb2_queue
+ vcam_vdev.queue = &vcam_queue; // vb2_ioctl_qbuf 需要把 queue 绑定到 video_device
vcam_queue_init(&vcam_queue, V4L2_BUF_TYPE_VIDEO_CAPTURE, &vcam_mutex);

printk(KERN_INFO "simple_vcam: registered video device /dev/video%d\n", vcam_vdev.minor);

应用程序修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Index: test/test.c
===================================================================
--- test.orig/test.c
+++ test/test.c
@@ -6,6 +6,7 @@
#include <linux/videodev2.h>
#include <errno.h>
#include <sys/mman.h>
+#include <string.h>

int main() {
int fd;
@@ -65,6 +66,18 @@ int main() {

printf("mmap camera bufer %p\n", buffer_start);

+ // 往映射内存中写入数据
+ const char *msg = "hello vcam this is user";
+ memcpy(buffer_start, msg, strlen(msg) + 1);
+
+ printf("write %s to kernel\n", (char*)buffer_start);
+
+ if (ioctl(fd, VIDIOC_QBUF, &buf) == -1) {
+ perror("将缓冲区放入队列失败");
+ close(fd);
+ return 1;
+ }
+
// 关闭设备
close(fd);
printf("camera close\n")

验证结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
console:/cache # ./mytest
[ 33.442391] simple_vcam: decvaimceer ao poepneend

card: Virtual Camera
driver: virtual_vcam
bus_info: Simple Virtual Camera
alloc 3 buffer
缓冲区大小: 320000 bytes
mmap camera bufer 0x7ef2c21000
write hello vcam this is user to kernel // 应用打印
camera close
[ 33.448609] vcam_queue_setup
[ 33.458952] vcam_mem: hello vcam this is user, length:320000 // 在内核中打印出应用写入的数据.
[ 33.461500] simple_vcam: device closed

7. 添加启动视频流功能

添加启动视频流的功能, 并且在启动的时候, 模拟 sensor 处理完视频并提交这帧. 模拟的方式很简单就是往 mem 中写入 “hello vcam this is kernel”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
Index: kernel/drivers/media/my_camera.c
===================================================================
--- kernel.orig/drivers/media/my_camera.c
+++ kernel/drivers/media/my_camera.c
@@ -90,6 +90,7 @@ const struct v4l2_ioctl_ops vcam_v4l2_io
.vidioc_g_fmt_vid_cap = vcam_v4l2_g_fmt_vid_cap, // 用于支持返回驱动支持的视频格式
.vidioc_querybuf = vcam_v4l2_querybuf, // 添加驱动用于支持查询缓冲区信息的功能
.vidioc_qbuf = vb2_ioctl_qbuf, // 用于支持将缓冲区放入队列
+ .vidioc_streamon = vb2_ioctl_streamon, // 使用标准的 vb2_ioctl_streamon 接口
};

// 添加对 mmap 的支持
@@ -138,10 +139,36 @@ static int vcam_buffer_prepare(struct vb
return 0;
}

+// 直接在 steram on 中 模拟数据采集完成
+static int vcam_start_streaming(struct vb2_queue *vq, unsigned int count)
+{
+ struct vb2_buffer *vb;
+ void *mem;
+
+ if (list_empty(&vq->queued_list)) {
+ printk("vcam: queued_list is empty, cannot start streaming\n");
+ return 0;
+ }
+
+ vb = list_first_entry(&vq->queued_list, struct vb2_buffer, queued_entry);
+
+ // 模拟采集过程, 这里直接修改 buffer 内容
+ mem = vb2_plane_vaddr(vb, 0);
+ memcpy(mem, "hello vcam this is kernel", strlen("hello vcam this is kernel") + 1);
+
+ // 采集完成将 buffer 放到 vb2_queue->done_list 链表
+ vb2_buffer_done(vb, VB2_BUF_STATE_DONE); // 将 buffer 放到完成链表
+
+ printk("%s change mem to %s\n", __func__, (char*)mem);
+
+ return 0;
+}
+
static struct vb2_ops vcam_queue_qops = {
.queue_setup = vcam_queue_setup, // 用于设置 vb2_plane 的数量和大小
.buf_queue = vcam_buffer_queue, // 必须实现
.buf_prepare = vcam_buffer_prepare, // 用于获取前面分配的 buffer 的虚拟地址
+ .start_streaming = vcam_start_streaming, // vb2_ioctl_streamon 的后续接口, 这个是真正的 stream on 接口
};

应用修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Index: test/test.c
===================================================================
--- test.orig/test.c
+++ test/test.c
@@ -72,12 +72,22 @@ int main() {

printf("write %s to kernel\n", (char*)buffer_start);

+ // 5. 将缓冲区放入队列
if (ioctl(fd, VIDIOC_QBUF, &buf) == -1) {
perror("将缓冲区放入队列失败");
close(fd);
return 1;
}

+ // 6. 启动视频流
+ if (ioctl(fd, VIDIOC_STREAMON, &buf.type) == -1) {
+ perror("启动视频流失败");
+ close(fd);
+ return 1;
+ }
+
+ printf("vcam stream on\n");
+
// 关闭设备
close(fd);
printf("camera close\n");

验证结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
console:/cache # ./mytest
[ 51.017063] simple_vcam: device opened
camera open
card: Virtual Camera
driver: virtual_vcam
bus_info: Simple Virtual Camera
alloc 3 buffer
缓冲区大小: 320000 bytes
mmap camera bufer 0x7a81864000
write hello vcam this is user to kernel
vcam stream on // 启动摄像头
camera close
[ 51.023179] vcam_queue_setup
[ 51.033537] vcam_mem: hello vcam this is user, length:320000
[ 51.033591] vcam_start_streaming change mem to hello vcam this is kernel // 将 mem 的内容该成 hello vcam this is kernel
[ 51.037391] simple_vcam: device closed

8. 从队列中取出缓冲

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Index: kernel/drivers/media/my_camera.c
===================================================================
--- kernel.orig/drivers/media/my_camera.c
+++ kernel/drivers/media/my_camera.c
@@ -91,6 +91,7 @@ const struct v4l2_ioctl_ops vcam_v4l2_io
.vidioc_querybuf = vcam_v4l2_querybuf, // 添加驱动用于支持查询缓冲区信息的功能
.vidioc_qbuf = vb2_ioctl_qbuf, // 用于支持将缓冲区放入队列
.vidioc_streamon = vb2_ioctl_streamon, // 使用标准的 vb2_ioctl_streamon 接口
+ .vidioc_dqbuf = vb2_ioctl_dqbuf, // 使用标准的 vb2_ioctl_dqbuf 接口
};

// 添加对 mmap 的支持
@@ -169,6 +170,8 @@ static struct vb2_ops vcam_queue_qops =
.buf_queue = vcam_buffer_queue, // 必须实现
.buf_prepare = vcam_buffer_prepare, // 用于获取前面分配的 buffer 的虚拟地址
.start_streaming = vcam_start_streaming, // vb2_ioctl_streamon 的后续接口, 这个是真正的 stream on 接口
+ .wait_prepare = vb2_ops_wait_prepare, // 使用标准接口加锁
+ .wait_finish = vb2_ops_wait_finish, // 使用标准接口解锁
};

// 初始化 vb2_queue

应用修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Index: test/test.c
===================================================================
--- test.orig/test.c
+++ test/test.c
@@ -88,6 +88,14 @@ int main() {

printf("vcam stream on\n");

+ if (ioctl(fd, VIDIOC_DQBUF, &buf) == -1) {
+ perror("从队列中取出缓冲区失败");
+ close(fd);
+ return 1;
+ }
+
+ printf("%s\n", (char*)buffer_start);
+
// 关闭设备
close(fd);
printf("camera close\n");

验证结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
console:/cache # ./mytest
[ 99.390015] simple_vcam: device opened
camera open
card: Virtual Camera
driver: virtual_vcam
bus_info: Simple Virtual Camera
alloc 3 buffer
缓冲区大小: 320000 bytes
mmap camera bufer 0x741ca9f000
write hello vcam this is user to kernel
vcam stream on
hello vcam this is kernel // 内核读出处理过后的 buffer
camera close
[ 99.396111] vcam_queue_setup
[ 99.406462] vcam_mem: hello vcam this is user, length:320000
[ 99.406512] vcam_start_streaming change mem to hello vcam this is kernel // 修改 buffer
[ 99.412631] simple_vcam: device closed

驱动完全是根据前面的文章内容完成的.