3、V4L2框架入门:核心数据结构与驱动实现
好,咱们今天来啃V4L2这块硬骨头。说实话,我当年刚接触多摄像头同步时,V4L2框架把我折腾得够呛。但摸透了之后你会发现,它其实挺有条理的。
这一章,我会带你从最核心的数据结构入手,再走一遍ioctl调用流程,最后手写一个简单的摄像头打开/关闭驱动。嗯,都是实战干货。
3.1 V4L2核心数据结构
V4L2框架里,有两个结构体你绕不开:video_device 和 vb2_queue。说白了,一个管“设备节点”,一个管“数据流”。
3.1.1 video_device:设备节点的灵魂
每个摄像头在用户空间都对应一个 /dev/videoX 节点。这个节点背后,就是 video_device 结构体。
struct video_device {
const struct v4l2_file_operations *fops; // 文件操作函数集
struct v4l2_device *v4l2_dev; // 父设备
struct device *dev; // 内核设备结构
int index; // 设备节点编号
// ... 其他成员
};
我个人习惯,在初始化时重点关注 fops 和 v4l2_dev。前者决定了用户程序怎么操作你的摄像头,后者负责和V4L2核心层通信。
关键点:video_device 注册成功后,内核会自动创建 /dev/videoX 节点。你不需要手动 mknod。
3.1.2 vb2_queue:数据流的调度中心
摄像头采集到的数据怎么流转?全靠 vb2_queue。它管理着缓冲区队列,负责从硬件取数据、交给用户程序。
struct vb2_queue {
enum v4l2_buf_type type; // 缓冲区类型(V4L2_BUF_TYPE_VIDEO_CAPTURE等)
unsigned int io_modes; // 支持的IO模式(VB2_MMAP | VB2_USERPTR | VB2_DMABUF)
struct device *dev; // DMA设备
int (*queue_setup)(struct vb2_queue *q, unsigned int *num_buffers,
unsigned int *num_planes, unsigned int sizes[],
struct device *alloc_devs[]);
// ... 回调函数
};
我记得第一次写多摄像头驱动时,queue_setup 回调里忘记设置正确的 sizes[],结果图像全是花的。排查了整整一下午。
我的经验:初始化 vb2_queue 时,io_modes 最好同时支持 VB2_MMAP 和 VB2_USERPTR。很多上层应用(比如 GStreamer)默认用 USERPTR 模式。
3.2 ioctl调用流程:从用户态到内核态
用户程序调用 ioctl(fd, VIDIOC_XXX, &arg) 后,发生了什么?我画个简化的流程图给你看:
- 用户程序调用 ioctl()
- VFS 层根据 fd 找到对应的 video_device
- 调用 video_device->fops->unlocked_ioctl()
- 进入 V4L2 核心层的 video_ioctl2()
- 根据命令号分发到具体的处理函数(比如 v4l2_s_fmt())
- 最终调用驱动实现的回调函数
你想想看,这个流程其实很清晰。但坑在哪里?
注意:V4L2 驱动中,unlocked_ioctl 和 compat_ioctl 都要实现。32位应用在64位内核上调用时,如果没有 compat_ioctl,会直接返回 -ENOTTY。我曾经因为这个被测试部门追着骂...
3.3 实战:简单的摄像头打开/关闭驱动
光说不练假把式。咱们直接写一个最小化的摄像头驱动,只实现打开和关闭。
#include <linux/module.h>
#include <media/v4l2-device.h>
#include <media/v4l2-ioctl.h>
#include <media/videobuf2-v4l2.h>
static struct video_device vdev;
static struct v4l2_device v4l2_dev;
static int mycam_open(struct file *file)
{
struct video_device *vdev = video_devdata(file);
pr_info("mycam: device opened, index=%d\n", vdev->index);
return 0;
}
static int mycam_close(struct file *file)
{
pr_info("mycam: device closed\n");
return 0;
}
static const struct v4l2_file_operations mycam_fops = {
.owner = THIS_MODULE,
.open = mycam_open,
.release = mycam_close,
.unlocked_ioctl = video_ioctl2,
};
static int __init mycam_init(void)
{
int ret;
// 1. 注册 v4l2_device
ret = v4l2_device_register(NULL, &v4l2_dev);
if (ret < 0)
return ret;
// 2. 初始化 video_device
strscpy(vdev.name, "mycam", sizeof(vdev.name));
vdev.v4l2_dev = &v4l2_dev;
vdev.fops = &mycam_fops;
vdev.release = video_device_release_empty;
vdev.device_caps = V4L2_CAP_VIDEO_CAPTURE;
// 3. 注册 video_device
ret = video_register_device(&vdev, VFL_TYPE_VIDEO, -1);
if (ret < 0) {
v4l2_device_unregister(&v4l2_dev);
return ret;
}
pr_info("mycam: registered as /dev/video%d\n", vdev.num);
return 0;
}
static void __exit mycam_exit(void)
{
video_unregister_device(&vdev);
v4l2_device_unregister(&v4l2_dev);
pr_info("mycam: unregistered\n");
}
module_init(mycam_init);
module_exit(mycam_exit);
MODULE_LICENSE("GPL");
这段代码虽然简单,但五脏俱全。你编译加载后,用 ls /dev/video* 就能看到新设备。用 cat /dev/video0 试试?嗯,会报错,因为我们还没实现读取操作。但打开和关闭的日志会打印出来。
验证方法:加载驱动后执行 v4l2-ctl -d /dev/video0 --info,如果能看到设备信息,说明注册成功。
3.4 避坑指南
最后,分享几个我踩过的坑:
- 设备节点编号冲突:注册时
video_register_device()的第三个参数传 -1,让内核自动分配。我曾经手写死编号,结果和内置摄像头冲突,系统直接卡死。 - release 回调不能少:
vdev.release必须赋值,否则卸载驱动时内核会 panic。用video_device_release_empty是最省事的。 - module_init 顺序:如果你的驱动依赖其他模块(比如 I2C 控制器),记得用
module_init()的优先级宏,或者用late_initcall()。
好了,这一章的内容就到这里。下一章我们会给这个驱动加上真正的数据采集功能,到时候 vb2_queue 就要大显身手了。
课后练习:把上面的代码编译成模块,加载后试试用 v4l2-ctl --list-devices 查看设备。再试试 open() 和 close() 系统调用,观察内核日志。