一、顶级架构一句话总结

视频设备 → V4L2框架 → 缓冲区管理 → DMA传输 → 用户空间

V4L2(Video for Linux 2)是Linux视频设备的标准框架,专为摄像头、电视卡、视频采集卡等设备设计。


二、V4L2核心概念

V4L2设备类型

设备类型描述主设备号
视频捕获/dev/videoX81
视频输出/dev/videoX81
VBI设备/dev/vbiX81
收音机/dev/radioX81

视频格式

  • RGB格式:RGB24、RGB32、BGR24
  • YUV格式:YUYV、UYVY、NV12、NV21、YUV420P
  • 压缩格式:MJPEG、H.264、H.265

三、V4L2核心数据结构

video_device

#include <linux/videodev2.h>
#include <media/v4l2-device.h>
#include <media/v4l2-dev.h>

struct video_device {
    const struct v4l2_file_operations *fops;
    struct device dev;
    struct cdev *cdev;

    int minor;
    int num;
    char name[32];

    struct v4l2_device *v4l2_dev;
    void *priv;

    enum vfl_devnode_type vfl_type;
    enum vfl_direction vfl_dir;

    struct mutex lock;
    atomic_t refcount;
};

v4l2_device

struct v4l2_device {
    struct device *dev;
    struct media_device *mdev;

    struct list_head subdevs;
    spinlock_t lock;

    char name[32];

    void (*release)(struct v4l2_device *v4l2_dev);
};

vb2_queue(videobuf2队列)

#include <media/videobuf2-core.h>

struct vb2_queue {
    const struct vb2_ops *ops;
    void *drv_priv;

    enum vb2_state state;
    struct mutex lock;
    spinlock_t done_lock;

    struct list_head queued_list;
    struct list_head done_list;

    unsigned int num_buffers;
    unsigned int buffer_count;

    struct vb2_buffer *bufs[32];
};

四、V4L2文件操作

v4l2_file_operations

struct v4l2_file_operations {
    struct module *owner;

    ssize_t (*read)(struct file *, char __user *, size_t, loff_t *);
    ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *);

    unsigned int (*poll)(struct file *, struct poll_table_struct *);

    long (*ioctl)(struct file *, unsigned int, unsigned long);
    long (*compat_ioctl)(struct file *, unsigned int, unsigned long);

    int (*mmap)(struct file *, struct vm_area_struct *);

    int (*open)(struct file *);
    int (*release)(struct file *);
};

关键ioctl命令

// 查询设备能力
VIDIOC_QUERYCAP

// 设置/获取格式
VIDIOC_S_FMT
VIDIOC_G_FMT
VIDIOC_TRY_FMT

// 请求缓冲区
VIDIOC_REQBUFS

// 查询缓冲区
VIDIOC_QUERYBUF

// 缓冲区入队
VIDIOC_QBUF

// 缓冲区出队
VIDIOC_DQBUF

// 启动/停止流
VIDIOC_STREAMON
VIDIOC_STREAMOFF

五、简单V4L2驱动示例

虚拟摄像头驱动

#include <linux/module.h>
#include <linux/platform_device.h>
#include <media/v4l2-device.h>
#include <media/v4l2-dev.h>
#include <media/v4l2-ioctl.h>
#include <media/videobuf2-core.h>
#include <media/videobuf2-vmalloc.h>

#define DRIVER_NAME "virtual_camera"
#define WIDTH 640
#define HEIGHT 480
#define FORMAT V4L2_PIX_FMT_YUYV

struct virtual_camera_dev {
    struct v4l2_device v4l2_dev;
    struct video_device vdev;
    struct vb2_queue vb_queue;

    struct mutex lock;
    int streaming;

    u8 *frame_buffer;
    unsigned int sequence;
    struct timer_list timer;
};

static struct virtual_camera_dev *cam_dev;

static void virtual_camera_generate_frame(struct virtual_camera_dev *dev)
{
    int i, j;
    u8 *p = dev->frame_buffer;

    for (i = 0; i < HEIGHT; i++) {
        for (j = 0; j < WIDTH; j += 2) {
            *p++ = 0x40 + (i + dev->sequence) % 128;  // Y
            *p++ = 0x80;                                  // U
            *p++ = 0x40 + (j + dev->sequence) % 128;  // Y
            *p++ = 0x80;                                  // V
        }
    }
    dev->sequence++;
}

