3、V4L2驱动框架详解:核心数据结构、ioctl调用流程、视频采集流程
V4L2,全称 Video for Linux 2,是 Linux 内核里处理视频设备的统一接口。说白了,它就是摄像头和应用程序之间的翻译官。你上层调 open、ioctl、mmap,底层怎么跟硬件寄存器打交道,V4L2 帮你封装好了。
我刚开始接触 V4L2 时,觉得数据结构又多又绕。后来调了几个 sensor,才摸清门道。今天咱们就把它的核心骨架拆开看看。
3.1 核心数据结构:V4L2 的骨架
V4L2 驱动里,有几个结构体你绕不开。我习惯把它们分成三层:设备层、队列层、缓冲区层。
| 层级 | 核心结构体 | 作用 |
|---|---|---|
| 设备层 | struct video_device | 注册字符设备,暴露 /dev/videoX |
| 队列层 | struct vb2_queue | 管理缓冲区队列,处理入队出队 |
| 缓冲区层 | struct vb2_buffer | 代表一帧数据,含 dma 地址、长度、状态 |
这三个结构体,是 V4L2 驱动的基石。你写驱动,本质上就是在填充和操作它们。
3.1.1 video_device:暴露给用户的门面
video_device 负责注册字符设备。它内部有个 v4l2_file_operations,定义了 open、close、ioctl 等回调。我一般这样初始化:
static struct video_device vdev = {
.name = "my_camera",
.vfl_dir = VFL_DIR_RX,
.fops = &my_fops,
.ioctl_ops = &my_ioctl_ops,
.release = video_device_release_empty,
};
嗯,这里要注意:release 回调不能乱填。我曾经图省事填了个 NULL,结果卸载模块时内核直接 panic。老老实实用 video_device_release_empty 或者自己实现一个。
3.1.2 vb2_queue:数据流的调度中心
vb2_queue 是 videobuf2 框架的核心。它管理着所有缓冲区的生命周期。你想想看,应用程序调用 QBUF 把缓冲区交给你,你填完数据再通过 DQBUG 还回去——这一切都由 vb2_queue 调度。
初始化时,你需要实现一组 vb2_ops:
static const struct vb2_ops my_vb2_ops = {
.queue_setup = my_queue_setup,
.buf_prepare = my_buf_prepare,
.buf_queue = my_buf_queue,
.start_streaming = my_start_streaming,
.stop_streaming = my_stop_streaming,
};
我个人习惯把 queue_setup 放在最前面写。它决定了缓冲区数量和大小。如果这里算错了,后面采集时要么内存不够,要么帧率上不去。
3.1.3 vb2_buffer:一帧数据的容器
vb2_buffer 代表一个缓冲区。它里面存着 dma 地址、长度、时间戳、状态标志。驱动里你拿到它,往里面填数据,然后标记完成。
关键字段:
planes[]:多平面时,每个平面一个 entrytimestamp:时间戳,我踩过坑——忘了赋值,结果应用层拿到的时间全是 0state:缓冲区状态,VB2_BUF_STATE_DONE 表示数据已准备好
vb2_buffer.state,没加锁。结果偶发性的 DQBUG 返回空数据。后来老老实实用 vb2_buffer_done() 接口,再也没出过问题。
3.2 ioctl 调用流程:从用户态到内核态
应用程序调用 ioctl(fd, VIDIOC_QBUF, &buf),这中间发生了什么?我画了个流程图,你看一眼就明白了。
流程其实很清晰:
- 应用调用
ioctl(fd, cmd, arg) - 进入内核
sys_ioctl(),通过文件操作找到v4l2_ioctl() v4l2_ioctl()根据 cmd 查表,调用你注册的ioctl_ops回调- 你的回调里操作硬件,返回结果
为什么 V4L2 要搞这么一层分发?说白了,是为了统一接口。不管你接的是 USB 摄像头还是 MIPI sensor,应用层调用的 VIDIOC_* 命令都一样。驱动开发者只需要填好回调表就行。
printk(KERN_DEBUG "cmd: 0x%x\n", cmd)。这样能快速确认应用层发来的命令是否被正确路由。曾经有个项目,应用层传错了参数,我靠这行日志半小时定位了问题。
3.3 视频采集流程:一帧数据的一生
视频采集,说白了就是:申请缓冲区 → 入队 → 启动流 → 硬件填数据 → 出队 → 处理 → 再入队。这个循环跑起来,摄像头就活了。
我把它拆成 5 步:
| 步骤 | 应用层调用 | 驱动层响应 |
|---|---|---|
| 1. 申请缓冲区 | VIDIOC_REQBUFS | 调用 queue_setup,分配内存 |
| 2. 查询并映射 | VIDIOC_QUERYBUF + mmap | 返回缓冲区信息,建立用户态映射 |
| 3. 入队 | VIDIOC_QBUF | 调用 buf_prepare,缓冲区加入队列 |
| 4. 启动流 | VIDIOC_STREAMON | 调用 start_streaming,开启硬件采集 |
| 5. 出队处理 | VIDIOC_DQBUF | 等待硬件填完数据,返回给应用 |
这里有个关键点:入队和出队是异步的。硬件在后台填数据,应用在前台取数据。如果队列空了,DQBUF 会阻塞,直到有数据回来。
3.3.1 中断处理:数据回来的那一刻
硬件采集完一帧,会触发中断。驱动在中断处理函数里要做三件事:
- 从硬件寄存器读取帧完成标志
- 获取当前正在使用的
vb2_buffer - 调用
vb2_buffer_done()标记完成
代码大概长这样:
static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
struct my_device *dev = dev_id;
struct vb2_buffer *vb;
// 1. 检查中断状态
if (!(readl(dev->base + REG_STATUS) & FRAME_DONE))
return IRQ_NONE;
// 2. 获取当前缓冲区
vb = dev->cur_vb;
if (!vb)
return IRQ_HANDLED;
// 3. 设置时间戳
vb->timestamp = ktime_get_ns();
// 4. 标记完成
vb2_buffer_done(vb, VB2_BUF_STATE_DONE);
// 5. 启动下一帧(如果有)
my_start_capture(dev);
return IRQ_HANDLED;
}
printk 调试,结果帧率直接掉了一半。中断服务函数要快进快出,复杂逻辑放到 tasklet 或 workqueue 里。
3.3.2 流控制:start 和 stop 的配合
start_streaming 回调里,你要开启硬件采集。但有个细节:如果硬件启动失败,你得把已经入队的缓冲区全部归还。否则应用层会一直等 DQBUF,死锁。
我踩过这个坑。当时硬件初始化失败,我直接返回了错误码,但没清理队列。结果应用层卡在 DQBUF 上,kill 都杀不掉。后来在 start_streaming 失败路径里加了:
if (ret) {
spin_lock_irqsave(&q->lock, flags);
list_for_each_entry_safe(vb, tmp, &q->queued_list, queued_entry) {
list_del(&vb->queued_entry);
vb2_buffer_done(vb, VB2_BUF_STATE_ERROR);
}
spin_unlock_irqrestore(&q->lock, flags);
}
嗯,从此再也没死锁过。
3.4 小结:V4L2 驱动的心法
V4L2 框架,说白了就是一套约定。你遵守它的数据结构,实现它的回调,它帮你搞定字符设备、内存管理、用户态交互。你只需要关注硬件操作本身。
我个人觉得,学 V4L2 最好的方法就是读一个现成的驱动。比如内核里 drivers/media/platform/ 下的那些,挑一个简单的 sensor 驱动,从头到尾读一遍。数据结构怎么初始化,回调怎么注册,中断怎么处理——看一遍比看十遍文档都管用。