一、简介
V4L2 是 Video for Linux 2 的简称,是 Linux 系统中用于视频设备管理和应用访问的标准框架。它为摄像头、视频采集卡等视频设备提供统一的驱动接口和用户空间访问接口。
在 Linux 中,视频设备通常会被抽象为设备文件,应用程序可以像访问普通文件一样,通过 open、read、write、ioctl、mmap 等系统调用对其进行操作,从而完成视频采集、格式设置、参数控制和缓冲区管理等功能。
V4L2 框架中常见的字符设备节点主要包括两类:
1、/dev/videoX:视频设备节点,通常面向应用程序使用,用于完成视频数据采集、输出或处理等操作。
2、/dev/v4l-subdevX:V4L2 子设备节点,通常对应摄像头传感器、MIPI CSI 接收器、解码器、ISP 等子模块,主要用于配置和控制视频采集链路中的各个硬件单元。
二、核心对象
初次学习 V4L2 框架时,可能会觉得整个框架比较复杂,涉及的结构体和调用流程很多。因此,理解 V4L2 时不建议一开始就陷入所有细节,而是应该先抓住几个核心对象。对于 i.MX6ULL 的 CSI 视频采集驱动来说,可以重点关注以下 4 个核心对象。
cpp
1. struct video_device,也就是 vdev
- 它是 /dev/video0 在内核里的代表。
- CSI 驱动注册的是 vdev。
- 用户 open/ioctl/mmap/read 最终都会围绕这个 vdev 走。
2. struct mx6s_csi_dev,也就是 csi_dev
- 这是 mx6s CSI 驱动自己的私有结构。
- 里面保存 vdev、v4l2_dev、vb2_vidq、sd、pix、capture 链表等。
- video_set_drvdata(vdev, csi_dev) 后,后面可以通过 video_drvdata(file) 找回来。
3. struct v4l2_subdev,也就是 sd
- 它是 OV5640 这种 sensor 在 V4L2 里的抽象。
- CSI host 本身负责采集和 DMA。
- sensor subdev 负责上电、设置格式、设置帧率、开始/停止输出图像。
4. struct vb2_queue,也就是 csi_dev->vb2_vidq
- 它是 videobuf2 框架里的 buffer 队列管理器。
- 它不是图像内存本身,而是管理多个 vb2_buffer。
- REQBUFS、QUERYBUF、QBUF、DQBUF、STREAMON 都会访问它。
1、mx6s_csi_dev 与 v4l2_subdev 的关系
在 i.MX6ULL 的摄像头采集链路中,mx6s_csi_dev 表示 CSI 控制器驱动的私有结构,v4l2_subdev 表示 OV5640 这类 sensor 在 V4L2 框架中的抽象。设备树中,CSI 和 OV5640 一般通过 port、endpoint 和 remote-endpoint 建立连接关系:
&csi {
status = "okay";
port {
csi1_ep: endpoint {
remote-endpoint = <&ov5640_ep>;
};
};
};
&i2c2 {
status = "okay";
ov5640: ov5640@3c {
compatible = "ovti,ov5640";
status = "okay";
port {
ov5640_ep: endpoint {
remote-endpoint = <&csi1_ep>;
};
};
};
};
内核解析设备树后,&csi 会生成 CSI 控制器对应的 platform_device,并匹配 mx6s CSI 驱动;ov5640@3c 会生成 i2c_client,并匹配 OV5640 I2C 驱动。OV5640 驱动 probe 时,会创建自己的私有结构体,其中包含一个 struct v4l2_subdev 成员:
struct ov5640 {
struct v4l2_subdev subdev;
struct i2c_client *i2c_client;
struct v4l2_pix_format pix;
struct v4l2_captureparm streamcap;
...
};
驱动调用:
v4l2_i2c_subdev_init(&ov5640_data.subdev, client, &ov5640_subdev_ops);
把 ov5640_data.subdev 这个成员初始化成 V4L2 框架认识的 I2C subdev。这一步主要完成两件事:1、设置:sd->ops = &ov5640_subdev_ops。2、将 sd 和 i2c_client 互相关联。
ov5640_subdev_ops 本质上就是把 OV5640 驱动内部的 s_power、set_fmt、get_fmt、enum_mbus_code、g_parm、s_parm 等函数,按照 V4L2 subdev 的 core、video、pad 接口分类挂出来。初始化完成后,OV5640 调用:
v4l2_async_register_subdev(&ov5640_data.subdev);
告诉 V4L2 async 框架:这个 sensor subdev 已经准备好了。
CSI probe 时,驱动从自己的 csi1_ep 出发,通过 remote-endpoint 找到 ov5640_ep,再找到它的父节点 ov5640@3c。CSI 不会直接自己去拿 OV5640 的 subdev,而是构造一个 v4l2_async_subdev 等待项 asd,设置为 ov5640@3c 的 device_node,然后通过:
v4l2_async_notifier_register()
把这个等待请求交给 V4L2 async 框架。V4L2 async 框架负责匹配 CSI 的等待项和 OV5640 注册的 subdev。如果两边的 of_node 相同,就调用 CSI 的:
subdev_notifier_bound()
在这个回调里,CSI 执行:csi_dev->sd = subdev;把 OV5640 的 v4l2_subdev 指针保存到 mx6s_csi_dev 私有结构中。后面 CSI 要控制 sensor 时,就通过 csi_dev->sd 调用:
v4l2_subdev_call()
间接执行 OV5640 的 ov5640_s_power()、ov5640_set_fmt()、ov5640_g_parm() 等函数。