static void virtual_camera_timer(unsigned long data)
{
    struct virtual_camera_dev *dev = (struct virtual_camera_dev *)data;
    struct vb2_buffer *vb;
    unsigned long flags;

    if (!dev->streaming)
        return;

    spin_lock_irqsave(&dev->vb_queue.done_lock, flags);

    if (list_empty(&dev->vb_queue.queued_list)) {
        spin_unlock_irqrestore(&dev->vb_queue.done_lock, flags);
        mod_timer(&dev->timer, jiffies + msecs_to_jiffies(33));
        return;
    }

    vb = list_first_entry(&dev->vb_queue.queued_list,
                          struct vb2_buffer, queued_entry);
    list_del(&vb->queued_entry);

    spin_unlock_irqrestore(&dev->vb_queue.done_lock, flags);

    virtual_camera_generate_frame(dev);

    void *vaddr = vb2_plane_vaddr(vb, 0);
    unsigned int size = vb2_plane_size(vb, 0);

    memcpy(vaddr, dev->frame_buffer, min(size, (unsigned int)(WIDTH * HEIGHT * 2)));

    vb->v4l2_buf.sequence = dev->sequence;
    vb->v4l2_buf.timestamp.tv_sec = ktime_get_real_seconds();
    vb->v4l2_buf.timestamp.tv_usec = ktime_get_real_ns() % NSEC_PER_SEC / 1000;

    vb2_buffer_done(vb, VB2_BUF_STATE_DONE);

    mod_timer(&dev->timer, jiffies + msecs_to_jiffies(33));
}

static int virtual_camera_queue_setup(struct vb2_queue *vq,
                                       unsigned int *nbuffers,
                                       unsigned int *nplanes,
                                       unsigned int sizes[],
                                       struct device *alloc_devs[])
{
    struct virtual_camera_dev *dev = vb2_get_drv_priv(vq);

    if (*nplanes != 1)
        return -EINVAL;

    sizes[0] = WIDTH * HEIGHT * 2;

    return 0;
}

static int virtual_camera_buf_prepare(struct vb2_buffer *vb)
{
    struct vb2_queue *vq = vb->vb2_queue;
    struct virtual_camera_dev *dev = vb2_get_drv_priv(vq);

    if (vb2_plane_size(vb, 0) < WIDTH * HEIGHT * 2)
        return -EINVAL;

    vb2_set_plane_payload(vb, 0, WIDTH * HEIGHT * 2);

    return 0;
}

static void virtual_camera_buf_queue(struct vb2_buffer *vb)
{
    struct vb2_queue *vq = vb->vb2_queue;
    struct virtual_camera_dev *dev = vb2_get_drv_priv(vq);
    unsigned long flags;

    spin_lock_irqsave(&vq->done_lock, flags);
    list_add_tail(&vb->queued_entry, &vq->queued_list);
    spin_unlock_irqrestore(&vq->done_lock, flags);
}

static int virtual_camera_start_streaming(struct vb2_queue *vq, unsigned int count)
{
    struct virtual_camera_dev *dev = vb2_get_drv_priv(vq);

    dev->streaming = 1;
    mod_timer(&dev->timer, jiffies + msecs_to_jiffies(33));

    return 0;
}

static void virtual_camera_stop_streaming(struct vb2_queue *vq)
{
    struct virtual_camera_dev *dev = vb2_get_drv_priv(vq);
    struct vb2_buffer *vb;
    unsigned long flags;

    dev->streaming = 0;
    del_timer_sync(&dev->timer);

    spin_lock_irqsave(&vq->done_lock, flags);
    while (!list_empty(&vq->queued_list)) {
        vb = list_first_entry(&vq->queued_list,
                              struct vb2_buffer, queued_entry);
        list_del(&vb->queued_entry);
        vb2_buffer_done(vb, VB2_BUF_STATE_ERROR);
    }
    spin_unlock_irqrestore(&vq->done_lock, flags);
}

static const struct vb2_ops virtual_camera_vb2_ops = {
    .queue_setup     = virtual_camera_queue_setup,
    .buf_prepare     = virtual_camera_buf_prepare,
    .buf_queue       = virtual_camera_buf_queue,
    .start_streaming = virtual_camera_start_streaming,
    .stop_streaming  = virtual_camera_stop_streaming,
};

