V4L2 视频采集到底怎么工作的?从 videodev2 驱动到 QBUF/DQBUF 完整拆解

我入行做的第一个嵌入式项目就是接一个 MIPI CSI 摄像头到 i.MX6 平台。当时对着 V4L2 文档看了三天——不是我菜,是 V4L2 的文档实在是太分散了。

核心的数据流在驱动和用户态之间怎么转?QBUF 和 DQBUF 到底发生了什么?videodev2 驱动该注册什么 callback?这篇文章我把这些问题全部摸透。


一、先看数据流全景

一个完整的 V4L2 视频采集链路:

MIPI CSI-2
4-lane

ISP处理
Bayer→RGB/YUV

DMA写内存

QBUF入队

硬件开始
捕捉下一帧

DQBUF出队
通知用户态

处理完数据
QBUF放回

Camera Sensor
(OV5640/IMX219)

CSI Host Controller

ISP Pipeline

Video Buffer
(DMA物理地址)

V4L2 Device Driver

用户态App
(FFmpeg/Gstreamer)

最核心的就是 QBUF/DQBUF 循环——V4L2 设计了一套 buffer 借还机制,相当于生产者-消费者模式:

  1. 用户态申请 N 个 buffer(通常 4~6 个)
  2. 所有 buffer QBUF 入队
  3. 调用 VIDIOC_STREAMON 启动采集
  4. 硬件填满一个 buffer → 产生中断 → 驱动把 buffer 标记为 done → 通知用户态
  5. 用户态 DQBUF 拿走已填好的 buffer → 处理数据
  6. 处理完 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,
};

模块加载

REQBUFS

STREAMON
→ start_streaming

QBUF → buf_queue
→ 硬件开始采集
→ 中断 → buffer_done

STREAMOFF
→ stop_streaming

QBUF取消 / 关闭设备

IDLE

BUFFERED

STREAMING


三、从零写一个虚拟 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 原理

用户态虚拟空间

内核虚拟空间

物理内存

dma_alloc_coherent
返回dma_addr

驱动访问

用户程序直接读写

DMA Buffer
(物理连续)

kmap / vmap

mmap映射

DMA_ENG

DRV

APP

关键: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 的核心就三句话:

  1. QBUF 是"借" — 用户态把空 buffer 交给驱动
  2. DQBUF 是"还" — 驱动把填好数据的 buffer 还给用户态
  3. MMAP 是"零拷贝" — 用户态和硬件共享同一块物理内存

整个框架的精巧之处在于它绕过了内核的数据拷贝。驱动写 DMA 地址、硬件填数据、用户态直接读——中间没有一次 memcpy。正是这个设计让嵌入式设备能用 200MHz 的 CPU 处理 1080p30 的视频流。

如果你的摄像头项目 DQBUF 一直超时,先从 dmesgv4l2-ctl 开始查——大概率是 MIPI 时钟或者 sensor 初始化没走对。


参考:Linux kernel 6.6 source (drivers/media/v4l2-core/, include/media/), V4L2 Specification, i.MX6 CSI Driver


最后:公众号【浅止菌】回复 “v4l2” ,可领取本文完整的高清调用序列图。
Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