2、mx6s_csi_dev 与 video_device 的关系
mx6s_csi_dev 是 mx6s CSI 驱动的私有结构体。video_device,也就是 vdev,是 /dev/video0 在 V4L2 框架中的内核对象。两者关系可以理解为:
cpp
struct mx6s_csi_dev
├─ struct video_device *vdev
│ // CSI 私有结构中保存 video_device 指针
│
├─ struct v4l2_device v4l2_dev
│ // V4L2 设备管理对象
│
├─ struct vb2_queue vb2_vidq
│ // videobuf2 队列,用于管理视频 buffer
│
└─ struct v4l2_subdev *sd
// V4L2 async 匹配到的 OV5640 subdev
也就是说,mx6s_csi_dev 是 CSI 驱动自己的核心私有结构,而 video_device 是 V4L2 框架对外暴露 /dev/video0 的对象。
2.1 mx6s_csi_probe() 中创建并填充 vdev
mx6s CSI 驱动在 probe 阶段会创建并初始化 video_device:
cpp
mx6s_csi_probe()
└─ video_device_alloc()
// 分配 struct video_device
├─ vdev->v4l2_dev = &csi_dev->v4l2_dev
│ // vdev 属于这个 v4l2_device
│
├─ vdev->fops = &mx6s_csi_fops
│ // 绑定 mx6s CSI 驱动自己的文件操作
│ // open/read/mmap/poll/ioctl 等入口在这里
│
├─ vdev->ioctl_ops = &mx6s_csi_ioctl_ops
│ // 绑定 mx6s CSI 驱动自己的 V4L2 ioctl 命令表
│ // VIDIOC_QUERYCAP、REQBUFS、QBUF、DQBUF 等在这里
│
├─ vdev->queue = &csi_dev->vb2_vidq
│ // 绑定 vb2 队列
│
├─ csi_dev->vdev = vdev
│ // CSI 私有结构保存 vdev 指针
│
├─ video_set_drvdata(vdev, csi_dev)
│ // vdev 反向保存 csi_dev
│ // 后面可以通过 video_drvdata(file) 找回 csi_dev
│
└─ video_register_device(vdev, VFL_TYPE_GRABBER, -1)
// 注册 video_device
// V4L2 core 内部会注册字符设备和设备模型
这里最关键的是:
cpp
vdev->fops = &mx6s_csi_fops;
vdev->ioctl_ops = &mx6s_csi_ioctl_ops;
vdev->queue = &csi_dev->vb2_vidq;
这几项决定了用户访问 /dev/video0 时,最终会进入 mx6s CSI 驱动自己的操作函数。
2.2 video_register_device() 中注册字符设备
调用 video_register_device() 后,V4L2 core 会进一步完成字符设备注册:
cpp
video_register_device()
└─ __video_register_device()
// V4L2 core 注册 video_device
├─ vdev->cdev = cdev_alloc()
│ // 分配字符设备对象
│
├─ vdev->cdev->ops = &v4l2_fops
│ // 字符设备最外层 file_operations 是 V4L2 core 的 v4l2_fops
│
├─ cdev_add(...)
│ // 注册字符设备
│
└─ device_register(&vdev->dev)
// 注册 Linux device
// /sys/class/video4linux/video0 和 /dev/video0 都和它有关
所以,/dev/video0 对应的字符设备最外层入口并不是 mx6s CSI 驱动自己的 fops,而是 V4L2 core 的通用入口表 v4l2_fops。
2.3 V4L2 core 的通用入口表:v4l2_fops
用户访问 /dev/video0 时,第一层进入的是 V4L2 core 的通用 file_operations:
cpp
static const struct file_operations v4l2_fops = {
.owner = THIS_MODULE,
.read = v4l2_read,
.write = v4l2_write,
.open = v4l2_open,
.unlocked_ioctl = v4l2_ioctl,
};
这张表是 V4L2 core 的通用入口表。它的作用是先接收用户空间的 open、read、ioctl 等系统调用,然后再根据当前 /dev/video0 对应的 video_device,转发到具体驱动的 vdev->fops。
2.4 mx6s CSI 驱动自己的文件操作表:mx6s_csi_fops
对 mx6s CSI 来说,vdev->fops 指向的是:
static struct v4l2_file_operations mx6s_csi_fops = {
.owner = THIS_MODULE,
.open = mx6s_csi_open,
.unlocked_ioctl = video_ioctl2,
};
也就是说,V4L2 core 收到用户操作后,会继续转发到 mx6s CSI 驱动自己的 mx6s_csi_fops。
2.5 open() 调用流程
用户执行:open("/dev/video0")。整体调用流程如下:
cpp
用户 open("/dev/video0")
└─ v4l2_fops.open = v4l2_open
// 先进入 V4L2 core
├─ vdev = video_devdata(file)
│ // 通过 minor 找到对应的 video_device
│
└─ vdev->fops->open(file)
// 转发到 mx6s CSI 驱动
└─ mx6s_csi_open(file)
// 初始化 vb2 队列、sensor 上电、初始化 CSI 等
所以 open() 的入口层次是:
cpp
用户空间 open()
↓
V4L2 core: v4l2_open()
↓
mx6s CSI: mx6s_csi_open()
2.6 ioctl() 调用流程
ioctl() 流程是重点。例如用户执行:ioctl(fd, VIDIOC_REQBUFS, &req)。整体调用流程如下:
cpp
用户 ioctl(fd, VIDIOC_REQBUFS, &req)
└─ v4l2_fops.unlocked_ioctl = v4l2_ioctl
// 第一层进入 V4L2 core
└─ vdev->fops->unlocked_ioctl(file, cmd, arg)
// 对 mx6s 来说是:
// mx6s_csi_fops.unlocked_ioctl = video_ioctl2
└─ video_ioctl2(file, cmd, arg)
// V4L2 通用 ioctl 分发器
└─ __video_do_ioctl()
// 根据 VIDIOC_xxx 查 v4l2_ioctls[] 分发表
├─ 找到 VIDIOC_REQBUFS 对应:
│ IOCTL_INFO_FNC(VIDIOC_REQBUFS, v4l_reqbufs, ...)
│
├─ 调用 v4l_reqbufs()
│ // V4L2 core 做通用检查
│
└─ ops->vidioc_reqbufs(file, fh, p)
// ops 就是 vdev->ioctl_ops
// 对 mx6s 来说就是 mx6s_csi_ioctl_ops
└─ mx6s_vidioc_reqbufs()
// 最终进入 mx6s CSI 驱动函数
所以 ioctl() 的调用链可以简化理解为:
cpp
用户 ioctl()
↓
V4L2 core: v4l2_ioctl()
↓
mx6s CSI fops: video_ioctl2()
↓
V4L2 core 根据 VIDIOC_xxx 分发
↓
mx6s CSI ioctl_ops: mx6s_vidioc_xxx()
2.7 mx6s CSI 的 ioctl 操作表
mx6s CSI 驱动自己的 ioctl 操作表如下:
static const struct v4l2_ioctl_ops mx6s_csi_ioctl_ops = {
.vidioc_querycap = mx6s_vidioc_querycap,
.vidioc_reqbufs = mx6s_vidioc_reqbufs,
.vidioc_querybuf = mx6s_vidioc_querybuf,
.vidioc_qbuf = mx6s_vidioc_qbuf,
.vidioc_dqbuf = mx6s_vidioc_dqbuf,
.vidioc_streamon = mx6s_vidioc_streamon,
.vidioc_streamoff = mx6s_vidioc_streamoff,
};
2.8 三张操作表的区别
可以把这三张表这样区分:
cpp
v4l2_fops
// V4L2 core 的 file_operations
// 挂在 vdev->cdev->ops 上
// 用户 open/read/mmap/ioctl 第一层先进这里
mx6s_csi_fops
// mx6s CSI 驱动自己的 v4l2_file_operations
// 挂在 vdev->fops 上
// V4L2 core 再转发到这里
mx6s_csi_ioctl_ops
// mx6s CSI 驱动自己的 V4L2 ioctl 命令表
// 挂在 vdev->ioctl_ops 上
// video_ioctl2 查 v4l2_ioctls[] 后最终调用这里
3、mx6s_csi_dev 与 vb2_queue 的关系
mx6s_csi_dev 是 mx6s CSI 驱动的私有结构体,其中嵌入了一个 vb2_queue,也就是 vb2_vidq。这个队列主要负责管理用户空间申请的视频 buffer。整体关系可以理解为:
struct mx6s_csi_dev
├─ struct video_device *vdev
│ // 对应 /dev/video0,负责给用户空间提供 V4L2 设备节点
│
├─ struct v4l2_device v4l2_dev
│ // V4L2 总设备对象,用来管理 video_device、subdev 等
│
├─ struct vb2_queue vb2_vidq
│ // videobuf2 队列,负责管理用户申请的视频 buffer
│
├─ struct v4l2_subdev *sd
│ // async 匹配到的 OV5640 sensor subdev
│
├─ struct list_head capture
│ // CSI 驱动自己的等待采集 buffer 链表
│
├─ struct list_head active_bufs
│ // 当前已经交给 CSI DMA 硬件使用的 buffer
│
└─ struct list_head discard
// 用户 buffer 不够时使用的丢帧 buffer 链表
vb2_queue 本质上是 V4L2 buffer 管理框架中的队列对象。它不直接代表某一块图像内存,而是负责管理多个 vb2_buffer。
struct vb2_queue
├─ type = V4L2_BUF_TYPE_VIDEO_CAPTURE
│ // 这是视频采集队列
│
├─ ops = &mx6s_videobuf_ops
│ // vb2 core 回调 CSI 驱动的函数表
│
├─ mem_ops = &vb2_dma_contig_memops
│ // vb2 core 用来真正分配 DMA 内存的函数表
│
├─ drv_priv = csi_dev
│ // vb2 回调里通过它找回 mx6s_csi_dev
│
├─ bufs[index]
│ // 保存每个 vb2_buffer / mx6s_buffer 管理结构
│
├─ queued_list
│ // vb2 core 记录已经 QBUF 的 buffer
│
└─ done_list
// 采集完成后,等待用户 DQBUF 取走的 buffer
3.1 vb2_ops 与 vb2_mem_ops 的区别
这里有两张表需要分清楚。第一张是 vb2_ops,也就是 buffer 管理流程回调表:
static struct vb2_ops mx6s_videobuf_ops = {
.queue_setup = mx6s_videobuf_setup,
.buf_prepare = mx6s_videobuf_prepare,
.buf_queue = mx6s_videobuf_queue,
.wait_prepare = vb2_ops_wait_prepare,
.wait_finish = vb2_ops_wait_finish,
.start_streaming = mx6s_start_streaming,
.stop_streaming = mx6s_stop_streaming,
};
这张表是 buffer 管理流程回调表,它不是真正的内存分配器。
mx6s_videobuf_ops
├─ queue_setup()
│ // REQBUFS 时调用,告诉 vb2 一帧多大、几个 plane
│
├─ buf_prepare()
│ // QBUF 时调用,设置这个 buffer 的有效数据大小
│
├─ buf_queue()
│ // QBUF 后调用,把 buffer 交给 CSI 驱动
│
├─ start_streaming()
│ // STREAMON 时调用,启动 CSI DMA
│
└─ stop_streaming()
// STREAMOFF 时调用,停止 CSI DMA
真正分配视频 DMA 内存的是 vb2_mem_ops:
cpp
const struct vb2_mem_ops vb2_dma_contig_memops = {
.alloc = vb2_dc_alloc,
.put = vb2_dc_put,
...
};
q->mem_ops = &vb2_dma_contig_memops
└─ q->mem_ops->alloc()
// MMAP 模式下真正申请视频内存
└─ vb2_dc_alloc()
// vb2 dma-contig 分配器
└─ dma_alloc_attrs()
// 真正申请 DMA 可访问的连续内存
简单来说:
mx6s_videobuf_ops
// 负责 buffer 管理流程,例如 setup、prepare、queue、start_streaming
vb2_dma_contig_memops
// 负责真正分配和释放 DMA 内存
3.2 VIDIOC_REQBUFS 流程
完整的 VIDIOC_REQBUFS 流程可以这样理解:
用户 ioctl(fd, VIDIOC_REQBUFS, &req)
└─ vdev->cdev->ops = &v4l2_fops
// /dev/video0 的字符设备操作入口
└─ v4l2_ioctl()
// V4L2 core 的 ioctl 总入口
└─ vdev->fops->unlocked_ioctl = video_ioctl2
// 进入 V4L2 ioctl 分发器
└─ v4l2_ioctls[] 找到 VIDIOC_REQBUFS
// 根据 ioctl cmd 找处理函数
└─ v4l_reqbufs()
// V4L2 core 做通用检查
└─ vdev->ioctl_ops->vidioc_reqbufs = mx6s_vidioc_reqbufs()
// 调 CSI 驱动自己的 reqbufs
└─ vb2_reqbufs(&csi_dev->vb2_vidq, req)
// 进入 vb2 框架申请 buffer
└─ vb2_core_reqbufs()
// vb2 core 真正处理 buffer 申请
├─ q->ops->queue_setup = mx6s_videobuf_setup()
│ // 询问 CSI 驱动:
│ // 每帧多大?几个 plane?最少几个 buffer?
│
├─ __vb2_queue_alloc()
│ // 分配 buffer 管理结构
│ // 本驱动实际分配 struct mx6s_buffer
│ // 并保存到 q->bufs[index]
│
└─ __vb2_buf_mem_alloc()
// MMAP 模式下分配真正视频内存
└─ q->mem_ops->alloc = vb2_dc_alloc()
// 使用 dma-contig 分配器
└─ dma_alloc_attrs()
// 申请 DMA 可访问内存
所以,REQBUFS 之后大概会形成这样的结构:
csi_dev->vb2_vidq
├─ num_buffers = 4
│
├─ bufs[0] -> struct mx6s_buffer
│ └─ vb2_buffer
│ ├─ index = 0
│ ├─ state = DEQUEUED
│ └─ planes[0].mem_priv -> DMA 内存对象
│
├─ bufs[1] -> struct mx6s_buffer
├─ bufs[2] -> struct mx6s_buffer
└─ bufs[3] -> struct mx6s_buffer
也就是说,REQBUFS 的主要作用是:
申请 buffer 管理结构
↓
申请真正的 DMA 视频内存
↓
把这些 buffer 记录到 vb2_queue 中
3.3 VIDIOC_QBUF 流程
接着看 QBUF。用户执行:
用户 ioctl(fd, VIDIOC_QBUF, &buf)
└─ mx6s_vidioc_qbuf()
// CSI ioctl_ops 里的 qbuf 回调
└─ vb2_qbuf(&csi_dev->vb2_vidq, buf)
// 把用户指定的 buffer 交给 vb2
├─ 找到 q->bufs[buf.index]
│ // 例如 index = 0,就找到 bufs[0]
│
├─ q->ops->buf_prepare = mx6s_videobuf_prepare()
│ // 设置 payload,例如一帧有效大小 = width * height * bpp
│
└─ q->ops->buf_queue = mx6s_videobuf_queue()
// 把 buffer 真正交给 CSI 驱动
└─ list_add_tail(&buf->internal.queue, &csi_dev->capture)
// 挂到 CSI 驱动自己的 capture 链表
也就是说,csi_dev->capture 里的 buffer 是从 QBUF 之后来的,并不是 start_streaming() 才挂进去的。QBUF 的作用可以理解为:
用户把某个 buffer 交给内核
↓
vb2 找到这个 buffer
↓
调用驱动的 buf_prepare()
↓
调用驱动的 buf_queue()
↓
把 buffer 挂到 csi_dev->capture 链表
3.4 VIDIOC_STREAMON 流程
STREAMON 流程如下:
用户 ioctl(fd, VIDIOC_STREAMON, &type)
└─ mx6s_vidioc_streamon()
// CSI ioctl_ops 里的 streamon 回调
└─ vb2_streamon(&csi_dev->vb2_vidq, type)
// 进入 vb2 streaming 流程
└─ q->ops->start_streaming = mx6s_start_streaming()
// 启动 CSI 硬件采集
├─ 从 csi_dev->capture 取第 1 个用户 buffer
│ // 这个 buffer 之前由 QBUF 放进 capture 链表
│
├─ vb2_dma_contig_plane_dma_addr(vb, 0)
│ // 取得这个 buffer 的 DMA 物理地址
│
├─ csi_write(addr, CSI_CSIDMASA_FB1)
│ // 把 DMA 地址写入 CSI FB1 地址寄存器
│
├─ 从 csi_dev->capture 取第 2 个用户 buffer
│
├─ csi_write(addr, CSI_CSIDMASA_FB2)
│ // 把 DMA 地址写入 CSI FB2 地址寄存器
│
├─ list_move_tail(..., &csi_dev->active_bufs)
│ // 这两个 buffer 变成硬件正在使用的 active buffer
│
└─ mx6s_csi_enable()
// 打开 CSI、DMA 请求、中断
这里需要特别注意:
不是 CPU 把图像数据写到 DMA 内存。
真实的数据写入过程是:
OV5640 输出像素数据
└─ CSI 控制器接收数据
└─ CSI DMA 根据 FB1/FB2 寄存器里的地址
└─ 把图像数据写入用户申请的 DMA buffer
也就是说,STREAMON 的作用是:
从 capture 链表取出已经 QBUF 的 buffer
↓
获取这些 buffer 的 DMA 地址
↓
把 DMA 地址写入 CSI 的 FB1/FB2 寄存器
↓
启动 CSI 和 DMA
↓
后续由 CSI DMA 硬件把图像写进这些 buffer
3.5 一帧采集完成后的中断流程
当一帧采集完成后,会触发 CSI 中断:
CSI 中断
└─ mx6s_csi_irq_handler()
// CSI 一帧完成,进入中断处理
└─ mx6s_csi_frame_done()
// 处理完成的 buffer
├─ 找到 active_bufs 中完成的 buffer
│
├─ 填 timestamp / sequence
│ // 时间戳、帧序号
│
├─ vb2_buffer_done(vb, VB2_BUF_STATE_DONE)
│ // 通知 vb2:这个 buffer 采集完成
│
├─ vb2 把 buffer 放入 done_list
│ // 等待用户 DQBUF
│
└─ 唤醒等待队列
// 如果用户正在 DQBUF 睡眠,会被唤醒
也就是说,一帧完成后,驱动会调用:
vb2_buffer_done(vb, VB2_BUF_STATE_DONE);
告诉 vb2:
这个 buffer 已经采集完成,可以交给用户取走了。
之后 vb2 会把该 buffer 放入 done_list,等待用户 DQBUF。
3.6 VIDIOC_DQBUF 流程
最后是用户 DQBUF:
用户 ioctl(fd, VIDIOC_DQBUF, &buf)
└─ mx6s_vidioc_dqbuf()
// CSI ioctl_ops 里的 dqbuf 回调
└─ vb2_dqbuf(&csi_dev->vb2_vidq, buf, nonblocking)
// 从 vb2 done_list 取一个完成 buffer
├─ 取出已经 DONE 的 vb2_buffer
│
├─ 把 index / bytesused / timestamp / sequence 填回用户 struct v4l2_buffer
│
└─ buffer 状态变回 DEQUEUED
// 用户又拿回这个 buffer,可以读取图像,也可以再次 QBUF
DQBUF 的作用可以理解为:
从 vb2 的 done_list 中取出一个已经采集完成的 buffer
↓
把 buffer 信息填回用户空间
↓
用户拿到图像数据
↓
这个 buffer 状态变回 DEQUEUED
↓
用户后续可以再次 QBUF
3.7 总结
mx6s_csi_dev 里面嵌入了 vb2_queue,用它管理用户申请的视频 buffer。
REQBUFS 时,vb2 先调用 mx6s_videobuf_setup() 询问 buffer 规格,
再分配 struct mx6s_buffer 管理结构,
最后通过 vb2_dma_contig_memops 分配真正的 DMA 内存。
QBUF 时,vb2 找到用户指定的 buffer,
准备好后调用 mx6s_videobuf_queue(),
把 buffer 挂到 csi_dev->capture 链表。
STREAMON 时,mx6s_start_streaming() 从 csi_dev->capture 取 buffer,
把它们的 DMA 地址写入 CSI 的 FB1/FB2 寄存器,
然后启动 CSI。
之后图像数据是由 CSI DMA 硬件写入这些 buffer 的。
一帧完成后,CSI 中断调用 vb2_buffer_done(),
vb2 把完成的 buffer 放入 done_list,
用户 DQBUF 时再取回。