static int virtual_camera_querycap(struct file *file, void *fh,
                                    struct v4l2_capability *cap)
{
    strcpy(cap->driver, DRIVER_NAME);
    strcpy(cap->card, "Virtual Camera");
    strcpy(cap->bus_info, "platform:virtual_camera");

    cap->device_caps = V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_STREAMING;
    cap->capabilities = cap->device_caps | V4L2_CAP_DEVICE_CAPS;

    return 0;
}

static int virtual_camera_enum_fmt_vid_cap(struct file *file, void *fh,
                                             struct v4l2_fmtdesc *f)
{
    if (f->index != 0)
        return -EINVAL;

    f->pixelformat = FORMAT;
    strcpy(f->description, "YUYV 4:2:2");

    return 0;
}

static int virtual_camera_g_fmt_vid_cap(struct file *file, void *fh,
                                          struct v4l2_format *f)
{
    f->fmt.pix.width = WIDTH;
    f->fmt.pix.height = HEIGHT;
    f->fmt.pix.pixelformat = FORMAT;
    f->fmt.pix.field = V4L2_FIELD_INTERLACED;
    f->fmt.pix.bytesperline = WIDTH * 2;
    f->fmt.pix.sizeimage = WIDTH * HEIGHT * 2;
    f->fmt.pix.colorspace = V4L2_COLORSPACE_SRGB;

    return 0;
}

static int virtual_camera_s_fmt_vid_cap(struct file *file, void *fh,
                                          struct v4l2_format *f)
{
    if (f->fmt.pix.pixelformat != FORMAT)
        return -EINVAL;

    f->fmt.pix.width = WIDTH;
    f->fmt.pix.height = HEIGHT;
    f->fmt.pix.field = V4L2_FIELD_INTERLACED;
    f->fmt.pix.bytesperline = WIDTH * 2;
    f->fmt.pix.sizeimage = WIDTH * HEIGHT * 2;
    f->fmt.pix.colorspace = V4L2_COLORSPACE_SRGB;

    return 0;
}

static int virtual_camera_try_fmt_vid_cap(struct file *file, void *fh,
                                            struct v4l2_format *f)
{
    f->fmt.pix.width = WIDTH;
    f->fmt.pix.height = HEIGHT;
    f->fmt.pix.field = V4L2_FIELD_INTERLACED;
    f->fmt.pix.bytesperline = WIDTH * 2;
    f->fmt.pix.sizeimage = WIDTH * HEIGHT * 2;
    f->fmt.pix.colorspace = V4L2_COLORSPACE_SRGB;

    return 0;
}

static const struct v4l2_ioctl_ops virtual_camera_ioctl_ops = {
    .vidioc_querycap      = virtual_camera_querycap,
    .vidioc_enum_fmt_vid_cap = virtual_camera_enum_fmt_vid_cap,
    .vidioc_g_fmt_vid_cap = virtual_camera_g_fmt_vid_cap,
    .vidioc_s_fmt_vid_cap = virtual_camera_s_fmt_vid_cap,
    .vidioc_try_fmt_vid_cap = virtual_camera_try_fmt_vid_cap,
    .vidioc_reqbufs       = vb2_ioctl_reqbufs,
    .vidioc_querybuf      = vb2_ioctl_querybuf,
    .vidioc_qbuf          = vb2_ioctl_qbuf,
    .vidioc_dqbuf         = vb2_ioctl_dqbuf,
    .vidioc_streamon      = vb2_ioctl_streamon,
    .vidioc_streamoff     = vb2_ioctl_streamoff,
};

static const struct v4l2_file_operations virtual_camera_fops = {
    .owner        = THIS_MODULE,
    .open         = v4l2_fh_open,
    .release      = vb2_fop_release,
    .read         = vb2_fop_read,
    .mmap         = vb2_fop_mmap,
    .poll         = vb2_fop_poll,
    .unlocked_ioctl = video_ioctl2,
};

