ESC
输入关键词搜索文章标题和内容

V4L2与零拷贝:从视频采集到DMA-BUF跨设备共享的全链路实战

本文由 linuxROS 整理发布,首发于 linuxros.cn,转载请注明出处。

V4L2与零拷贝:从视频采集到DMA-BUF跨设备共享的全链路实。

摄像头采集一帧图像,传统做法要经过两次CPU拷贝才能显示到屏幕上——V4L2采集到内核缓冲区,CPU搬到用户空间,再CPU搬到GPU/DRM。080p@60fps下,每秒光拷贝就吃掉几百MB/s带宽。DMA-BUF就是来解决这个问题的:让V4L2、GPU、DRM共享同一块物理内存,零拷贝走完全程,基于Linux 6.8内核,从V4L2框架讲起,过videobuf2缓冲区管理,最后把DMA-BUF零拷贝机制和实战代码串起来。

一、V4L2框架概述

V4L2(Video for Linux 2)是Linux内核的视频设备标准接口,摄像头、TV采集卡、视频输出设备、SDR全归它管。用户空间通过/dev/videoX设备节点和标准ioctl跟驱动交互,不用管底层硬件差异。

架构分层

flowchart TB A["用户空间<br/>V4L2 ioctl"] -->|"open / ioctl / mmap"| B["V4L2核心br/>v4l2-dev / v4l2-ioctl"] B -->|"回调驱动函数"| C["驱动<br/>v4l2_file_operations"] C -->|"操作寄存。DMA"| D["硬件<br/>摄像。/ ISP"] A --- A1["。VIDIOC_QUERYCAP<br/>。VIDIOC_S_FMT<br/>。VIDIOC_REQBUFS<br/>。VIDIOC_STREAMON"] B --- B1["。ioctl分发与参数校br/>。设备注册/注销<br/>。子设备管理"] C --- C1["。初始化硬br/>。配置ISP管线<br/>。管理DMA传输"] D --- D1["。Sensor输出YUV/RAW<br/>。ISP缩放/去马赛克<br/>。DMA写入缓冲"] style A fill:#E3F2FD style B fill:#FFF8E1 style C fill:#E8F5E9 style D fill:#F3E5F5 style A1 fill:#E3F2FD style B1 fill:#FFF8E1 style C1 fill:#E8F5E9 style D1 fill:#F3E5F5

支持的设备类。

类型 V4L2设备类型 设备节点 典型硬件
视频捕获 V4L2_CAP_VIDEO_CAPTURE /dev/videoX USB摄像头、MIPI CSI Sensor
视频输出 V4L2_CAP_VIDEO_OUTPUT /dev/videoX HDMI输出、LCD显示
VBI数据 V4L2_CAP_VBI_CAPTURE /dev/vbiX TV场消隐期数据
SDR V4L2_CAP_SDR_CAPTURE /dev/swradioX 软件定义无线电
元数据 V4L2_CAP_META_CAPTURE /dev/videoX Sensor内嵌元数据

日常开发打交道最多的就是视频捕获,后面内容也以捕获为主。

二、V4L2核心数据结构

V4L2驱动的骨架就四个结构体:v4l2_device管设备实例,video_device管设备节点,v4l2_subdev管子设备,v4l2_ioctl_ops管ioctl回调试

v4l2_device:设备实。

// include/media/v4l2-device.h
struct v4l2_device {
    struct device *dev;          // 指向底层device
    struct list_head subdevs;    // 子设备链。    struct mutex lock;           // 全局互斥。    char name[V4L2_DEVICE_NAME_SIZE]; // 设备。    struct v4l2_prio_state prio; // 优先级状态    struct kref ref;             // 引用计数
    void (*release)(struct v4l2_device *v4l2_dev); // 释放回调
};

v4l2_device是整个V4L2驱动的根,通常嵌入在驱动的私有结构体里,注册用v4l2_device_register,注销用v4l2_device_unregister。

v4l2_subdev:子设备

// include/media/v4l2-subdev.h
struct v4l2_subdev {
    struct v4l2_device *v4l2_dev;   // 指向父设。    const struct v4l2_subdev_ops *ops; // 子设备操作集
    struct list_head list;          // 挂在v4l2_device.subdevs链表。    u32 flags;
    struct v4l2_subdev_platform_data *pdata;
};

子设备代表Sensor、ISP、Tuner等独立功能模块。Sensor驱动注册为subdev,ISP驱动也注册为subdev,V4L2核心层通过v4l2_device统一管理它们之间的连接关系。

video_device:注册到/dev/videoX

// include/media/v4l2-dev.h(关键字段)
struct video_device {
    const struct v4l2_file_operations *fops;  // 文件操作
    const struct v4l2_ioctl_ops *ioctl_ops;   // ioctl操作
    struct v4l2_device *v4l2_dev;             // 指向父设。    struct device dev;                        // 内嵌device
    enum vfl_devnode_type vfl_type;           // 设备类型
    enum vfl_devnode_direction vfl_dir;       // 输入/输出方向
    int minor;                                // 次设备号
    struct vb2_queue *queue;                  // 绑定的vb2队列
    u32 device_caps;                          // 设备能力位图
};

video_device是用户空间能看到的实体——注册后会在/dev/下创建videoX节点。.8内核里video_device内嵌了device,完整融入设备模型。

v4l2_file_operations与v4l2_ioctl_ops

