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_operations和v4l2_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; // 次设备号
// ... 还有其他字段
};
这里要注意的是fops和ioctl_ops。前者处理open、close、read、write、mmap等基本文件操作;后者处理各种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_operations和v4l2_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 | 设置回调函数 | 填充fops和ioctl_ops |
| 4 | 注册video_device | video_register_device |
| 5 | 实现具体功能 | 实现各个ioctl回调 |
| 6 | 卸载清理 | video_unregister_device + v4l2_device_unregister |
记住:V4L2驱动注册的核心就是“先有父设备,再有子设备;先注销子设备,再注销父设备”。这个顺序搞对了,80%的问题就避免了。
好了,这一章的内容就到这里。V4L2框架其实不复杂,关键是理解v4l2_device和video_device的关系,以及注册流程的顺序。下一章我会带你深入ioctl_ops的各个回调,看看怎么真正让摄像头动起来。