i.MX6ULL 的 V4L2 框架原理与应用

一、简介

V4L2 是 Video for Linux 2 的简称,是 Linux 系统中用于视频设备管理和应用访问的标准框架。它为摄像头、视频采集卡等视频设备提供统一的驱动接口和用户空间访问接口。

在 Linux 中,视频设备通常会被抽象为设备文件,应用程序可以像访问普通文件一样,通过 openreadwriteioctlmmap 等系统调用对其进行操作,从而完成视频采集、格式设置、参数控制和缓冲区管理等功能。

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_devv4l2_subdev 的关系

在 i.MX6ULL 的摄像头采集链路中,mx6s_csi_dev 表示 CSI 控制器驱动的私有结构,v4l2_subdev 表示 OV5640 这类 sensor 在 V4L2 框架中的抽象。设备树中,CSI 和 OV5640 一般通过 portendpointremote-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、将 sdi2c_client 互相关联。

ov5640_subdev_ops 本质上就是把 OV5640 驱动内部的 s_powerset_fmtget_fmtenum_mbus_codeg_parms_parm 等函数,按照 V4L2 subdev 的 corevideopad 接口分类挂出来。初始化完成后,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@3cdevice_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_devvideo_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 的通用入口表。它的作用是先接收用户空间的 openreadioctl 等系统调用,然后再根据当前 /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_devvb2_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_opsvb2_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 时再取回。