4. V4L2框架入门:架构、核心数据结构与驱动注册流程

好,咱们进入V4L2的世界。说实话,我刚接触Linux视频驱动时,也被V4L2那一堆结构体搞得头晕。但后来我发现,只要抓住几个核心点,整个框架其实挺清晰的。今天我就带你捋一捋V4L2的架构、关键数据结构,以及驱动是怎么注册进去的。

4.1 V4L2整体架构:从应用到硬件的桥梁

V4L2的全称是Video for Linux 2,它是Linux内核中视频设备驱动的标准框架。说白了,它定义了一套统一的接口,让用户空间的应用程序(比如摄像头App)可以不管底层硬件是什么,都能用同样的方式去操作。

整个架构分三层:

  • 用户空间:应用程序通过open()ioctl()mmap()等标准系统调用与V4L2设备交互。
  • V4L2核心层:内核中的videodev.ko模块,负责管理设备节点、分发ioctl请求。
  • 驱动层:你写的具体硬件驱动,实现v4l2_file_operationsv4l2_ioctl_ops等回调。

我个人习惯把V4L2想象成一个“中间人”。它不关心你的摄像头是USB还是MIPI接口,它只负责把应用发来的命令翻译成驱动能理解的动作。

核心思想:V4L2通过/dev/videoX设备节点暴露给用户空间。每个节点对应一个视频输入或输出设备。

4.2 核心数据结构:video_device 和 v4l2_device

这两个结构体是V4L2驱动的基石。我当年第一次写摄像头驱动时,就是没搞清楚它们的关系,结果设备注册成功了但应用打不开——嗯,后来才发现是v4l2_device没初始化好。

4.2.1 struct video_device

这个结构体代表一个V4L2设备实例。它包含了设备的所有操作函数指针、设备信息、以及一些控制标志。

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

这里要注意的是fopsioctl_ops。前者处理openclosereadwritemmap等基本文件操作;后者处理各种V4L2特有的控制命令,比如设置格式、启动流、请求缓冲区等。

我的经验ioctl_ops里有很多回调函数,但你不需要全部实现。只实现你的硬件支持的功能即可。比如你的摄像头不支持裁剪,那vidioc_cropcap就可以不实现。

4.2.2 struct v4l2_device

这个结构体是V4L2驱动的“根容器”。一个物理设备(比如一个USB摄像头)通常对应一个v4l2_device,它可以包含多个video_device(比如一个摄像头既有视频流又有视频输出)。

struct v4l2_device {
    struct device *dev;           // 父设备
    struct list_head subdevs;     // 子设备链表
    spinlock_t lock;              // 自旋锁
    char name[V4L2_DEVICE_NAME_SIZE]; // 设备名称
    // ...
};

我建议你把v4l2_device看作“公司总部”,video_device是“各个部门”。公司总部负责整体管理,各部门负责具体业务。

4.3 驱动注册流程:一步步来

好了,理论说完了,咱们看看实际怎么写一个V4L2驱动的骨架。我以最简单的摄像头驱动为例,展示注册流程。

4.3.1 第一步:分配和初始化 v4l2_device

struct v4l2_device *v4l2_dev;
v4l2_dev = kzalloc(sizeof(*v4l2_dev), GFP_KERNEL);
if (!v4l2_dev)
    return -ENOMEM;

snprintf(v4l2_dev->name, sizeof(v4l2_dev->name), "my_camera");
v4l2_device_register(dev, v4l2_dev);

这里v4l2_device_register会把你的设备注册到V4L2核心中。第一个参数是父设备(通常是平台设备或USB设备),第二个参数就是你的v4l2_device实例。

我曾经踩过的坑:忘记检查v4l2_device_register的返回值。如果注册失败,后续所有操作都会出问题。一定要加错误处理!

4.3.2 第二步:分配和初始化 video_device

struct video_device *vdev;
vdev = video_device_alloc();
if (!vdev) {
    v4l2_device_unregister(v4l2_dev);
    return -ENOMEM;
}

vdev->v4l2_dev = v4l2_dev;
vdev->fops = &my_camera_fops;
vdev->ioctl_ops = &my_camera_ioctl_ops;
vdev->release = video_device_release;
snprintf(vdev->name, sizeof(vdev->name), "My Camera");

注意release回调。当最后一个用户关闭设备时,内核会调用这个函数来释放video_device。如果你不设置,就会内存泄漏。

4.3.3 第三步:注册 video_device

int ret;
ret = video_register_device(vdev, VFL_TYPE_VIDEO, -1);
if (ret < 0) {
    video_device_release(vdev);
    v4l2_device_unregister(v4l2_dev);
    return ret;
}

video_register_device的第二个参数指定设备类型:

类型 说明
VFL_TYPE_VIDEO 视频捕获/输出设备(/dev/videoX)
VFL_TYPE_VBI 垂直消隐区间设备(/dev/vbiX)
VFL_TYPE_RADIO 收音机设备(/dev/radioX)

第三个参数是次设备号,传-1表示让内核自动分配。

4.3.4 第四步:实现文件操作和ioctl回调

这是最核心的部分。你需要实现v4l2_file_operationsv4l2_ioctl_ops中的回调函数。最基本的:

static const struct v4l2_file_operations my_camera_fops = {
    .owner      = THIS_MODULE,
    .open       = my_camera_open,
    .release    = my_camera_release,
    .read       = my_camera_read,
    .mmap       = my_camera_mmap,
    .unlocked_ioctl = video_ioctl2,  // 标准V4L2 ioctl分发器
};

static const struct v4l2_ioctl_ops my_camera_ioctl_ops = {
    .vidioc_querycap      = my_camera_querycap,
    .vidioc_enum_fmt_vid_cap = my_camera_enum_fmt,
    .vidioc_s_fmt_vid_cap = my_camera_set_fmt,
    .vidioc_reqbufs       = my_camera_reqbufs,
    .vidioc_qbuf          = my_camera_qbuf,
    .vidioc_dqbuf         = my_camera_dqbuf,
    .vidioc_streamon      = my_camera_streamon,
    .vidioc_streamoff     = my_camera_streamoff,
};

注意unlocked_ioctl要设置为video_ioctl2,这是V4L2核心提供的标准分发函数。它会根据ioctl命令号自动调用ioctl_ops中对应的回调。

4.3.5 第五步:卸载时清理

static void my_camera_remove(struct platform_device *pdev)
{
    struct v4l2_device *v4l2_dev = platform_get_drvdata(pdev);
    struct video_device *vdev = container_of(v4l2_dev, struct my_camera_dev, v4l2_dev)->vdev;

    video_unregister_device(vdev);
    v4l2_device_unregister(v4l2_dev);
    // 释放其他资源
}

顺序很重要:先注销video_device,再注销v4l2_device。反了的话,设备节点还在但父设备已经没了,应用访问时会崩溃。

4.4 完整注册流程总结

我把整个流程画成一张表,方便你对照:

步骤 操作 关键函数
1 分配v4l2_device kzalloc + v4l2_device_register
2 分配video_device video_device_alloc
3 设置回调函数 填充fopsioctl_ops
4 注册video_device video_register_device
5 实现具体功能 实现各个ioctl回调
6 卸载清理 video_unregister_device + v4l2_device_unregister

记住:V4L2驱动注册的核心就是“先有父设备,再有子设备;先注销子设备,再注销父设备”。这个顺序搞对了,80%的问题就避免了。

好了,这一章的内容就到这里。V4L2框架其实不复杂,关键是理解v4l2_devicevideo_device的关系,以及注册流程的顺序。下一章我会带你深入ioctl_ops的各个回调,看看怎么真正让摄像头动起来。