4、V4L2驱动框架:Video for Linux 2概述、H616 V4L2解码驱动(cedar_ve)注册流程、视频节点(/dev/videoX)创建、ioctl接口详解

4.1 V4L2驱动框架:到底是个啥?

好,咱们直接切入正题。V4L2,全称 Video for Linux 2,是 Linux 内核里处理视频设备的统一框架。说白了,它就是给上层应用(比如 GStreamer、FFmpeg)和底层硬件驱动之间搭了一座桥。

我个人习惯把 V4L2 理解成一个「标准化接口」。你想想看,市面上有海思、全志、瑞芯微这么多家的芯片,每家都有自己的视频硬件。如果没有 V4L2,那上层应用得为每款芯片写一套代码,这得多崩溃?

V4L2 的核心思想就是:驱动负责把硬件能力暴露出来,应用通过统一的 ioctl 命令去调用。驱动注册一个 video_device,应用打开 /dev/videoX,然后通过 ioctl 设置格式、申请缓冲区、启动流、获取帧数据。就这么简单。

关键点:V4L2 不仅仅用于摄像头采集,它同样适用于视频解码、编码、输出等场景。H616 的 cedar_ve 解码驱动就是基于 V4L2 的 M2M(Memory to Memory)框架实现的。

4.2 H616 V4L2 解码驱动(cedar_ve)注册流程

好,咱们来看看 H616 上的 cedar_ve 驱动是怎么注册到 V4L2 框架里的。我在调试全志平台时,经常需要翻看这部分代码,因为注册流程一旦出错,整个解码链路就断了。

驱动注册的核心流程,我总结为三步:

  1. 平台驱动注册:cedar_ve 作为一个 platform_driver,在系统启动时被加载。
  2. V4L2 设备初始化:在 probe 函数里,分配并初始化 v4l2_device 和 video_device 结构体。
  3. M2M 设备注册:调用 v4l2_m2m_init() 和 v4l2_m2m_register_device(),将解码器注册为 M2M 设备。

下面这段代码,是我从 H616 内核源码里摘出来的核心注册逻辑:

static int cedar_ve_probe(struct platform_device *pdev)
{
    struct cedar_ve_dev *dev;
    struct video_device *vfd;
    int ret;

    // 1. 分配驱动私有数据结构体
    dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
    if (!dev)
        return -ENOMEM;

    // 2. 初始化 v4l2_device
    dev->v4l2_dev = &dev->v4l2_dev_struct;
    ret = v4l2_device_register(&pdev->dev, dev->v4l2_dev);
    if (ret) {
        dev_err(&pdev->dev, "Failed to register V4L2 device\n");
        return ret;
    }

    // 3. 初始化 M2M 设备
    dev->m2m_dev = v4l2_m2m_init(&cedar_ve_m2m_ops);
    if (IS_ERR(dev->m2m_dev)) {
        ret = PTR_ERR(dev->m2m_dev);
        goto err_v4l2;
    }

    // 4. 初始化 video_device
    vfd = &dev->vfd;
    strscpy(vfd->name, "cedar_ve", sizeof(vfd->name));
    vfd->fops = &cedar_ve_fops;
    vfd->ioctl_ops = &cedar_ve_ioctl_ops;
    vfd->vfl_dir = VFL_DIR_M2M;
    vfd->device_caps = V4L2_CAP_VIDEO_M2M | V4L2_CAP_STREAMING;
    vfd->v4l2_dev = dev->v4l2_dev;
    video_set_drvdata(vfd, dev);

    // 5. 注册 video_device,创建 /dev/videoX 节点
    ret = video_register_device(vfd, VFL_TYPE_VIDEO, -1);
    if (ret) {
        dev_err(&pdev->dev, "Failed to register video device\n");
        goto err_m2m;
    }

    dev_info(&pdev->dev, "cedar_ve registered as /dev/video%d\n", vfd->num);
    return 0;

err_m2m:
    v4l2_m2m_release(dev->m2m_dev);
err_v4l2:
    v4l2_device_unregister(dev->v4l2_dev);
    return ret;
}

避坑指南:我曾经在调试时发现 video_register_device 返回 -ENOMEM,查了半天才发现是 v4l2_device_register 和 video_device 的 parent 设备没设置对。记住,v4l2_device 的 parent 一定要指向 platform_device,否则 sysfs 节点创建会失败。

4.3 视频节点(/dev/videoX)的创建

视频节点是怎么来的?其实就是上面代码里 video_register_device() 这个函数干的活。它会向内核的 VFS(虚拟文件系统)注册一个字符设备,主设备号是 81(V4L2 的固定主设备号),次设备号动态分配。