// 文件操作,大部分直接用V4L2提供的默认实。struct v4l2_file_operations {
    struct module *owner;
    int (*open)(struct video_device *vdev);
    int (*release)(struct video_device *vdev);
    ssize_t (*read)(struct video_device *vdev, char __user *buf,
                    size_t count, loff_t *ppos);
    ssize_t (*write)(struct video_device *vdev, const char __user *buf,
                     size_t count, loff_t *ppos);
    __poll_t (*poll)(struct file *file, struct poll_table_struct *wait);
    long (*ioctl)(struct video_device *vdev, unsigned int cmd, void *arg);
    int (*mmap)(struct video_device *vdev, struct vm_area_struct *vma);
};

// ioctl操作,驱动必须实现的核心回调
struct v4l2_ioctl_ops {
    int (*vidioc_querycap)(struct file *file, void *fh,
                           struct v4l2_capability *cap);
    int (*vidioc_enum_fmt_vid_cap)(struct file *file, void *fh,
                                   struct v4l2_fmtdesc *f);
    int (*vidioc_g_fmt_vid_cap)(struct file *file, void *fh,
                                struct v4l2_format *fmt);
    int (*vidioc_s_fmt_vid_cap)(struct file *file, void *fh,
                                struct v4l2_format *fmt);
    int (*vidioc_reqbufs)(struct file *file, void *fh,
                          struct v4l2_requestbuffers *b);
    int (*vidioc_qbuf)(struct file *file, void *fh,
                       struct v4l2_buffer *b);
    int (*vidioc_dqbuf)(struct file *file, void *fh,
                        struct v4l2_buffer *b);
    int (*vidioc_streamon)(struct file *file, void *fh,
                           enum v4l2_buf_type type);
    int (*vidioc_streamoff)(struct file *file, void *fh,
                            enum v4l2_buf_type type);
    // ... 更多回调
};

v4l2_file_operations大部分可以直接用V4L2核心提供的v4l2_fops默认实现,驱动只需要关注open和release。真正需要驱动实现的是v4l2_ioctl_ops里的回调试

三、V4L2 ioctl操作流程

V4L2采集有一套固定的ioctl调用顺序,跳步或乱序都会报错。完整流程如下:

flowchart TB A(["开销"]) --> B["open /dev/videoX"] B --> C["VIDIOC_QUERYCAP<br/>查询设备能力"] C --> D["VIDIOC_ENUM_FMT<br/>枚举支持格式"] D --> E["VIDIOC_S_FMT<br/>设置图像格式"] E --> F["VIDIOC_REQBUFS<br/>申请缓冲"] F --> G["mmap<br/>映射缓冲区到用户空间"] G --> H["VIDIOC_QBUF<br/>缓冲区入"] H --> I["VIDIOC_STREAMON<br/>启动采集"] I --> J["VIDIOC_DQBUF<br/>缓冲区出"] J --> K["处理图像数据"] K --> L{"继续采集?"} L -->|"是"| H2["VIDIOC_QBUF<br/>缓冲区重新入"] H2 --> J L -->|"否"| M["VIDIOC_STREAMOFF<br/>停止采集"] M --> N["close<br/>关闭设备"] N --> O(["结束"]) style A fill:#E8F5E9 style B fill:#E3F2FD style C fill:#E3F2FD style D fill:#E3F2FD style E fill:#E3F2FD style F fill:#E3F2FD style G fill:#E3F2FD style H fill:#FFF8E1 style I fill:#FFF8E1 style J fill:#FFF8E1 style K fill:#F3E5F5 style L fill:#FFF8E1 style H2 fill:#FFF8E1 style M fill:#FFEBEE style N fill:#FFEBEE style O fill:#E8F5E9

各步骤对应的ioctl和驱动回调

步骤 用户空间ioctl 驱动回调 说明
查询能力 VIDIOC_QUERYCAP vidioc_querycap 返回设备能力位图,必须第一步调用
枚举格式 VIDIOC_ENUM_FMT vidioc_enum_fmt_vid_cap 遍历硬件支持的像素格式
设置格式 VIDIOC_S_FMT vidioc_s_fmt_vid_cap 设定分辨率、像素格式、场
申请缓冲区 VIDIOC_REQBUFS vidioc_reqbufs 指定缓冲区数量和内存类型
映射缓冲区 mmap vb2_mmap 把内核缓冲区映射到用户空间
缓冲区入队 VIDIOC_QBUF vidioc_qbuf 把空闲缓冲区交给驱动填充
启动采集 VIDIOC_STREAMON vidioc_streamon 通知驱动开始DMA传输
缓冲区出队 VIDIOC_DQBUF vidioc_dqbuf 取回已填充数据的缓冲区
停止采集 VIDIOC_STREAMOFF vidioc_streamoff 停止DMA,所有缓冲区回到用户

关键点:REQBUFS必须在QBUF之前,STREAMON必须在所有初始缓冲区入队之后。顺序搞反,ioctl直接返回EINVAL。

四、videobuf2缓冲区管理

videobuf2(简称vb2)是V4L2的缓冲区管理框架,缓冲区分配、映射、入队出队、DMA对接全由它管。驱动不直接操作缓冲区内存,全部走vb2接口。

vb2_queue:缓冲区队列