static int virtual_camera_probe(struct platform_device *pdev)
{
    int ret;

    cam_dev = devm_kzalloc(&pdev->dev, sizeof(*cam_dev), GFP_KERNEL);
    if (!cam_dev)
        return -ENOMEM;

    mutex_init(&cam_dev->lock);

    cam_dev->frame_buffer = devm_kmalloc(&pdev->dev, WIDTH * HEIGHT * 2, GFP_KERNEL);
    if (!cam_dev->frame_buffer)
        return -ENOMEM;

    ret = v4l2_device_register(&pdev->dev, &cam_dev->v4l2_dev);
    if (ret)
        return ret;

    cam_dev->vb_queue.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    cam_dev->vb_queue.io_modes = VB2_MMAP | VB2_USERPTR | VB2_DMABUF;
    cam_dev->vb_queue.drv_priv = cam_dev;
    cam_dev->vb_queue.buf_struct_size = sizeof(struct vb2_buffer);
    cam_dev->vb_queue.ops = &virtual_camera_vb2_ops;
    cam_dev->vb_queue.mem_ops = &vb2_vmalloc_memops;
    cam_dev->vb_queue.lock = &cam_dev->lock;

    ret = vb2_queue_init(&cam_dev->vb_queue);
    if (ret) {
        v4l2_device_unregister(&cam_dev->v4l2_dev);
        return ret;
    }

    strcpy(cam_dev->vdev.name, DRIVER_NAME);
    cam_dev->vdev.fops = &virtual_camera_fops;
    cam_dev->vdev.ioctl_ops = &virtual_camera_ioctl_ops;
    cam_dev->vdev.v4l2_dev = &cam_dev->v4l2_dev;
    cam_dev->vdev.release = video_device_release_empty;
    cam_dev->vdev.queue = &cam_dev->vb_queue;
    cam_dev->vdev.vfl_type = VFL_TYPE_GRABBER;
    cam_dev->vdev.vfl_dir = VFL_DIR_RX;

    video_set_drvdata(&cam_dev->vdev, cam_dev);

    ret = video_register_device(&cam_dev->vdev, VFL_TYPE_GRABBER, -1);
    if (ret) {
        vb2_queue_release(&cam_dev->vb_queue);
        v4l2_device_unregister(&cam_dev->v4l2_dev);
        return ret;
    }

    setup_timer(&cam_dev->timer, virtual_camera_timer, (unsigned long)cam_dev);

    platform_set_drvdata(pdev, cam_dev);

    pr_info("Virtual camera driver probed: /dev/video%d\n", cam_dev->vdev.num);

    return 0;
}

static int virtual_camera_remove(struct platform_device *pdev)
{
    struct virtual_camera_dev *dev = platform_get_drvdata(pdev);

    del_timer_sync(&dev->timer);
    video_unregister_device(&dev->vdev);
    vb2_queue_release(&dev->vb_queue);
    v4l2_device_unregister(&dev->v4l2_dev);

    pr_info("Virtual camera driver removed\n");

    return 0;
}

static struct platform_driver virtual_camera_driver = {
    .probe  = virtual_camera_probe,
    .remove = virtual_camera_remove,
    .driver = {
        .name = DRIVER_NAME,
    },
};

static struct platform_device *virtual_camera_device;

static int __init virtual_camera_init(void)
{
    int ret;

    ret = platform_driver_register(&virtual_camera_driver);
    if (ret)
        return ret;

    virtual_camera_device = platform_device_alloc(DRIVER_NAME, -1);
    if (!virtual_camera_device) {
        platform_driver_unregister(&virtual_camera_driver);
        return -ENOMEM;
    }

    ret = platform_device_add(virtual_camera_device);
    if (ret) {
        platform_device_put(virtual_camera_device);
        platform_driver_unregister(&virtual_camera_driver);
        return ret;
    }

    pr_info("Virtual camera driver loaded\n");

    return 0;
}

static void __exit virtual_camera_exit(void)
{
    platform_device_unregister(virtual_camera_device);
    platform_driver_unregister(&virtual_camera_driver);

    pr_info("Virtual camera driver unloaded\n");
}

