4. V4L2 驱动核心数据结构:深入理解四大金刚

做摄像头驱动开发,说白了就是跟 V4L2 这套框架打交道。我刚开始接触的时候,也被那一堆结构体搞得头晕。但后来发现,只要抓住四个核心结构体,整个框架的脉络就清晰了。今天我们就来拆解一下这四位主角:struct video_device、struct v4l2_device、struct vb2_queue 和 struct v4l2_pix_format。

核心观点:这四个结构体分别对应了 V4L2 驱动的四个维度——设备节点、顶层管理、缓冲区管理和像素格式。理解它们之间的关系,就等于拿到了驱动开发的钥匙。

V4L2 驱动核心数据结构关系图 struct v4l2_device 顶层管理实例 struct video_device 字符设备节点 struct vb2_queue 缓冲区队列 struct v4l2_pix_format 像素格式描述 说明: v4l2_device 管理整个设备实例;video_device 提供 /dev/videoX 接口; vb2_queue 负责缓冲区分配与流转;v4l2_pix_format 描述图像格式参数。

4.1 struct v4l2_device:驱动的大脑

这个结构体是整个 V4L2 驱动的顶层管理者。你可以把它想象成一个公司的 CEO——它不直接干活,但所有资源都得归它管。

我个人习惯在 probe 函数里第一个就初始化它。它的核心成员其实不多,但有一个特别重要:dev 指针,指向对应的 struct device。这个指针决定了你的驱动怎么跟内核设备模型挂钩。

struct v4l2_device {
    struct device *dev;          // 指向内核设备结构
    struct list_head subdevs;    // 子设备链表
    spinlock_t lock;             // 内部自旋锁
    // ... 其他成员
};

我的经验:初始化时一定要调用 v4l2_device_register(),而不是手动 memset。这个函数会帮你处理好内核链表和引用计数。我曾经图省事自己初始化,结果调试了整整两天才发现是这里埋的坑。

4.2 struct video_device:用户态的门面

这个结构体对应的是 /dev/videoX 设备节点。用户空间的应用程序就是通过它来调用驱动的。

它的关键成员包括:

  • fops:文件操作函数集,定义了 open、close、ioctl 等接口
  • vfl_type:设备类型,比如 VFL_TYPE_VIDEO 或 VFL_TYPE_VBI
  • queue:指向 vb2_queue 的指针,连接缓冲区管理
  • entity:媒体实体,用于媒体控制器框架

你想想看,用户态调用 open("/dev/video0") 时,内核最终会走到 video_device->fops->open。所以这个结构体就是用户态和内核态之间的桥梁。

struct video_device {
    const struct v4l2_file_operations *fops;
    u32 vfl_type;
    struct vb2_queue *queue;
    struct media_entity entity;
    // ...
};

注意:注册 video_device 之前,一定要先设置好 v4l2_dev 字段。否则内核会报错,而且很难排查。我曾经犯过这个错,日志里只看到一个 "NULL pointer dereference",查了半天才发现是 parent 没设对。

4.3 struct vb2_queue:缓冲区的大管家

这个结构体是 videobuf2 框架的核心。它管理着所有视频缓冲区的分配、入队、出队和回收。说白了,它就是数据流的调度中心。

它的主要成员包括:

成员 类型 说明
type enum v4l2_buf_type 缓冲区类型,如 V4L2_BUF_TYPE_VIDEO_CAPTURE
io_modes u32 支持的 I/O 模式,如 VB2_MMAP | VB2_USERPTR
ops struct vb2_ops * 回调函数集,驱动必须实现
buf_struct_size unsigned int 每个缓冲区私有数据的大小
min_buffers_needed unsigned int 驱动正常工作所需的最小缓冲区数

嗯,这里要注意 ops 这个回调集。它里面有几个函数是必须实现的:queue_setup、buf_prepare、buf_queue 和 start_streaming/stop_streaming。我刚开始写驱动时,queue_setup 里忘记设置 alloc_devs,结果 DMA 分配一直失败。

static struct vb2_ops my_vb2_ops = {
    .queue_setup    = my_queue_setup,
    .buf_prepare    = my_buf_prepare,
    .buf_queue      = my_buf_queue,
    .start_streaming = my_start_streaming,
    .stop_streaming  = my_stop_streaming,
};

关键点:min_buffers_needed 这个值设多少很讲究。设太小,驱动可能因为缓冲区不足而丢帧;设太大,内存占用又高。我一般根据摄像头帧率和系统内存来权衡,通常设 3-4 个比较稳妥。

4.4 struct v4l2_pix_format:图像的身份证

这个结构体描述了一帧图像的具体格式。用户态通过 VIDIOC_S_FMT 和 VIDIOC_G_FMT 来设置和查询它。

它的核心成员:

  • width / height:图像分辨率,单位像素
  • pixelformat:像素格式,如 V4L2_PIX_FMT_YUYV、V4L2_PIX_FMT_MJPEG
  • field:场序,逐行还是隔行
  • bytesperline:每行字节数,对齐后的值
  • sizeimage:一帧图像的总字节数
  • colorspace:色彩空间,如 sRGB、BT.709
struct v4l2_pix_format {
    __u32           width;
    __u32           height;
    __u32           pixelformat;
    __u32           field;
    __u32           bytesperline;
    __u32           sizeimage;
    __u32           colorspace;
    // ...
};

为什么 bytesperline 和 sizeimage 要分开?因为硬件 DMA 通常要求每行对齐到某个字节数(比如 32 字节对齐)。你想想看,如果图像宽度是 640 像素,每个像素 2 字节,那每行就是 1280 字节。但如果硬件要求 32 字节对齐,那 bytesperline 就得是 1280 向上对齐到 32 的倍数,也就是 1312 字节。

避坑指南:我曾经遇到一个摄像头,用户态设置格式后,驱动返回的 sizeimage 跟实际 DMA 传输的大小不一致。后来发现是 bytesperline 没对齐导致的。所以建议在 try_fmt 回调里就做好对齐计算,别等到 s_fmt 才处理。

4.5 四个结构体的协作流程

这四个结构体不是孤立的,它们之间有着紧密的协作关系。我画个流程图帮你理清思路:

  1. 驱动加载:先注册 v4l2_device,再注册 video_device
  2. 打开设备:用户态 open() 触发 video_device->fops->open
  3. 设置格式:用户态 VIDIOC_S_FMT 设置 v4l2_pix_format
  4. 申请缓冲区:用户态 VIDIOC_REQBUFS 触发 vb2_queue 分配缓冲区
  5. 开始采集:用户态 VIDIOC_STREAMON 启动数据流
  6. 数据流转:硬件 DMA 填充缓冲区,vb2_queue 通过回调通知驱动

总结一下:这四个结构体构成了 V4L2 驱动的骨架。v4l2_device 是根,video_device 是门面,vb2_queue 是数据通道,v4l2_pix_format 是数据格式描述。理解它们的关系,你就能看懂大部分 V4L2 驱动的代码了。

嗯,我记得刚开始学的时候,总觉得这些结构体成员太多记不住。后来我换了个思路——先记住每个结构体的核心职责,用到哪个成员再去查内核文档。这样反而学得更快。你也试试看?