// include/media/videobuf2-core.h(关键字段)
struct vb2_queue {
    enum v4l2_buf_type type;          // 缓冲区类。V4L2_BUF_TYPE_VIDEO_CAPTURE)
    enum vb2_memory memory;           // 内存类型(VB2_MEMORY_MMAP/DMABUF/USERPTR)
    struct mutex *lock;               // 队列。    const struct vb2_ops *ops;        // 驱动回调
    const struct vb2_mem_ops *mem_ops;// 内存操作
    u32 min_buffers_needed;           // 最少需要的缓冲区数
    u32 buf_struct_size;              // 驱动私有缓冲区结构大。    struct vb2_buffer *bufs[VB2_MAX_FRAME]; // 缓冲区数。    unsigned int num_buffers;         // 已分配缓冲区。};

vb2_ops:驱动回调

// 驱动需要实现的vb2操作
struct vb2_ops {
    int (*queue_setup)(struct vb2_queue *q,
                       unsigned int *num_buffers,
                       unsigned int *num_planes,
                       unsigned int sizes[],
                       struct device *alloc_devs[]);
    void (*buf_queue)(struct vb2_buffer *vb);     // 缓冲区入队时调用
    int (*buf_prepare)(struct vb2_buffer *vb);    // 缓冲区准备(可选)
    void (*buf_finish)(struct vb2_buffer *vb);    // 缓冲区完成(可选)
    int (*start_streaming)(struct vb2_queue *q, unsigned int count);
    void (*stop_streaming)(struct vb2_queue *q);
    void (*buf_cleanup)(struct vb2_buffer *vb);   // 缓冲区清理(可选)
};

queue_setup在REQBUFS时被调用,驱动可以调整缓冲区数量和大小。buf_queue在每次QBUF时被调用,驱动在这里把缓冲区交给硬件DMA引擎。start_streaming/stop_streaming对应STREAMON/STREAMOFF。

vb2_mem_ops:内存操。

// 三种内存操作集,驱动选择一。struct vb2_mem_ops {
    void *(*alloc)(struct vb2_buffer *vb, struct device *dev,
                   unsigned long size);
    void (*put)(void *buf_priv);
    void *(*get_userptr)(struct vb2_buffer *vb, struct device *dev,
                         unsigned long vaddr, unsigned long size);
    void (*put_userptr)(void *buf_priv);
    struct dma_buf *(*get_dmabuf)(void *buf_priv, unsigned long flags);
    int (*mmap)(void *buf_priv, struct vm_area_struct *vma);
    // ...
};

三种内存模式

模式 mem_ops 分配方式 适用场景
VMALLOC vb2_vmalloc_memops vmalloc分配虚拟连续内存 调试、USB摄像头
DMA-contiguous vb2_dma_contig_memops dma_alloc_coherent分配物理连续内存 嵌入式平台ISP/DMA引擎
DMA-scatter/gather vb2_dma_sg_memops dma_map_sg映射分散页面 高端摄像头、大分辨率

嵌入式平台最常用DMA-contiguous模式,因为很多SoC的DMA引擎要求物理地址连续。dma_alloc_coherent分配的内存物理连续且cache一致,硬件DMA直接拿返回的DMA地址搬数据就行。
DMA-contiguous模式的关键代码:

// 初始化vb2_queue时指定DMA-contiguous
q->mem_ops = vb2_dma_contig_memops;

// vb2内部通过dma_alloc_coherent分配
// drivers/media/common/videobuf2/videobuf2-dma-contig.c
static void *vb2_dc_alloc(struct vb2_buffer *vb, struct device *dev,
                          unsigned long size)
{
    struct vb2_dc_buf *buf;
    dma_addr_t dma_addr;

    buf = kzalloc(sizeof(*buf), GFP_KERNEL);
    // 分配物理连续、cache一致的DMA缓冲。    buf->vaddr = dma_alloc_coherent(dev, size, &dma_addr,
                                    GFP_KERNEL | vb->dma_attrs);
    buf->dma_addr = dma_addr;
    buf->size = size;
    buf->dev = dev;
    return buf;
}

五、简单V4L2驱动示例

下面写一个虚拟摄像头驱动,基于vb2框架,生成彩色测试图案。代码结构参考内核的vivid驱动,但大幅简化,只保留核心逻辑。

// v4l2_dummy_cam.c - 虚拟摄像头驱动(Linux 6.8。#include <linux/module.h>
#include <linux/platform_device.h>
#include <media/v4l2-device.h>
#include <media/v4l2-ioctl.h>
#include <media/v4l2-fh.h>
#include <media/videobuf2-v4l2.h>
#include <media/videobuf2-vmalloc.h>

#define DUMMY_CAM_WIDTH  640
#define DUMMY_CAM_HEIGHT 480
#define DUMMY_CAM_PIXFMT V4L2_PIX_FMT_YUYV
#define DUMMY_CAM_NBUFS  4

struct dummy_cam_dev {
    struct v4l2_device  v4l2_dev;
    struct video_device  vdev;
    struct vb2_queue     queue;
    struct mutex         lock;
    /* 当前帧计数,用于生成测试图案 */
    u32 frame_count;
};

/* ---- vb2_ops ---- */

static int dummy_cam_queue_setup(struct vb2_queue *q,
                                  unsigned int *nbuffers,
                                  unsigned int *nplanes,
                                  unsigned int sizes[],
                                  struct device *alloc_devs[])
{
    /* YUYV格式,每像素2字节 */
    sizes[0] = DUMMY_CAM_WIDTH * DUMMY_CAM_HEIGHT * 2;
    *nplanes = 1;
    return 0;
}