module_init(virtual_camera_init);
module_exit(virtual_camera_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Driver Developer");
MODULE_DESCRIPTION("Virtual V4L2 Camera Driver");

六、videobuf2操作

vb2_queue初始化

vb2_queue.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
vb2_queue.io_modes = VB2_MMAP | VB2_USERPTR | VB2_DMABUF;
vb2_queue.drv_priv = dev;
vb2_queue.ops = &my_vb2_ops;
vb2_queue.mem_ops = &vb2_vmalloc_memops;
vb2_queue.lock = &dev->lock;

vb2_queue_init(&vb2_queue);

vb2_ops回调

struct vb2_ops {
    int (*queue_setup)(struct vb2_queue *vq, unsigned int *nbuffers,
                       unsigned int *nplanes, unsigned int sizes[],
                       struct device *alloc_devs[]);

    int (*buf_prepare)(struct vb2_buffer *vb);
    void (*buf_queue)(struct vb2_buffer *vb);

    int (*start_streaming)(struct vb2_queue *vq, unsigned int count);
    void (*stop_streaming)(struct vb2_queue *vq);

    void (*buf_cleanup)(struct vb2_buffer *vb);
    int (*buf_init)(struct vb2_buffer *vb);
};

缓冲区完成

vb2_buffer_done(vb, VB2_BUF_STATE_DONE);
vb2_buffer_done(vb, VB2_BUF_STATE_ERROR);

七、用户空间V4L2应用示例

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <linux/videodev2.h>

#define WIDTH 640
#define HEIGHT 480
#define BUFFER_COUNT 4

struct buffer {
    void *start;
    size_t length;
};

int main() {
    int fd = open("/dev/video0", O_RDWR);
    if (fd < 0) {
        perror("open");
        return 1;
    }

    struct v4l2_capability cap;
    ioctl(fd, VIDIOC_QUERYCAP, &cap);
    printf("Driver: %s\nCard: %s\n", cap.driver, cap.card);

    struct v4l2_format fmt = {0};
    fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    fmt.fmt.pix.width = WIDTH;
    fmt.fmt.pix.height = HEIGHT;
    fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
    fmt.fmt.pix.field = V4L2_FIELD_INTERLACED;
    ioctl(fd, VIDIOC_S_FMT, &fmt);

    struct v4l2_requestbuffers req = {0};
    req.count = BUFFER_COUNT;
    req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    req.memory = V4L2_MEMORY_MMAP;
    ioctl(fd, VIDIOC_REQBUFS, &req);

    struct buffer *buffers = calloc(req.count, sizeof(*buffers));

    for (int 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);

        buffers[i].length = buf.length;
        buffers[i].start = mmap(NULL, buf.length, PROT_READ | PROT_WRITE,
                                 MAP_SHARED, fd, buf.m.offset);
    }

    for (int 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);
    }

    enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    ioctl(fd, VIDIOC_STREAMON, &type);

    for (int i = 0; i < 10; i++) {
        struct v4l2_buffer buf = {0};
        buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        buf.memory = V4L2_MEMORY_MMAP;
        ioctl(fd, VIDIOC_DQBUF, &buf);

        printf("Frame %d: seq=%u, ts=%ld.%06ld\n", i,
               buf.sequence, buf.timestamp.tv_sec, buf.timestamp.tv_usec);

        ioctl(fd, VIDIOC_QBUF, &buf);
    }

    ioctl(fd, VIDIOC_STREAMOFF, &type);

    for (int i = 0; i < req.count; i++)
        munmap(buffers[i].start, buffers[i].length);

    free(buffers);
    close(fd);

    return 0;
}

八、调试技巧

查看V4L2设备

# 查看视频设备
ls -l /dev/video*

# 查看设备信息
v4l2-ctl --all

# 列出支持的格式
v4l2-ctl --list-formats

# 列出支持的分辨率
v4l2-ctl --list-formats-ext

测试视频采集

# 采集一帧图像
v4l2-ctl --stream-mmap=3 --stream-count=1 --stream-to=frame.raw

# 使用ffplay播放
ffplay -f rawvideo -pixel_format yuyv422 -video_size 640x480 frame.raw

九、终极总结

V4L2驱动 = 视频设备的标准框架

  • 核心结构:video_device、v4l2_device、vb2_queue
  • 缓冲区管理:videobuf2管理DMA缓冲区
  • 两种模式:read()、mmap()/streaming
  • 关键ioctl:QUERYCAP、S_FMT、REQBUFS、QBUF/DQBUF、STREAMON/OFF

掌握V4L2,就能开发各类视频设备!

更多推荐