你可能会问:为什么是 /dev/video0 而不是 /dev/video1?

嗯,这里有个细节。video_register_device 的第三个参数是 nr,如果传 -1,内核会自动分配一个空闲的次设备号。H616 上通常 cedar_ve 会拿到 video0 或 video1,具体取决于其他视频设备的注册顺序。

我建议你在调试时,先执行 ls -l /dev/video* 看看有哪些节点。然后通过 cat /sys/class/video4linux/video0/name 确认哪个是 cedar_ve。

重要:cedar_ve 注册的是 VFL_TYPE_VIDEO 类型的设备,所以节点是 /dev/videoX。如果是 VFL_TYPE_VBI 或 VFL_TYPE_RADIO,节点名会不同。这一点在写应用层代码时要注意。

4.4 ioctl 接口详解

ioctl 是 V4L2 的灵魂。应用层通过 ioctl 和驱动交互,驱动通过 ioctl_ops 结构体里的回调函数响应。cedar_ve 的 ioctl_ops 定义如下:

static const struct v4l2_ioctl_ops cedar_ve_ioctl_ops = {
    .vidioc_querycap        = cedar_ve_querycap,
    .vidioc_enum_fmt_vid_cap  = cedar_ve_enum_fmt,
    .vidioc_enum_fmt_vid_out  = cedar_ve_enum_fmt,
    .vidioc_s_fmt_vid_cap   = cedar_ve_s_fmt_cap,
    .vidioc_s_fmt_vid_out   = cedar_ve_s_fmt_out,
    .vidioc_g_fmt_vid_cap   = cedar_ve_g_fmt_cap,
    .vidioc_g_fmt_vid_out   = cedar_ve_g_fmt_out,
    .vidioc_reqbufs         = cedar_ve_reqbufs,
    .vidioc_querybuf        = cedar_ve_querybuf,
    .vidioc_qbuf            = cedar_ve_qbuf,
    .vidioc_dqbuf           = cedar_ve_dqbuf,
    .vidioc_streamon        = cedar_ve_streamon,
    .vidioc_streamoff       = cedar_ve_streamoff,
};

我来挑几个重点说一下:

  • VIDIOC_QUERYCAP:查询设备能力。应用会问「你是不是视频解码设备?」驱动返回 V4L2_CAP_VIDEO_M2M 就对了。
  • VIDIOC_S_FMT / G_FMT:设置/获取格式。对于解码器,输出端(OUTPUT)是编码后的码流格式,捕获端(CAPTURE)是解码后的 YUV 格式。
  • VIDIOC_REQBUFS:申请缓冲区。这里要注意,M2M 设备需要为 OUTPUT 和 CAPTURE 两端分别申请缓冲区。
  • VIDIOC_QBUF / DQBUF:入队/出队缓冲区。这是解码的核心循环:把码流数据放入 OUTPUT 队列,解码完成后从 CAPTURE 队列取出 YUV 数据。

曾经踩过的坑:我在调试 H616 解码时,发现应用层调用 VIDIOC_STREAMON 后,驱动没有启动硬件解码。查了两天才发现,M2M 设备必须先调用 OUTPUT 端的 STREAMON,再调用 CAPTURE 端的 STREAMON,顺序反了会导致死锁。这是 V4L2 M2M 框架的一个约定,文档里写得很隐晦。

最后,我整理了一个常用的 ioctl 调用时序表,方便你对照:

步骤 ioctl 命令 说明
1 VIDIOC_QUERYCAP 查询设备能力,确认是 M2M 解码器
2 VIDIOC_S_FMT (OUTPUT) 设置码流格式(如 H.264)
3 VIDIOC_S_FMT (CAPTURE) 设置解码后 YUV 格式和分辨率
4 VIDIOC_REQBUFS (OUTPUT) 申请码流缓冲区
5 VIDIOC_REQBUFS (CAPTURE) 申请 YUV 缓冲区
6 VIDIOC_QBUF (OUTPUT) 将码流数据入队
7 VIDIOC_STREAMON (OUTPUT) 启动输出端流
8 VIDIOC_STREAMON (CAPTURE) 启动捕获端流
9 VIDIOC_DQBUF (CAPTURE) 取出解码后的 YUV 数据

嗯,这一章的内容就到这里。V4L2 框架其实不复杂,关键是理解它的分层思想和 ioctl 的调用流程。下一章我会深入 cedar_ve 的硬件解码引擎,讲讲它到底是怎么把码流变成图像的。