static int dummy_cam_buf_prepare(struct vb2_buffer *vb)
{
    struct vb2_v4l2_buffer *vbuf = to_vb2_v4l2_buffer(vb);
    unsigned long size = DUMMY_CAM_WIDTH * DUMMY_CAM_HEIGHT * 2;

    if (vb2_plane_size(vb, 0) < size)
        return -EINVAL;

    vb2_set_plane_payload(vb, 0, size);
    vbuf->field = V4L2_FIELD_NONE;
    return 0;
}

static void dummy_cam_buf_queue(struct vb2_buffer *vb)
{
    struct dummy_cam_dev *dev = vb2_get_drv_priv(vb->vb2_queue);
    struct vb2_v4l2_buffer *vbuf = to_vb2_v4l2_buffer(vb);

    /* 模拟硬件填充:生成YUYV测试图案 */
    void *vaddr = vb2_plane_vaddr(vb, 0);
    if (vaddr) {
        u8 *p = vaddr;
        u32 frame = dev->frame_count++;
        /* 简单渐变色图案,Y分量随帧号变更*/
        memset(p, (frame * 3) & 0xFF,
               DUMMY_CAM_WIDTH * DUMMY_CAM_HEIGHT * 2);
    }

    /* 立即完成,模拟硬件中断回调*/
    vbuf->sequence = dev->frame_count;
    vbuf->vb2_buf.timestamp = ktime_get_ns();
    vb2_buffer_done(&vbuf->vb2_buf, VB2_BUF_STATE_DONE);
}

static int dummy_cam_start_streaming(struct vb2_queue *q, unsigned int count)
{
    struct dummy_cam_dev *dev = vb2_get_drv_priv(q);
    dev->frame_count = 0;
    return 0;
}

static void dummy_cam_stop_streaming(struct vb2_queue *q)
{
    struct dummy_cam_dev *dev = vb2_get_drv_priv(q);
    /* 清除所有未完成的缓冲区 */
    struct vb2_buffer *vb;
    while ((vb = vb2_done_list_del(q)))
        vb2_buffer_done(vb, VB2_BUF_STATE_ERROR);
}

static const struct vb2_ops dummy_cam_vb2_ops = {
    .queue_setup     = dummy_cam_queue_setup,
    .buf_prepare     = dummy_cam_buf_prepare,
    .buf_queue       = dummy_cam_buf_queue,
    .start_streaming = dummy_cam_start_streaming,
    .stop_streaming  = dummy_cam_stop_streaming,
};

/* ---- v4l2_ioctl_ops ---- */

static int dummy_cam_querycap(struct file *file, void *fh,
                               struct v4l2_capability *cap)
{
    strscpy(cap->driver, "dummy_cam", sizeof(cap->driver));
    strscpy(cap->card, "Dummy Camera", sizeof(cap->card));
    strscpy(cap->bus_info, "platform:dummy_cam", sizeof(cap->bus_info));
    cap->device_caps = V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_STREAMING;
    cap->capabilities = cap->device_caps | V4L2_CAP_DEVICE_CAPS;
    return 0;
}

static int dummy_cam_enum_fmt(struct file *file, void *fh,
                               struct v4l2_fmtdesc *f)
{
    if (f->index > 0)
        return -EINVAL;
    f->pixelformat = DUMMY_CAM_PIXFMT;
    return 0;
}

static int dummy_cam_g_fmt(struct file *file, void *fh,
                            struct v4l2_format *fmt)
{
    fmt->fmt.pix.width       = DUMMY_CAM_WIDTH;
    fmt->fmt.pix.height      = DUMMY_CAM_HEIGHT;
    fmt->fmt.pix.pixelformat = DUMMY_CAM_PIXFMT;
    fmt->fmt.pix.field       = V4L2_FIELD_NONE;
    fmt->fmt.pix.bytesperline = DUMMY_CAM_WIDTH * 2;
    fmt->fmt.pix.sizeimage   = DUMMY_CAM_WIDTH * DUMMY_CAM_HEIGHT * 2;
    fmt->fmt.pix.colorspace  = V4L2_COLORSPACE_SRGB;
    return 0;
}

static int dummy_cam_s_fmt(struct file *file, void *fh,
                            struct v4l2_format *fmt)
{
    /* 虚拟设备只支持一种格式,直接返回固定*/
    return dummy_cam_g_fmt(file, fh, fmt);
}

static const struct v4l2_ioctl_ops dummy_cam_ioctl_ops = {
    .vidioc_querycap       = dummy_cam_querycap,
    .vidioc_enum_fmt_vid_cap = dummy_cam_enum_fmt,
    .vidioc_g_fmt_vid_cap  = dummy_cam_g_fmt,
    .vidioc_s_fmt_vid_cap  = dummy_cam_s_fmt,
    .vidioc_reqbufs        = vb2_ioctl_reqbufs,
    .vidioc_create_bufs    = vb2_ioctl_create_bufs,
    .vidioc_qbuf           = vb2_ioctl_qbuf,
    .vidioc_dqbuf          = vb2_ioctl_dqbuf,
    .vidioc_streamon       = vb2_ioctl_streamon,
    .vidioc_streamoff      = vb2_ioctl_streamoff,
    .vidioc_expbuf         = vb2_ioctl_expbuf,
    .vidioc_mmap           = vb2_ioctl_mmap,
};

/* ---- v4l2_file_operations ---- */

static int dummy_cam_open(struct file *file)
{
    struct dummy_cam_dev *dev = video_drvdata(file);
    return v4l2_fh_open(file);
}

