3、V4L2框架详解:核心数据结构、ioctl控制流程、视频采集缓冲区管理
好,咱们今天来啃一块硬骨头——V4L2框架。说实话,很多做嵌入式Linux开发的朋友,一听到V4L2就头大。我刚开始接触那会儿也是,翻着内核文档,看着那一堆结构体,心里直犯嘀咕:这玩意儿到底怎么串起来的?
其实没那么玄乎。V4L2说白了就是一套标准接口,让应用程序和摄像头驱动能对上话。你想想看,市面上那么多摄像头芯片,OV、索尼、安森美……每家寄存器都不一样。要是每个芯片都搞一套自己的API,那应用层工程师不得疯掉?V4L2就是来解决这个问题的。
3.1 V4L2核心数据结构
我们先从最基础的数据结构说起。我个人习惯把V4L2的数据结构分成三层:设备层、队列层、缓冲区层。咱们一层一层剥开看。
3.1.1 设备层:struct video_device
这是最顶层的数据结构,代表一个视频设备节点。你在/dev目录下看到的video0、video1,背后就是它。
struct video_device {
const struct v4l2_file_operations *fops;
struct v4l2_device *v4l2_dev;
struct device *dev;
struct cdev *cdev;
struct v4l2_ioctl_ops *ioctl_ops;
unsigned long flags;
int index;
// ... 还有其他成员
};
这里最关键的是ioctl_ops,它指向一个操作函数集。应用程序调用ioctl时,最终会落到这些函数上。我在项目中遇到过一个问题:注册video_device时忘了设置v4l2_dev指针,结果应用程序一打开设备就崩溃。嗯,这种低级错误犯过一次就记住了。
3.1.2 队列层:struct vb2_queue
缓冲区队列是V4L2的核心。应用程序从队列里取帧,驱动往队列里放帧。这个队列由struct vb2_queue管理。
struct vb2_queue {
enum v4l2_buf_type type;
unsigned int io_modes;
struct device *dev;
const struct vb2_ops *ops;
const struct vb2_mem_ops *mem_ops;
struct vb2_buffer *bufs[VIDEO_MAX_FRAME];
unsigned int num_buffers;
// ...
};
注意看vb2_ops,这是驱动开发者需要实现的核心回调。包括queue_setup、buf_prepare、buf_queue、start_streaming、stop_streaming等。说白了,驱动工程师的工作就是填好这些回调函数。
io_modes设置对。我见过有人同时设置了VB2_MMAP和VB2_USERPTR,但硬件只支持MMAP模式,结果应用程序用USERPTR模式申请缓冲区时,驱动直接返回-EINVAL。调试了半天才发现是这里的问题。
3.1.3 缓冲区层:struct vb2_buffer
每个缓冲区对应一个struct vb2_buffer。它里面包含了缓冲区的大小、状态、时间戳等信息。
struct vb2_buffer {
struct vb2_queue *vb2_queue;
unsigned int index;
unsigned int num_planes;
struct vb2_plane planes[VB2_MAX_PLANES];
enum vb2_buffer_state state;
struct vb2_v4l2_buffer vb2_buf;
// ...
};
这里有个概念要搞清楚:planes。对于YUV420这种格式,一个缓冲区可能包含三个plane:Y、U、V。对于MJPEG这种压缩格式,通常只有一个plane。我在做某个项目时,摄像头输出的是NV12格式,两个plane。应用程序那边按单plane去读数据,结果画面颜色完全不对。后来一查,是plane数量没对上。
3.2 ioctl控制流程
应用程序和V4L2驱动交互,主要靠ioctl。你想想看,打开设备、设置格式、申请缓冲区、开始采集、停止采集……这些操作都是通过ioctl完成的。
ioctl的调用路径是这样的:
- 应用程序调用
ioctl(fd, VIDIOC_XXX, &arg) - 进入内核的
v4l2_ioctl函数 - 根据命令号查找对应的处理函数
- 调用驱动实现的
ioctl_ops中的回调
举个例子,设置格式的流程:
// 应用程序代码
struct v4l2_format fmt = {0};
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt.fmt.pix.width = 640;
fmt.fmt.pix.height = 480;
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
ioctl(fd, VIDIOC_S_FMT, &fmt);
驱动这边,ioctl_ops里对应的回调是vidioc_s_fmt_vid_cap。驱动在这个函数里要做几件事:
- 检查格式是否支持
- 调整参数(比如宽度对齐到16的倍数)
- 配置硬件寄存器
- 把实际设置的格式写回
v4l2_format结构体
vidioc_s_fmt_vid_cap里修改了宽度,但没有更新bytesperline。结果应用程序用VIDIOC_G_FMT查询时,拿到的bytesperline还是旧的,导致缓冲区大小计算错误。这个问题排查了很久,最后发现是驱动实现不完整。
常用的ioctl命令大概有十几个,我列几个最核心的:
| 命令 | 功能 | 说明 |
|---|---|---|
| VIDIOC_QUERYCAP | 查询设备能力 | 获取驱动名称、支持的操作类型等 |
| VIDIOC_ENUM_FMT | 枚举支持的格式 | 应用程序用来遍历所有支持的像素格式 |
| VIDIOC_S_FMT | 设置格式 | 设置图像宽度、高度、像素格式 |
| VIDIOC_REQBUFS | 申请缓冲区 | 分配视频缓冲区内存 |
| VIDIOC_QBUF | 入队缓冲区 | 把缓冲区放入采集队列 |
| VIDIOC_DQBUF | 出队缓冲区 | 从采集队列取出已填满的缓冲区 |
| VIDIOC_STREAMON | 开始采集 | 启动视频流 |
| VIDIOC_STREAMOFF | 停止采集 | 停止视频流 |
3.3 视频采集缓冲区管理
缓冲区管理是V4L2里最复杂、也最容易出问题的地方。我刚开始做摄像头驱动时,这块折腾了整整一周。咱们慢慢说。
3.3.1 三种缓冲区管理模式
V4L2支持三种缓冲区管理模式:
- MMAP模式:驱动分配内存,应用程序通过mmap映射到用户空间。这是最常用的模式,性能好,适合大多数场景。
- USERPTR模式:应用程序自己分配内存,把用户空间地址传给驱动。适合需要自己管理内存的场景。
- DMABUF模式:通过DMA-BUF机制共享缓冲区。适合需要和其他硬件模块(如GPU、显示控制器)共享数据的场景。
我个人最常用的是MMAP模式。为什么?简单、高效、兼容性好。USERPTR模式虽然灵活,但有个问题:用户空间分配的内存可能不是物理连续的,有些DMA控制器不支持。我曾经在一个项目里用USERPTR模式,结果摄像头芯片的DMA只能访问物理连续内存,画面一直出不来。后来换成MMAP模式,问题就解决了。
3.3.2 缓冲区生命周期
一个缓冲区从创建到销毁,会经历几个状态:
- 已分配:通过REQBUFS申请后,缓冲区被创建,但还没使用
- 已入队:应用程序通过QBUF把缓冲区放入驱动队列,等待驱动填充数据
- 已出队:驱动填充完数据,应用程序通过DQBUF取出缓冲区,读取数据
- 已释放:通过REQBUFS(count=0)释放所有缓冲区
典型的采集循环是这样的:
// 1. 申请缓冲区
struct v4l2_requestbuffers req = {0};
req.count = 4;
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
req.memory = V4L2_MEMORY_MMAP;
ioctl(fd, VIDIOC_REQBUFS, &req);
// 2. 映射缓冲区
for (i = 0; i < req.count; i++) {
struct v4l2_buffer buf = {0};
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = i;
ioctl(fd, VIDIOC_QUERYBUF, &buf);
// mmap映射
buffers[i].start = mmap(NULL, buf.length,
PROT_READ | PROT_WRITE,
MAP_SHARED, fd, buf.m.offset);
buffers[i].length = buf.length;
}
// 3. 所有缓冲区入队
for (i = 0; i < req.count; i++) {
struct v4l2_buffer buf = {0};
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = i;
ioctl(fd, VIDIOC_QBUF, &buf);
}
// 4. 开始采集
ioctl(fd, VIDIOC_STREAMON, &type);
// 5. 采集循环
while (1) {
struct v4l2_buffer buf = {0};
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
// 出队 - 等待一帧数据
ioctl(fd, VIDIOC_DQBUF, &buf);
// 处理数据
process_frame(buffers[buf.index].start, buf.bytesused);
// 重新入队
ioctl(fd, VIDIOC_QBUF, &buf);
}
3.3.3 常见问题与避坑
缓冲区管理这块,我踩过的坑不少。说几个典型的:
- 缓冲区不够用:如果缓冲区数量太少,驱动填完所有缓冲区后没有可用的空缓冲区,就会丢帧。表现为画面卡顿或撕裂。解决办法是增加缓冲区数量。
- DQBUF阻塞:如果驱动没有新帧,DQBUF会阻塞。这时候要考虑用select/poll来检测是否有数据可读,避免线程卡死。
- 内存泄漏:程序退出时忘了munmap和REQBUFS(count=0),会导致内存泄漏。我在一个长期运行的监控项目里就吃过这个亏,跑了三天后系统内存耗尽。
- 多plane处理:对于多plane格式,每个plane要单独映射。有些新手只映射了第一个plane,结果读取U/V数据时访问了非法地址。
v4l2-ctl工具测试一下。比如v4l2-ctl --list-formats查看支持的格式,v4l2-ctl --stream-mmap --stream-count=10测试采集。如果工具能正常工作,说明驱动没问题,问题出在应用程序。反之,问题在驱动。
好了,V4L2框架的核心内容就这些。数据结构、ioctl流程、缓冲区管理,这三块搞明白了,V4L2开发就算入门了。下一章咱们会讲怎么用V4L2写一个完整的摄像头采集程序,到时候会把这些知识串起来用。