第2章:Linux V4L2框架精讲:核心数据结构、ioctl调用流程、视频采集与缓冲队列管理
好,咱们直接进入正题。V4L2 是 Linux 下视频设备驱动的标准框架。说白了,它就是一套规范,让上层应用和底层硬件能好好沟通。我刚开始接触 V4L2 时,也被那一堆结构体搞得头晕。但摸清楚套路后,你会发现它设计得其实挺巧妙。
2.1 V4L2 核心数据结构
V4L2 的核心数据结构就那么几个。你想想看,一个视频设备要工作,需要描述什么?设备本身、输入输出格式、缓冲区、还有控制参数。嗯,V4L2 都帮你定义好了。
2.1.1 struct video_device
这是 V4L2 设备的核心。每个视频设备在驱动中对应一个 video_device 实例。它负责注册到内核,并关联操作函数集。
struct video_device {
const struct v4l2_file_operations *fops; // 文件操作
struct v4l2_device *v4l2_dev; // 父设备
struct device *dev; // 内核设备
// ... 还有很多字段
v4l2_std_id tvnorms; // 支持的电视制式
unsigned int minor; // 次设备号
};
我个人习惯在 probe 函数里先分配 video_device,然后设置好 fops 和 release 回调。记得要调用 video_register_device() 注册。我曾经犯过一个低级错误——注册前忘了初始化 v4l2_dev 指针,结果内核直接 oops 了。
2.1.2 struct v4l2_device
这个结构体是 V4L2 设备的顶层容器。一个物理设备可能包含多个子设备(比如摄像头、音频输入),v4l2_device 负责管理它们。
struct v4l2_device {
struct device *dev; // 物理设备
struct list_head subdevs; // 子设备链表
spinlock_t lock; // 自旋锁
char name[V4L2_DEVICE_NAME_SIZE]; // 设备名
};
实际项目中,我通常用 v4l2_device_register() 来注册。注意,这个函数需要传入父设备的 struct device * 指针。如果你用的是 platform 设备,那就是 &pdev->dev。
2.1.3 struct v4l2_pix_format
描述图像格式的结构体。应用层通过它告诉驱动:我要什么分辨率、什么像素格式。
struct v4l2_pix_format {
__u32 width; // 宽度
__u32 height; // 高度
__u32 pixelformat; // 像素格式,如 V4L2_PIX_FMT_YUYV
__u32 field; // 隔行模式
__u32 bytesperline; // 每行字节数
__u32 sizeimage; // 一帧图像大小
// ...
};
这里有个坑:bytesperline 和 sizeimage 必须正确计算。我见过不少驱动直接把 width * height * bpp 塞进去,但有些硬件要求行对齐到 32 字节。嗯,不按硬件要求来,采集出来的图像就是花的。
2.1.4 struct v4l2_buffer 与 struct v4l2_requestbuffers
缓冲区管理是 V4L2 的精髓。应用层通过 VIDIOC_REQBUFS 请求分配缓冲区,然后用 VIDIOC_QBUF 和 VIDIOC_DQBUF 来入队出队。
struct v4l2_requestbuffers {
__u32 count; // 缓冲区数量
__u32 type; // 缓冲区类型,如 V4L2_BUF_TYPE_VIDEO_CAPTURE
__u32 memory; // 内存类型,如 V4L2_MEMORY_MMAP
};
struct v4l2_buffer {
__u32 index; // 缓冲区索引
__u32 type;
__u32 bytesused; // 实际数据大小
__u32 flags; // 标志位
__u32 field;
struct timeval timestamp; // 时间戳
// ...
};
我个人建议缓冲区数量至少设 4 个。太少会导致应用层来不及处理,丢帧;太多又浪费内存。4 个是经验值,既能保证流水线顺畅,又不会太吃资源。
2.2 ioctl 调用流程
V4L2 的 ioctl 调用流程,说白了就是应用层通过系统调用进入内核,然后 V4L2 框架帮你分发到具体的驱动函数。整个过程分三步走:
- 应用层调用 ioctl():传入文件描述符和命令码。
- V4L2 框架拦截:在
v4l2_ioctl()中根据命令码查找对应的处理函数。 - 驱动回调执行:调用你在
v4l2_ioctl_ops中注册的函数。
来看一个典型的 ioctl 处理链:
// 应用层
int fd = open("/dev/video0", O_RDWR);
ioctl(fd, VIDIOC_QUERYCAP, &cap);
// 内核层
long v4l2_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
struct video_device *vdev = video_devdata(filp);
// 先做权限检查、参数校验
// 然后调用 vdev->fops->ioctl 或者走默认处理
return v4l2_ioctl_ops->vidioc_querycap(filp, arg);
}
你可能会问:为什么 V4L2 不直接用标准的 unlocked_ioctl?其实它用了,但 V4L2 框架在 v4l2_ioctl() 里做了很多通用处理,比如参数拷贝、命令码匹配。这样驱动开发者只需要关注业务逻辑。
我记得有一次调试,发现应用层调用 VIDIOC_S_FMT 后驱动没反应。查了半天,原来是 v4l2_ioctl_ops 里没注册 vidioc_s_fmt 回调。嗯,这种低级错误,犯过一次就记住了。
2.3 视频采集与缓冲队列管理
视频采集的核心就是缓冲队列。驱动负责把硬件采集到的数据填到缓冲区,应用层负责取走处理。两者通过队列来解耦。
2.3.1 队列初始化
驱动中通常用 vb2_queue_init() 来初始化一个 vb2_queue 结构体。这个结构体管理着所有缓冲区。
struct vb2_queue {
enum v4l2_buf_type type; // 缓冲区类型
unsigned int io_modes; // 支持的 IO 模式
const struct vb2_ops *ops; // 操作函数集
struct mutex *lock; // 互斥锁
// ...
};
初始化时,你需要设置好 vb2_ops 回调。其中最重要的几个:
queue_setup():分配缓冲区时调用,告诉框架你需要多少内存。buf_prepare():缓冲区入队前调用,做准备工作。start_streaming():开始采集时调用,启动硬件。stop_streaming():停止采集时调用,关闭硬件。
2.3.2 入队与出队流程
典型的采集流程是这样的:
- 应用层调用
VIDIOC_REQBUFS申请缓冲区。 - 调用
VIDIOC_QBUF将空缓冲区入队。 - 调用
VIDIOC_STREAMON启动采集。 - 硬件采集完一帧,触发中断。驱动在中断处理中标记缓冲区为完成状态。
- 应用层调用
VIDIOC_DQBUF取出已填满的缓冲区。 - 处理完数据后,再次
VIDIOC_QBUF入队,形成循环。
// 驱动中断处理伪代码
void irq_handler(struct vb2_queue *q)
{
struct vb2_buffer *vb = q->bufs[completed_index];
vb->timestamp = ktime_get_ns();
vb2_buffer_done(vb, VB2_BUF_STATE_DONE);
// 唤醒等待队列
wake_up(&q->done_wq);
}
这里有个关键点:vb2_buffer_done() 必须在中断上下文调用吗?不一定。如果你的硬件需要较长时间处理,可以在下半部(tasklet 或 workqueue)中调用。但要注意,vb2_buffer_done() 内部会调用 wake_up(),所以不能在原子上下文里调用带睡眠的函数。
2.3.3 多摄像头同步采集的挑战
多摄像头同步,说白了就是让多个摄像头在同一时刻开始采集,并且每一帧的时间戳对齐。我做过一个双目视觉项目,两个摄像头硬件上通过同一个 trigger 信号触发。但驱动里怎么处理?
我的做法是:
- 两个摄像头共用同一个
vb2_queue?不行,因为每个摄像头有自己的 DMA 通道。 - 分别初始化两个
vb2_queue,但共享一个start_streaming回调。当第一个摄像头调用start_streaming时,不立即启动硬件,而是等待第二个摄像头也调用start_streaming,然后同时触发硬件。 - 时间戳同步:在中断处理中,用同一个时钟源(比如
ktime_get_boottime())来打时间戳。
VIDIOC_STREAMON 的调用顺序会影响同步精度。建议应用层先打开所有设备,再依次调用 STREAMON,间隔尽量短。
2.4 避坑指南
我曾经踩过的坑:
- 缓冲区溢出: 有一次我忘了检查
bytesused是否超过sizeimage,结果应用层读取时内存越界。后来我在buf_prepare里加了校验。 - 时间戳混乱: 多摄像头项目中,我一开始用了
do_gettimeofday(),结果发现不同摄像头的时间戳有几十毫秒的偏差。换成ktime_get_boottime()后问题解决。 - 队列死锁: 在
stop_streaming回调里调用vb2_buffer_done()时,如果持有某个锁,而vb2_buffer_done()内部又尝试获取同一个锁,就会死锁。嗯,这种问题用 lockdep 能检测出来。
好了,V4L2 的核心内容就这些。你想想看,其实框架并不复杂,关键是理解数据结构和流程。下一章我会讲如何编写一个实际的摄像头驱动,到时候咱们再深入细节。