V4L2 视频采集到底怎么工作的?从 videodev2 驱动到 QBUF/DQBUF 完整拆解
V4L2 视频采集到底怎么工作的?从 videodev2 驱动到 QBUF/DQBUF 完整拆解
我入行做的第一个嵌入式项目就是接一个 MIPI CSI 摄像头到 i.MX6 平台。当时对着 V4L2 文档看了三天——不是我菜,是 V4L2 的文档实在是太分散了。
核心的数据流在驱动和用户态之间怎么转?QBUF 和 DQBUF 到底发生了什么?videodev2 驱动该注册什么 callback?这篇文章我把这些问题全部摸透。
一、先看数据流全景
一个完整的 V4L2 视频采集链路:
最核心的就是 QBUF/DQBUF 循环——V4L2 设计了一套 buffer 借还机制,相当于生产者-消费者模式:
- 用户态申请 N 个 buffer(通常 4~6 个)
- 所有 buffer QBUF 入队
- 调用
VIDIOC_STREAMON启动采集 - 硬件填满一个 buffer → 产生中断 → 驱动把 buffer 标记为 done → 通知用户态
- 用户态
DQBUF拿走已填好的 buffer → 处理数据 - 处理完
QBUF还回去 → 硬件继续填
核心设计思想:用 DMA 零拷贝,用户态直接操作物理地址映射过来的 buffer,不需要从内核搬数据。
二、V4L2 驱动的核心数据结构
写一个 V4L2 设备驱动,你需要实现这些核心结构体:
2.1 video_device——V4L2 的"字符设备"
struct video_device vdev = {
.fops = &my_v4l2_fops, // file_operations
.ioctl_ops = &my_v4l2_ioctl_ops, // 标准V4L2 ioctl
.release = video_device_release,
.device_caps = V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_STREAMING,
};
2.2 vb2_queue——buffer 队列的核心
struct vb2_queue my_queue = {
.type = V4L2_BUF_TYPE_VIDEO_CAPTURE, // 或VIDEO_CAPTURE_MPLANE
.io_modes = VB2_MMAP | VB2_USERPTR | VB2_DMABUF,
.buf_struct_size = sizeof(struct my_buffer), // 自定义buffer元数据
.ops = &my_vb2_ops, // 最重要的callback集合
.mem_ops = &vb2_dma_contig_memops, // DMA连续内存分配器
.timestamp_flags = V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC,
.min_buffers_needed = 2, // 最少需要2个buffer才能启动
};
2.3 vb2_ops——驱动必须实现的 callback
这是驱动最核心的部分:
static const struct vb2_ops my_vb2_ops = {
.queue_setup = my_queue_setup, // 配置buffer数量和大小
.buf_prepare = my_buf_prepare, // 每个buffer入队前调用
.buf_queue = my_buf_queue, // QBUF入队时调用
.start_streaming = my_start_streaming, // STREAMON触发
.stop_streaming = my_stop_streaming, // STREAMOFF触发
.wait_prepare = vb2_ops_wait_prepare,
.wait_finish = vb2_ops_wait_finish,
};
三、从零写一个虚拟 V4L2 采集驱动
纸上谈兵差不多了。我写一个简化但能跑通的虚拟摄像头驱动——它不依赖真实硬件,用内核定时器模拟帧中断:
3.1 初始化 v4l2_device 和 video_device
#include <media/v4l2-device.h>
#include <media/v4l2-ioctl.h>
#include <media/videobuf2-v4l2.h>
#include <media/videobuf2-dma-contig.h>
struct my_vdev {
struct v4l2_device v4l2_dev;
struct video_device vdev;
struct vb2_queue queue;
struct mutex lock;
spinlock_t slock;
struct timer_list frame_timer; // 模拟帧中断
struct list_head buf_list; // 待处理的buffer链表
int width;
int height;
u32 pixelformat; // V4L2_PIX_FMT_YUYV 等
};
static int my_probe(struct platform_device *pdev)
{
struct my_vdev *vdev;
int ret;
vdev = devm_kzalloc(&pdev->dev, sizeof(*vdev), GFP_KERNEL);
// 1. 初始化 v4l2_device
v4l2_device_register(&pdev->dev, &vdev->v4l2_dev);
// 2. 初始化 vb2_queue
vdev->queue.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
vdev->queue.io_modes = VB2_MMAP;
vdev->queue.ops = &my_vb2_ops;
vdev->queue.mem_ops = &vb2_dma_contig_memops;
vdev->queue.drv_priv = vdev;
vdev->queue.min_buffers_needed = 2;
vdev->queue.timestamp_flags = V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC;
vdev->queue.lock = &vdev->lock;
ret = vb2_queue_init(&vdev->queue);
// 3. 初始化 video_device
vdev->vdev.queue = &vdev->queue;
vdev->vdev.ioctl_ops = &my_ioctl_ops;
vdev->vdev.release = video_device_release_empty;
vdev->vdev.lock = &vdev->lock;
vdev->vdev.v4l2_dev = &vdev->v4l2_dev;
vdev->vdev.device_caps = V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_STREAMING;
ret = video_register_device(&vdev->vdev, VFL_TYPE_VIDEO, -1);
platform_set_drvdata(pdev, vdev);
return 0;
}
3.2 实现 vb2_ops 回调
// REQBUFS 时调用——告诉内核需要什么格式的buffer
static int my_queue_setup(struct vb2_queue *q,
unsigned int *nbuffers,
unsigned int *nplanes,
unsigned int sizes[],
struct device *alloc_devs[])
{
struct my_vdev *vdev = vb2_get_drv_priv(q);
unsigned int size = vdev->width * vdev->height * 2; // YUYV: 16bpp
if (*nplanes)
return sizes[0] < size ? -EINVAL : 0;
*nplanes = 1;
sizes[0] = size;
// 建议至少4个buffer
if (*nbuffers < 4)
*nbuffers = 4;
return 0;
}
// 每个buffer QBUF入队时调用
static void my_buf_queue(struct vb2_buffer *vb)
{
struct my_vdev *vdev = vb2_get_drv_priv(vb->vb2_queue);
struct vb2_v4l2_buffer *vbuf = to_vb2_v4l2_buffer(vb);
unsigned long flags;
spin_lock_irqsave(&vdev->slock, flags);
list_add_tail(&vbuf->done_entry, &vdev->buf_list);
spin_unlock_irqrestore(&vdev->slock, flags);
}
// STREAMON 时调用——启动采集
static int my_start_streaming(struct vb2_queue *q, unsigned int count)
{
struct my_vdev *vdev = vb2_get_drv_priv(q);
// 启动定时器,每33ms触发一次(模拟30fps)
mod_timer(&vdev->frame_timer, jiffies + HZ / 30);
return 0;
}
// STREAMOFF 时调用——停止采集
static void my_stop_streaming(struct vb2_queue *q)
{
struct my_vdev *vdev = vb2_get_drv_priv(q);
struct vb2_v4l2_buffer *vbuf, *tmp;
unsigned long flags;
del_timer(&vdev->frame_timer);
// 把所有pending buffer还给vb2框架
spin_lock_irqsave(&vdev->slock, flags);
list_for_each_entry_safe(vbuf, tmp, &vdev->buf_list, done_entry) {
list_del(&vbuf->done_entry);
vb2_buffer_done(&vbuf->vb2_buf, VB2_BUF_STATE_ERROR);
}
spin_unlock_irqrestore(&vdev->slock, flags);
}
3.3 定时器模拟帧完成中断
static void my_frame_timer_cb(struct timer_list *t)
{
struct my_vdev *vdev = from_timer(vdev, t, frame_timer);
struct vb2_v4l2_buffer *vbuf = NULL;
// 从链表拿一个buffer出来,填上测试数据
if (!list_empty(&vdev->buf_list)) {
vbuf = list_first_entry(&vdev->buf_list,
struct vb2_v4l2_buffer,
done_entry);
list_del(&vbuf->done_entry);
// 这里模拟硬件写DMA——实际驱动应该从硬件读
// 我们填一个彩条测试图案
fill_test_pattern(vb2_plane_vaddr(&vbuf->vb2_buf, 0),
vdev->width, vdev->height);
// 设置时间戳
vbuf->vb2_buf.timestamp = ktime_get_ns();
vbuf->sequence = vdev->sequence++;
vbuf->field = V4L2_FIELD_NONE;
// 通知vb2框架这个buffer完成
vb2_buffer_done(&vbuf->vb2_buf, VB2_BUF_STATE_DONE);
}
// 重新调度下一次帧中断
mod_timer(&vdev->frame_timer, jiffies + HZ / 30);
}
四、用户态怎么调用
驱动的活干完了,看看用户态怎么用:
#include <linux/videodev2.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <fcntl.h>
int main()
{
int fd = open("/dev/video0", O_RDWR);
struct v4l2_format fmt = {0};
struct v4l2_requestbuffers req = {0};
struct v4l2_buffer buf = {0};
struct buffer buffers[4];
// 1. 设置格式
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt.fmt.pix.width = 1920;
fmt.fmt.pix.height = 1080;
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
fmt.fmt.pix.field = V4L2_FIELD_NONE;
ioctl(fd, VIDIOC_S_FMT, &fmt);
// 2. 申请4个buffer
req.count = 4;
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
req.memory = V4L2_MEMORY_MMAP;
ioctl(fd, VIDIOC_REQBUFS, &req);
// 3. 查询并mmap每个buffer
for (int i = 0; i < req.count; i++) {
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = i;
ioctl(fd, VIDIOC_QUERYBUF, &buf);
buffers[i].length = buf.length;
buffers[i].start = mmap(NULL, buf.length,
PROT_READ | PROT_WRITE,
MAP_SHARED, fd, buf.m.offset);
}
// 4. 所有buffer入队
for (int i = 0; i < req.count; i++) {
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = i;
ioctl(fd, VIDIOC_QBUF, &buf);
}
// 5. 开始采集
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ioctl(fd, VIDIOC_STREAMON, &type);
// 6. 采集循环
for (int frame = 0; frame < 300; frame++) {
fd_set fds;
FD_ZERO(&fds);
FD_SET(fd, &fds);
// 用select等待帧完成
select(fd + 1, &fds, NULL, NULL, NULL);
// DQBUF取走完成帧
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
ioctl(fd, VIDIOC_DQBUF, &buf);
// 处理帧数据(这里可以传给FFmpeg编码或显示)
process_frame(buffers[buf.index].start, buf.bytesused);
// 处理完,QBUF还回去
ioctl(fd, VIDIOC_QBUF, &buf);
}
// 7. 停止
type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ioctl(fd, VIDIOC_STREAMOFF, &type);
// 清理
for (int i = 0; i < req.count; i++)
munmap(buffers[i].start, buffers[i].length);
close(fd);
}
五、四种 buffer IO 模式对比
V4L2 支持 4 种用户态与驱动交换 buffer 的方式:
| 模式 | 用户态分配 | 内核分配 | 拷贝次数 | 典型场景 |
|---|---|---|---|---|
| MMAP | 否 | 是 | 0(指针共享) | 默认选择,最常用 |
| USERPTR | 是 | 否 | 0 | 用户态自行管理内存池 |
| DMABUF | 是(DMA-BUF fd) | 否 | 0 | 跨设备共享(GPU→V4L2) |
| READ/WRITE | — | — | 1次拷贝 | 老旧设备兼容 |
MMAP 原理
关键:MMAP 模式下,用户态和内核态共享同一块物理内存。硬件 DMA 把帧数据写入这块内存,用户态直接就看到了——零拷贝。
六、V4L2 的 v4l2_subdev 机制
实际项目中摄像头不是直接挂在 CPU 总线上的,中间有 CSI 控制器和 MIPI 桥接芯片。V4L2 引入了 subdev(子设备)来管理这条链路:
// Sensor 子设备驱动
static const struct v4l2_subdev_video_ops sensor_video_ops = {
.s_stream = sensor_s_stream, // 开启/关闭图像数据流
.g_frame_interval = sensor_g_fmt, // 获取帧率
};
static const struct v4l2_subdev_core_ops sensor_core_ops = {
.init = sensor_init,
};
static const struct v4l2_subdev_ops sensor_ops = {
.video = &sensor_video_ops,
.core = &sensor_core_ops,
};
完整链路通常是这样:
Sensor (i2c subdev) → MIPI CSI-2 Receiver (video device) → ISP (video device) → 用户态
每个环节是一个 v4l2_subdev,通过 media controller 框架来路由数据流。
七、调试工具集
7.1 v4l2-ctl
# 列举所有V4L2设备
v4l2-ctl --list-devices
# 输出类似:
# i.MX6 CSI Camera (platform:30a20000.csi):
# /dev/video0
# 查看支持的格式
v4l2-ctl -d /dev/video0 --list-formats
# ioctl: VIDIOC_ENUM_FMT
# Type: Video Capture
# [0]: 'YUYV' (YUYV 4:2:2)
# [1]: 'NV12' (Y/CbCr 4:2:0)
# [2]: 'JPEG' (JFIF JPEG)
# 设置格式并单帧采集
v4l2-ctl -d /dev/video0 --set-fmt-video=width=1920,height=1080,pixelformat=YUYV
v4l2-ctl -d /dev/video0 --stream-mmap --stream-to=frame.yuv --stream-count=1
7.2 media-ctl
# 查看media controller拓扑
media-ctl -d /dev/media0 -p
# 配置路由
media-ctl -d /dev/media0 -V '"ov5640 1-003c":0 [fmt:UYVY/640x480]'
7.3 strace 抓取 VIDIOC_ 调用
# 抓取FFmpeg使用V4L2的ioctl调用
strace -e ioctl ffmpeg -f v4l2 -i /dev/video0 -frames 1 out.jpg 2>&1 | \
grep VIDIOC
八、常见问题
坑1:DQBUF 阻塞超时
用户态 DQBUF 在没有帧可用时会阻塞。如果摄像头没信号(线松了、MIPI 时钟没了),DQBUF 会一直等。
解决:用 select()/poll() 设置超时:
struct pollfd pfd = { .fd = fd, .events = POLLIN };
int ret = poll(&pfd, 1, 1000); // 1秒超时
if (ret == 0) {
// 超时,重新初始化
ioctl(fd, VIDIOC_STREAMOFF, &type);
ioctl(fd, VIDIOC_STREAMON, &type);
}
坑2:USERPTR 内存对齐要求
USERPTR 模式下,用户态分配的 buffer 必须满足 cache line 对齐(ARM 上通常是 64 字节)。不对齐的话,驱动在 DMA 时可能拿到错误的数据。
坑3:MIPI CSI 的时钟同步
CSI 接口的 D-PHY clock lane 和 data lane 之间有时序要求。常见翻车:Sensor 的帧率和 CSI host 的接收能力不匹配,导致 MIPI CRC 错误。
# 通过debugfs检查MIPI错误计数
cat /sys/kernel/debug/csi/errors
# CRC: 217 ← 说明信号有问题
坑4:QBUF 和 DQBUF 的顺序
同一个 buffer 不能同时被硬件填和用户态读。V4L2 严格保证:一个 buffer 从驱动出来(DQBUF)之前,硬件不会再碰它。但你如果把 VIDIOC_DQBUF 拿到的 buffer 放回去后忘了 QBUF,buffer 就会从循环中消失,最终 DQBUF 会一直阻塞。
总结
V4L2 的核心就三句话:
- QBUF 是"借" — 用户态把空 buffer 交给驱动
- DQBUF 是"还" — 驱动把填好数据的 buffer 还给用户态
- MMAP 是"零拷贝" — 用户态和硬件共享同一块物理内存
整个框架的精巧之处在于它绕过了内核的数据拷贝。驱动写 DMA 地址、硬件填数据、用户态直接读——中间没有一次 memcpy。正是这个设计让嵌入式设备能用 200MHz 的 CPU 处理 1080p30 的视频流。
如果你的摄像头项目 DQBUF 一直超时,先从 dmesg 和 v4l2-ctl 开始查——大概率是 MIPI 时钟或者 sensor 初始化没走对。
参考:Linux kernel 6.6 source (drivers/media/v4l2-core/ , include/media/ ), V4L2 Specification, i.MX6 CSI Driver
最后:公众号【浅止菌】回复 “v4l2” ,可领取本文完整的高清调用序列图。
更多推荐

所有评论(0)