static int dummy_cam_release(struct file *file)
{
    return vb2_fop_release(file);
}

static const struct v4l2_file_operations dummy_cam_fops = {
    .owner   = THIS_MODULE,
    .open    = dummy_cam_open,
    .release = dummy_cam_release,
    .read    = vb2_fop_read,
    .poll    = vb2_fop_poll,
    .mmap    = vb2_fop_mmap,
    .unlocked_ioctl = video_ioctl2,
};

/* ---- probe / remove ---- */

static int dummy_cam_probe(struct platform_device *pdev)
{
    struct dummy_cam_dev *dev;
    int ret;

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

    mutex_init(&dev->lock);

    /* 注册v4l2_device */
    ret = v4l2_device_register(&pdev->dev, &dev->v4l2_dev);
    if (ret)
        return ret;

    /* 初始化vb2_queue */
    dev->queue.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    dev->queue.io_modes = VB2_MMAP | VB2_DMABUF | VB2_READ;
    dev->queue.dev = &pdev->dev;
    dev->queue.ops = &dummy_cam_vb2_ops;
    dev->queue.mem_ops = &vb2_vmalloc_memops;
    dev->queue.buf_struct_size = sizeof(struct vb2_v4l2_buffer);
    dev->queue.lock = &dev->lock;
    dev->queue.min_buffers_needed = 1;
    ret = vb2_queue_init(&dev->queue);
    if (ret)
        goto err_v4l2_unregister;

    /* 初始化video_device */
    dev->vdev = video_device_alloc();
    if (!dev->vdev) {
        ret = -ENOMEM;
        goto err_v4l2_unregister;
    }
    dev->vdev->v4l2_dev = &dev->v4l2_dev;
    dev->vdev->fops = &dummy_cam_fops;
    dev->vdev->ioctl_ops = &dummy_cam_ioctl_ops;
    dev->vdev->queue = &dev->queue;
    dev->vdev->lock = &dev->lock;
    dev->vdev->device_caps = V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_STREAMING;
    video_set_drvdata(dev->vdev, dev);

    ret = video_register_device(dev->vdev, VFL_TYPE_VIDEO, -1);
    if (ret)
        goto err_vdev_release;

    platform_set_drvdata(pdev, dev);
    dev_info(&pdev->dev, "dummy camera registered as /dev/video%d\n",
             dev->vdev->num);
    return 0;

err_vdev_release:
    video_device_release(dev->vdev);
err_v4l2_unregister:
    v4l2_device_unregister(&dev->v4l2_dev);
    return ret;
}

static void dummy_cam_remove(struct platform_device *pdev)
{
    struct dummy_cam_dev *dev = platform_get_drvdata(pdev);

    video_unregister_device(dev->vdev);
    v4l2_device_unregister(&dev->v4l2_dev);
    mutex_destroy(&dev->lock);
}

static struct platform_driver dummy_cam_driver = {
    .probe  = dummy_cam_probe,
    .remove = dummy_cam_remove,
    .driver = {
        .name = "dummy_cam",
    },
};

module_platform_driver(dummy_cam_driver);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Dummy V4L2 camera driver for testing");

几点说明。
- buf_queue里直接调用vb2_buffer_done完成缓冲区,真实驱动是在中断处理函数里调试- vidioc_reqbufs/qbuf/dqbuf/streamon/streamoff/mmap这些直接用vb2提供的vb2_ioctl_*默认实现,不需要驱动重启- 内存操作用了vb2_vmalloc_memops,真实嵌入式平台换成vb2_dma_contig_memops

来自 linuxros.cn · linuxROS

六、用户空间V4L2应用

V4L2采集程序

// v4l2_capture.c - 用户空间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 <errno.h>
#include <linux/videodev2.h>

#define NBUFS 4

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

