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 框架里的。我在调试全志平台时,经常需要翻看这部分代码,因为注册流程一旦出错,整个解码链路就断了。
驱动注册的核心流程,我总结为三步:
- 平台驱动注册:cedar_ve 作为一个 platform_driver,在系统启动时被加载。
- V4L2 设备初始化:在 probe 函数里,分配并初始化 v4l2_device 和 video_device 结构体。
- 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 的硬件解码引擎,讲讲它到底是怎么把码流变成图像的。