第二章 V4L2框架基础:核心数据结构与驱动流程

好,我们直接进入正题。V4L2框架,说白了就是Linux下视频设备驱动的标准接口。你想想看,如果没有这个框架,每个摄像头驱动都得自己搞一套API,那上层应用得疯掉。我最早接触V4L2是在做Exynos 4412的ISP驱动时,当时被那些结构体绕得晕头转向。后来摸清了门道,发现其实就那么几个核心东西。

2.1 三大核心数据结构

V4L2框架里有三个最重要的结构体:video_devicev4l2_devicev4l2_subdev。它们之间的关系,我习惯用一个比喻来理解:

  • v4l2_device —— 相当于整个摄像头系统的“主板”,管理所有资源
  • video_device —— 相当于“USB接口”,用户空间通过它来操作
  • v4l2_subdev —— 相当于“子卡”,比如传感器、ISP模块这些独立部件

重要理解:这三个结构体不是平级关系,而是层级关系。v4l2_device是顶层容器,video_device是用户接口,v4l2_subdev是内部组件。

2.1.1 video_device —— 用户空间的窗口

这个结构体直接对应/dev/videoX设备节点。我刚开始写驱动时,以为只要注册了video_device就能用,结果发现还要配一堆回调函数。

struct video_device {
    const struct v4l2_file_operations *fops;  // 文件操作函数集
    struct v4l2_ioctl_ops *ioctl_ops;         // ioctl操作函数集
    struct device dev;                        // 内核设备结构体
    struct v4l2_device *v4l2_dev;             // 指向父设备
    unsigned long flags;                      // 设备标志位
    char name[32];                            // 设备名称
    int minor;                                // 次设备号
    // ... 其他成员
};

这里有个坑,我踩过。flags字段一定要设置V4L2_FL_REGISTERED标志,否则应用层打开设备时会报错。嗯,这个细节很容易被忽略。

2.1.2 v4l2_device —— 驱动的心脏

这个结构体负责管理所有子设备和video_device。在Exynos ISP平台上,一个v4l2_device可能管理着传感器、MIPI接口、ISP核心等多个子设备。

struct v4l2_device {
    struct device *dev;                       // 物理设备指针
    struct list_head subdevs;                 // 子设备链表
    spinlock_t lock;                          // 自旋锁
    char name[V4L2_DEVICE_NAME_SIZE];         // 设备名称
    // ... 其他成员
};

我个人习惯在probe函数里先初始化v4l2_device,再逐个添加子设备。顺序很重要,因为子设备注册时需要引用父设备。

2.1.3 v4l2_subdev —— 模块化的基石

这个结构体是V4L2框架最精妙的设计。它让传感器、ISP、DMA等模块可以独立开发,最后通过v4l2_device_register_subdev()组合起来。

struct v4l2_subdev {
    struct v4l2_device *v4l2_dev;             // 所属父设备
    struct module *owner;                     // 所属模块
    const struct v4l2_subdev_ops *ops;        // 子设备操作函数集
    struct v4l2_subdev_pad_config *config;    // pad配置
    char name[V4L2_SUBDEV_NAME_SIZE];         // 子设备名称
    // ... 其他成员
};

实战技巧:在Exynos ISP驱动中,我习惯把每个硬件模块(如CSI接收器、ISP核心、DMA引擎)都实现为一个v4l2_subdev。这样调试时可以单独测试每个模块,非常方便。

2.2 ioctl调用流程

ioctl是用户空间和内核空间通信的主要方式。V4L2定义了大量的ioctl命令,比如VIDIOC_QUERYCAPVIDIOC_S_FMTVIDIOC_REQBUFS等。

调用流程其实很简单:

  1. 用户空间调用ioctl(fd, VIDIOC_XXX, arg)
  2. 内核进入video_ioctl2()函数
  3. 根据命令号查找对应的处理函数
  4. 调用v4l2_ioctl_ops中的回调
  5. 返回结果给用户空间