int main(int argc, char **argv)
{
    const char *dev_name = "/dev/video0";
    int fd = -1;
    struct buffer buffers[NBUFS];
    int i;

    /* 1. 打开设备 */
    fd = open(dev_name, O_RDWR | O_NONBLOCK);
    if (fd < 0) {
        perror("open");
        return -1;
    }

    /* 2. 查询设备能力 */
    struct v4l2_capability cap;
    if (ioctl(fd, VIDIOC_QUERYCAP, &cap) < 0) {
        perror("VIDIOC_QUERYCAP");
        goto out;
    }
    if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) {
        fprintf(stderr, "不是视频捕获设备\n");
        goto out;
    }

    /* 3. 设置格式 */
    struct v4l2_format fmt;
    memset(&fmt, 0, sizeof(fmt));
    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;
    fmt.fmt.pix.field       = V4L2_FIELD_NONE;
    if (ioctl(fd, VIDIOC_S_FMT, &fmt) < 0) {
        perror("VIDIOC_S_FMT");
        goto out;
    }
    printf("实际格式: %dx%d, pixfmt=0x%08x\n",
           fmt.fmt.pix.width, fmt.fmt.pix.height,
           fmt.fmt.pix.pixelformat);

    /* 4. 申请缓冲*/
    struct v4l2_requestbuffers req;
    memset(&req, 0, sizeof(req));
    req.count  = NBUFS;
    req.type   = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    req.memory = V4L2_MEMORY_MMAP;
    if (ioctl(fd, VIDIOC_REQBUFS, &req) < 0) {
        perror("VIDIOC_REQBUFS");
        goto out;
    }

    /* 5. mmap映射 + QBUF入队 */
    for (i = 0; i < NBUFS; i++) {
        struct v4l2_buffer buf;
        memset(&buf, 0, sizeof(buf));
        buf.type   = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        buf.memory = V4L2_MEMORY_MMAP;
        buf.index  = i;
        if (ioctl(fd, VIDIOC_QUERYBUF, &buf) < 0) {
            perror("VIDIOC_QUERYBUF");
            goto out;
        }
        buffers[i].length = buf.length;
        buffers[i].start = mmap(NULL, buf.length,
                                PROT_READ | PROT_WRITE,
                                MAP_SHARED, fd, buf.m.offset);
        if (buffers[i].start == MAP_FAILED) {
            perror("mmap");
            goto out;
        }
        /* 入队 */
        if (ioctl(fd, VIDIOC_QBUF, &buf) < 0) {
            perror("VIDIOC_QBUF");
            goto out;
        }
    }

    /* 6. 启动采集 */
    enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    if (ioctl(fd, VIDIOC_STREAMON, &type) < 0) {
        perror("VIDIOC_STREAMON");
        goto out;
    }

    /* 7. 采集循环 */
    int frame_count = 0;
    while (frame_count < 30) {
        struct v4l2_buffer buf;
        memset(&buf, 0, sizeof(buf));
        buf.type   = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        buf.memory = V4L2_MEMORY_MMAP;

        /* 出队(阻塞等待) */
        if (ioctl(fd, VIDIOC_DQBUF, &buf) < 0) {
            if (errno == EAGAIN)
                continue;
            perror("VIDIOC_DQBUF");
            break;
        }

        printf("。%d: index=%d, bytesused=%u, seq=%u\n",
               frame_count, buf.index, buf.bytesused, buf.sequence);

        /* 此处可处理buffers[buf.index].start中的图像数据 */

        /* 重新入队 */
        if (ioctl(fd, VIDIOC_QBUF, &buf) < 0) {
            perror("VIDIOC_QBUF");
            break;
        }
        frame_count++;
    }

    /* 8. 停止采集 */
    ioctl(fd, VIDIOC_STREAMOFF, &type);

out:
    /* 9. 清理 */
    for (i = 0; i < NBUFS; i++) {
        if (buffers[i].start && buffers[i].start != MAP_FAILED)
            munmap(buffers[i].start, buffers[i].length);
    }
    if (fd >= 0)
        close(fd);
    return 0;
}

v4l2-ctl命令行工。

调试V4L2设备最方便的工具是v4l2-ctl,来自v4l-utils包:

# 查看设备信息
v4l2-ctl --device=/dev/video0 --all

# 列出支持的格。v4l2-ctl --device=/dev/video0 --list-formats-ext

# 设置格式并采集一。v4l2-ctl --device=/dev/video0 \
    --set-fmt-video=width=640,height=480,pixelformat=YUYV \
    --set-parm=30 \
    --stream-mmap --stream-to=frame.yuyv --stream-count=1

# 连续采集10。v4l2-ctl --device=/dev/video0 \
    --stream-mmap --stream-to=video.yuyv --stream-count=10

七、DMA-BUF零拷贝机。

传统拷贝的瓶。

V4L2采集到显示的传统数据流:

V4L2采集 。[内核DMA缓冲区] 。CPU拷贝 。[用户空间缓冲区] 。CPU拷贝 。[GPU缓冲区] 。渲染显示

1080p@60fps的YUYV图像,每帧约2MB,每。20MB。两次CPU拷贝意味着240MB/s的内存带宽浪费,还占CPU。在4K或HDR场景下更夸张。

DMA-BUF的解。

DMA-BUF让多个设备共享同一块物理内存,通过文件描述符(fd)在进程和设备之间传递:

flowchart LR A["V4L2采集<br/>DMA写入"] -->|"DMA-BUF fd<br/>(同一块物理内存)"| B["GPU处理<br/>DMA读取"] B -->|"DMA-BUF fd<br/>(同一块物理内存)"| C["DRM显示<br/>DMA读取"] A --- A1["。vb2分配DMA缓冲br/>。导出为dma_buf<br/>。传递fd给消费"] B --- B1["。通过fd导入dma_buf<br/>。GPU直接DMA访问<br/>。零拷贝处"] C --- C1["。通过fd导入dma_buf<br/>。显示控制器DMA读取<br/>。零拷贝显"] style A fill:#E3F2FD style B fill:#FFF8E1 style C fill:#E8F5E9 style A1 fill:#E3F2FD style B1 fill:#FFF8E1 style C1 fill:#E8F5E9

核心思路:V4L2驱动分配的DMA缓冲区,通过dma_buf_export导出为dma_buf对象,拿到一个fd;GPU和DRM通过dma_buf_get导入这个fd,直接DMA访问同一块物理内存。CPU全程不碰数据。

dma_buf_export / dma_buf_get

// 导出端(V4L2驱动。// include/linux/dma-buf.h

struct dma_buf_export_info {
    const char *exp_name;     // 导出者名。    const struct dma_buf_ops *ops; // 操作回调
    size_t size;              // 缓冲区大。    int flags;                // O_RDWR等标志    void *priv;               // 私有数据
};

// 导出dma_buf,返回fd
struct dma_buf *dma_buf_export(const struct dma_buf_export_info *exp_info);

// 导入端(GPU/DRM。// 通过fd获取dma_buf
struct dma_buf *dma_buf_get(int fd);

