i.MX6ULL 的 V4L2 框架原理与应用
一、简介V4L2 是 Video for Linux 2 的简称是 Linux 系统中用于视频设备管理和应用访问的标准框架。它为摄像头、视频采集卡等视频设备提供统一的驱动接口和用户空间访问接口。在 Linux 中视频设备通常会被抽象为设备文件应用程序可以像访问普通文件一样通过open、read、write、ioctl、mmap等系统调用对其进行操作从而完成视频采集、格式设置、参数控制和缓冲区管理等功能。V4L2 框架中常见的字符设备节点主要包括两类1、/dev/videoX视频设备节点通常面向应用程序使用用于完成视频数据采集、输出或处理等操作。2、/dev/v4l-subdevXV4L2 子设备节点通常对应摄像头传感器、MIPI CSI 接收器、解码器、ISP 等子模块主要用于配置和控制视频采集链路中的各个硬件单元。二、核心对象初次学习 V4L2 框架时可能会觉得整个框架比较复杂涉及的结构体和调用流程很多。因此理解 V4L2 时不建议一开始就陷入所有细节而是应该先抓住几个核心对象。对于 i.MX6ULL 的 CSI 视频采集驱动来说可以重点关注以下 4 个核心对象。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: ov56403c { compatible ovti,ov5640; status okay; port { ov5640_ep: endpoint { remote-endpoint csi1_ep; }; }; }; };内核解析设备树后csi会生成 CSI 控制器对应的platform_device并匹配 mx6s CSI 驱动ov56403c会生成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再找到它的父节点ov56403c。CSI 不会直接自己去拿 OV5640 的 subdev而是构造一个v4l2_async_subdev等待项asd设置为ov56403c的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 框架中的内核对象。两者关系可以理解为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.1mx6s_csi_probe()中创建并填充vdevmx6s CSI 驱动在probe阶段会创建并初始化video_devicemx6s_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 内部会注册字符设备和设备模型这里最关键的是vdev-fops mx6s_csi_fops; vdev-ioctl_ops mx6s_csi_ioctl_ops; vdev-queue csi_dev-vb2_vidq;这几项决定了用户访问/dev/video0时最终会进入 mx6s CSI 驱动自己的操作函数。2.2video_register_device()中注册字符设备调用video_register_device()后V4L2 core 会进一步完成字符设备注册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_operationsstatic 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.5open()调用流程用户执行open(/dev/video0)。整体调用流程如下用户 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()的入口层次是用户空间 open() ↓ V4L2 core: v4l2_open() ↓ mx6s CSI: mx6s_csi_open()2.6ioctl()调用流程ioctl()流程是重点。例如用户执行ioctl(fd, VIDIOC_REQBUFS, req)。整体调用流程如下用户 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()的调用链可以简化理解为用户 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 三张操作表的区别可以把这三张表这样区分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 取走的 buffer3.1vb2_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_opsconst 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.2VIDIOC_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.3VIDIOC_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.4VIDIOC_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 硬件把图像写进这些 buffer3.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.6VIDIOC_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可以读取图像也可以再次 QBUFDQBUF的作用可以理解为从 vb2 的 done_list 中取出一个已经采集完成的 buffer ↓ 把 buffer 信息填回用户空间 ↓ 用户拿到图像数据 ↓ 这个 buffer 状态变回 DEQUEUED ↓ 用户后续可以再次 QBUF3.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 时再取回。