我曾经在调试Exynos ISP的格式设置时,发现VIDIOC_S_FMT总是返回错误。查了半天,原来是v4l2_ioctl_ops中的vidioc_s_fmt回调没有实现。V4L2框架不会自动帮你做格式转换,你得自己写。

static const struct v4l2_ioctl_ops isp_ioctl_ops = {
    .vidioc_querycap      = isp_querycap,
    .vidioc_enum_fmt_vid_cap = isp_enum_fmt,
    .vidioc_s_fmt_vid_cap = isp_set_fmt,
    .vidioc_reqbufs       = isp_reqbufs,
    .vidioc_qbuf          = isp_qbuf,
    .vidioc_dqbuf         = isp_dqbuf,
    .vidioc_streamon      = isp_streamon,
    .vidioc_streamoff     = isp_streamoff,
};

注意:ioctl回调函数是在原子上下文调用的,不能睡眠。如果你需要做耗时操作,比如I2C通信,记得使用工作队列或线程。

2.3 Buffer管理机制

Buffer管理是V4L2驱动最复杂的部分,也是性能的关键。V4L2支持三种buffer类型:

类型 说明 适用场景
V4L2_MEMORY_MMAP 内存映射,内核分配buffer,用户空间mmap访问 大多数摄像头驱动
V4L2_MEMORY_USERPTR 用户指针,用户空间分配buffer 需要自定义内存管理的场景
V4L2_MEMORY_DMABUF DMA buffer,通过dma-buf机制共享 多设备共享内存,如ISP+GPU

我个人最常用的是MMAP方式,简单可靠。但在Exynos ISP平台上,我强烈建议使用DMABUF方式。为什么?因为ISP处理后的图像数据经常要送给GPU或显示控制器,用DMABUF可以零拷贝,性能提升非常明显。

2.3.1 Buffer生命周期

一个buffer从申请到释放,经历这几个状态:

  • 已申请 —— 通过VIDIOC_REQBUFS分配
  • 已入队 —— 通过VIDIOC_QBUF放入驱动队列
  • 已出队 —— 通过VIDIOC_DQBUF从驱动队列取出
  • 已释放 —— 通过VIDIOC_REQBUFS(0)释放

这里有个容易出错的地方:buffer入队后,驱动会填充数据,然后应用层通过DQBUF取回。如果应用层处理太慢,buffer队列会空,导致丢帧。我在项目中遇到过这个问题,后来通过增加buffer数量解决了。

// 典型的buffer操作流程
struct v4l2_buffer buf;
memset(&buf, 0, sizeof(buf));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = i;

// 入队
ioctl(fd, VIDIOC_QBUF, &buf);

// 启动流
ioctl(fd, VIDIOC_STREAMON, &type);

// 出队
ioctl(fd, VIDIOC_DQBUF, &buf);

// 处理数据...

// 重新入队
ioctl(fd, VIDIOC_QBUF, &buf);

性能优化建议:在Exynos ISP驱动中,我通常使用4-6个buffer组成环形队列。太少容易丢帧,太多浪费内存。你可以根据帧率和处理延迟来调整。

2.4 实战经验总结

好了,说了这么多,我总结几个关键点:

  • 数据结构要理清 —— v4l2_device是根,video_device是叶,v4l2_subdev是枝
  • ioctl回调要完整 —— 不用的回调可以设为NULL,但常用的几个必须实现
  • buffer管理要谨慎 —— 注意同步和内存泄漏,特别是DMABUF方式

我记得第一次在Exynos 5422上调试ISP驱动时,花了整整一周才让第一个画面出来。当时就是卡在buffer管理上,入队出队顺序搞反了。嗯,这些坑踩过之后,你就再也不会犯了。

下一章我们会深入Exynos ISP的硬件架构,看看这些V4L2组件如何与真实的ISP硬件交互。到时候我会拿一个实际的传感器驱动来讲解,保证干货满满。