// 使用完毕后释。void dma_buf_put(struct dma_buf *dmabuf);

V4L2侧不用驱动手动调dma_buf_export,vb2框架已经封装好了。用户空间通过VIDIOC_EXPBUFioctl导出缓冲区对应的fd。

// 用户空间:从V4L2缓冲区导出DMA-BUF fd
struct v4l2_exportbuffer expbuf;
memset(&expbuf, 0, sizeof(expbuf));
expbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
expbuf.index = 0;  // 缓冲区索。expbuf.flags = O_RDWR;
if (ioctl(fd, VIDIOC_EXPBUF, &expbuf) < 0) {
    perror("VIDIOC_EXPBUF");
    return -1;
}
// expbuf.fd 就是DMA-BUF文件描述。printf("DMA-BUF fd = %d\n", expbuf.fd);

传统拷贝 vs DMA-BUF零拷。

对比 传统mmap拷贝 DMA-BUF零拷贝
CPU参与 2次拷贝(内核→用户→GPU) 0次拷贝
内存带宽 3倍帧大小(读+写) 1倍帧大小(仅DMA写入)
延迟 微秒级拷贝开销 纯DMA传输延迟
内存占用 多份副本同时存在 一份物理内存共享
编程复杂度 低(标准mmap) 中(需协调fd传递)
适用场景 低帧率/调试 高帧率/嵌入式实战
前提条件 设备在同一IOMMU域或物理地址空间

八、V4L2 + DMA-BUF实战

V4L2输出DMA-BUF fd

驱动侧需要做两件事:vb2_queue支持VB2_DMABUF模式,ioctl_ops注册vidioc_expbuf。

/* 驱动初始化时允许DMABUF模式 */
dev->queue.io_modes = VB2_MMAP | VB2_DMABUF | VB2_READ;

/* ioctl_ops中注册expbuf */
static const struct v4l2_ioctl_ops dummy_cam_ioctl_ops = {
    /* ... 其他回调 ... */
    .vidioc_expbuf = vb2_ioctl_expbuf,  // vb2默认实现
};

用户空间导出DMA-BUF fd的完整流程:

// v4l2_dmabuf_export.c - 从V4L2导出DMA-BUF fd
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/videodev2.h>

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

    /* 申请缓冲区,使用DMABUF模式 */
    struct v4l2_requestbuffers req = {
        .count  = 4,
        .type   = V4L2_BUF_TYPE_VIDEO_CAPTURE,
        .memory = V4L2_MEMORY_MMAP,  // 内核侧仍用mmap分配
    };
    if (ioctl(fd, VIDIOC_REQBUFS, &req) < 0) {
        perror("REQBUFS");
        goto out;
    }

    /* 导出缓冲。的DMA-BUF fd */
    struct v4l2_exportbuffer expbuf = {
        .type  = V4L2_BUF_TYPE_VIDEO_CAPTURE,
        .index = 0,
        .flags = O_RDWR,
    };
    if (ioctl(fd, VIDIOC_EXPBUF, &expbuf) < 0) {
        perror("EXPBUF");
        goto out;
    }
    printf("DMA-BUF fd = %d\n", expbuf.fd);

    /* 将expbuf.fd传给GPU/DRM消费者使*/
    /* ... */

    close(expbuf.fd);
out:
    close(fd);
    return 0;
}

GPU/DRM导入DMA-BUF

GPU端(以EGL/DRM为例)导入DMA-BUF fd。

// GPU端导入DMA-BUF(EGL方式。#include <EGL/egl.h>
#include <EGL/eglext.h>

// 从DMA-BUF fd创建EGLImage
EGLImage create_eglimage_from_dmabuf(int dmabuf_fd, int width, int height)
{
    EGLint attribs[] = {
        EGL_WIDTH,           width,
        EGL_HEIGHT,          height,
        EGL_LINUX_DRM_FOURCC_EXT,  DRM_FORMAT_YUYV,
        EGL_DMA_BUF_PLANE0_FD_EXT,     dmabuf_fd,
        EGL_DMA_BUF_PLANE0_OFFSET_EXT, 0,
        EGL_DMA_BUF_PLANE0_PITCH_EXT,  width * 2,
        EGL_NONE,
    };

    return eglCreateImageKHR(eglGetCurrentDisplay(),
                             EGL_NO_CONTEXT,
                             EGL_LINUX_DMA_BUF_EXT,
                             (EGLClientBuffer)NULL,
                             attribs);
}

DRM显示端导入DMA-BUF fd。

// DRM端导入DMA-BUF(drm_prime_handle方式。#include <drm/drm.h>
#include <xf86drm.h>

int import_dmabuf_to_drm(int drm_fd, int dmabuf_fd)
{
    struct drm_prime_handle prime = {
        .fd = dmabuf_fd,
        .flags = 0,
    };

    /* 将DMA-BUF fd转为DRM GEM handle */
    if (drmIoctl(drm_fd, DRM_IOCTL_PRIME_FD_TO_HANDLE, &prime) < 0) {
        perror("PRIME_FD_TO_HANDLE");
        return -1;
    }

    printf("DRM GEM handle = %u\n", prime.handle);
    /* 后续用handle做fb创建和显*/
    return prime.handle;
}

内核侧跨设备共享的关键路径

// 消费者设备(如GPU驱动)通过dma_buf_get导入
struct dma_buf *dmabuf = dma_buf_get(fd);
if (IS_ERR(dmabuf))
    return PTR_ERR(dmabuf);

// 获取dma_buf_attachment,建立设备间的映射关。struct dma_buf_attachment *attach;
attach = dma_buf_attach(dmabuf, dev);  // dev是消费者设。if (IS_ERR(attach)) {
    dma_buf_put(dmabuf);
    return PTR_ERR(attach);
}

// 获取scatter-gather表,用于DMA传输
struct sg_table *sgt;
sgt = dma_buf_map_attachment(attach, DMA_BIDIRECTIONAL);
if (IS_ERR(sgt)) {
    dma_buf_detach(dmabuf, attach);
    dma_buf_put(dmabuf);
    return PTR_ERR(sgt);
}

// 此时sgt->sgl包含物理页面信息,消费者设备的DMA引擎
// 可以直接通过sg_table访问V4L2分配的同一块物理内存
// 使用完毕后释。dma_buf_unmap_attachment(attach, sgt, DMA_BIDIRECTIONAL);
dma_buf_detach(dmabuf, attach);
dma_buf_put(dmabuf);

dma_buf_attach建立设备间连接,dma_buf_map_attachment拿到scatter-gather表,消费者DMA引擎通过sg_table直接访问原始缓冲区。.8内核里,两个设备在同一个IOMMU域的话,dma_buf_map_attachment会自动建IOMMU映射,不用物理地址连续。

九、常见问题

Q1:VIDIOC_DQBUF一直超时,拿不到帧。

检查清单:

  1. 是否调用了STREAMON——忘了VIDIOC_STREAMON,DMA根本没启动,当然没有。2. 是否有缓冲区入队——STREAMON之前至少要QBUF一个缓冲区,否则硬件没地方写数。3. 中断是否正常——真实驱动里vb2_buffer_done在中断处理函数中调用,中断没注册或没触发就不会完成缓冲区
  2. *非阻塞模块——open时加了O_NONBLOCK,DQBUF没有就绪帧时立即返回EAGAIN,需要用poll/select等待
# 用v4l2-ctl验证设备是否能正常采。v4l2-ctl --device=/dev/video0 --stream-mmap --stream-count=5

Q2:mmap失败返回EINVAL。

常见原因。
1. 没有先REQBUFS——必须先VIDIOC_REQBUFS分配缓冲区,再QUERYBUF+mmap
2. 内存不足——嵌入式平台内存紧张。。080p缓冲区约32MB,可能分配失。3. 缓冲区类型不匹配——REQBUFS和QUERYBUF的type字段必须一。

Q3:DMA-BUF导入失败。

三个排查方向。
1. IOMMU域不一*——V4L2和GPU/DRM不在同一个IOMMU域,DMA地址无法互通。嵌入式SoC通常共享一个IOMMU,问题不大;PCIe设备可能各自有独立IOMMU
2.
EXPBUF未注——驱动v4l2_ioctl_ops里没注册vidioc_expbuf回调,或vb2_queue没声明VB2_DMABUF模式
3.
fd生命周期*——DMA-BUF fd在消费者使用期间不能关闭,否则底层dma_buf被释放,消费者访问会触发页错。

# 检查IOMMU分组
ls /sys/kernel/iommu_groups/
# 检查设备是否在同一。cat /sys/kernel/iommu_groups/*/devices

十、总结

V4L2 + DMA-BUF速查看

操作 API / ioctl 说明
注册V4L2设备 v4l2_device_register 初始化设备实例
注册子设备 v4l2_device_register_subdev Sensor/ISP
创建设备节点 video_register_device 生成/dev/videoX
初始化vb2队列 vb2_queue_init 绑定ops和mem_ops
查询能力 VIDIOC_QUERYCAP 第一步必须调用
设置格式 VIDIOC_S_FMT 分辨率+像素格式
申请缓冲区 VIDIOC_REQBUFS 指定数量和内存类型
映射缓冲区 mmap MMAP模式必须
缓冲区入队 VIDIOC_QBUF 交给驱动填充
启动采集 VIDIOC_STREAMON 开始DMA传输
缓冲区出队 VIDIOC_DQBUF 取回已填充帧
导出DMA-BUF VIDIOC_EXPBUF 获取零拷贝fd
导入DMA-BUF dma_buf_get 消费者通过fd导入
建立映射 dma_buf_map_attachment 获取sg_table
停止采集 VIDIOC_STREAMOFF 停止DMA

vb2内存模式选择

场景 推荐模式 原因
USB摄像头 VMALLOC USB传输已做一次拷贝,物理连续无意义
SoC内置ISP DMA-contiguous ISP DMA引擎要求物理连续
高分辨率Sensor DMA-sg 大缓冲区难以分配物理连续内存
零拷贝到GPU DMA-contiguous + EXPBUF DMA-BUF共享需要DMA地址

零拷贝适用场景判断

  • 帧率。0fps且分辨率。080p 。必须零拷贝,CPU拷贝扛不。- 多消费者(采集→AI推理→编码→显示)→ 零拷贝避免多份拷。- 低帧率调试场。。mmap够用,别给自己找麻烦

本文首发于linuxros.cn,转载请注明出处。

版权声明

作者linuxROS
协议本作品采用 CC BY-NC-SA 4.0 许可协议:署名-非商业性使用-相同方式共享
关注欢迎关注微信公众号 linuxROS,获取更多机器人 / 嵌入式 / Linux 干货
返回首页