V4L2 框架核心概念

Video for Linux 2 (V4L2) 是 Linux 内核中处理视频设备的标准框架,用于统一管理摄像头、视频采集卡等设备。设计 V4L2 框架的核心目的(思想):

  • 用户空间和内核空间分离:应用通过标准系统调用与内核交互
  • 设备抽象:所有视频设备通过 /dev/videoX 节点暴露
  • 缓冲区队列管理:通过 vb2_queue 实现高效的零拷贝数据传输
struct v4l2_device {
      struct device *dev;
      struct media_device *mdev;
      struct list_head subdevs;
      spinlock_t lock;
      char name[36];
      void (*notify)(struct v4l2_subdev *sd, unsigned int notification, void *arg);
      struct v4l2_ctrl_handler *ctrl_handler;
      struct v4l2_prio_state prio;
      struct kref ref;
      void (*release)(struct v4l2_device *v4l2_dev);
};

获取当前的格式

struct v4l2_format current_format;
memset(&current_format, 0, sizeof(current_format));
current_format.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ioctl(fd, VIDIOC_G_FMT, &current_format)

流程步骤深度解析

标准采集流程顺序: 打开设备 → 查询能力 → 设置格式 → 申请缓冲区 (REQBUFS) → 查询并映射缓冲区 (QUERYBUF + mmap) → 缓冲区入队 (QBUF)启动视频流 (STREAMON) → 取帧循环 (DQBUF → 处理 → QBUF) → 停止与释放。

关键点:必须先把缓冲区 QBUF 入队,再 STREAMON 开流。因为开流后硬件立即开始往队列里的缓冲区填数据,若队列为空则硬件无处可写。

打开设备文件 /dev/videoX

  • 系统调用:

    int fd = open("/dev/video0", O_RDWR);
    
  • 关键机制:

    • 内核通过 VFS(Virtual File System) 将设备文件映射到 v4l2_fops 操作集
    • 设备号(主设备号 81,次设备号 X)由内核在设备注册时分配(主设备号 81 专用于 V4L2)
  • 错误处理:

    • ENODEV:设备不存在
    • EBUSY:设备被其它进程占用

查询设备能力集

struct v4l2_capability cap;
ioctl(fd, VIDIOC_QUERYCAP, &cap)
struct v4l2_capability {
	__u8	driver[16];
	__u8	card[32];
	__u8	bus_info[32];
	__u32   version;
	__u32	capabilities;
	__u32	device_caps;
	__u32	reserved[3];
};
driver驱动程序名称。
cap.driver内核驱动模块名(如 uvcvideorkispmsm-camera 等)
cap.card设备名称(例如:“USB Camera”、“isp-video-output”)
cap.bus_info总线信息(设备挂载在哪条总线上)
cap.versionv4l2驱动版本
cap.capabilities设备能力标志(bitmask)
cap.device_caps具体 node 的能力(更细粒度)
reserved保留字段

常见能力位

  • V4L2_CAP_VIDEO_CAPTURE
    • 支持视频采集(摄像头)
  • V4L2_CAP_VIDEO_OUTPUT
    • 支持视频输出(显示设备)
  • V4L2_CAP_STREAMING
    • 支持 mmap / userptr / dmabuf
  • V4L2_CAP_READWRITE
    • 支持 read() / write() 方式
  • V4L2_CAP_META_CAPTURE
    • 支持元数据采集(ISP metadata)
if (cap.capabilities & V4L2_CAP_STREAMING) {
    // 支持零拷贝流
}

设置视频格式参数

struct v4l2_format fmt = {
    .type = V4L2_BUF_TYPE_VIDEO_CAPTURE,
    .fmt.pix = {
        .width       = 1920,
        .height      = 1080,
        .pixelformat = V4L2_PIX_FMT_YUYV,
        .field       = V4L2_FIELD_NONE
    }
};
ioctl(fd, VIDIOC_S_FMT, &fmt);

参数协商机制

  • 应用提出请求格式 → 驱动调整为最接近的硬件支持格式
  • 需要用 VIDIOC_G_FMT 验证最终生效的参数(或回读 S_FMT 调用后的 fmt

常见的格式

  • V4L2_PIX_FMT_YUYV:YUV 4:2:2 打包格式(裸数据,体积大)
  • V4L2_PIX_FMT_MJPEG:压缩格式(节省带宽)
  • V4L2_PIX_FMT_NV12:Android 常用的 YUV 4:2:0 格式

分配缓冲区(VIDIOC_REQBUFS

struct v4l2_requestbuffers req = {
    .count  = 4,                          // 请求 4 个缓冲区
    .type   = V4L2_BUF_TYPE_VIDEO_CAPTURE,
    .memory = V4L2_MEMORY_MMAP            // 内存类型
};
ioctl(fd, VIDIOC_REQBUFS, &req);
类型说明使用场景
V4L2_MEMORY_MMAP内核分配 + mmap 映射高性能场景(主流选择)
V4L2_MEMORY_USERPTR用户空间分配地址特殊内存管理需求
V4L2_MEMORY_DMABUFDMA 共享缓冲区多进程 / 跨设备共享数据型

流程

  1. 应用请求缓冲区数量
  2. 内核在 vb2_queue 中预分配资源
  3. 驱动返回实际分配到的缓冲区数量(req.count 可能被调整,应回读确认)

查询并映射缓冲区(VIDIOC_QUERYBUF + mmap

REQBUFS 只是预分配req.count 块缓冲区,应用还需逐个查询每块缓冲区的长度和偏移量,再用 mmap 映射到用户空间才能访问数据。

for (int i = 0; i < req.count; i++) {
    struct v4l2_buffer buf = {
        .type   = V4L2_BUF_TYPE_VIDEO_CAPTURE,
        .memory = V4L2_MEMORY_MMAP,
        .index  = i
    };
    ioctl(fd, VIDIOC_QUERYBUF, &buf);   // 拿到 buf.length 和 buf.m.offset

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

mmap 最后一个参数 buf.m.offset 必须来自 VIDIOC_QUERYBUF 的返回,不能凭空构造。

缓冲区入队(VIDIOC_QBUF

在开流之前,先把所有缓冲区交给内核,让硬件有地方写数据。

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

入队(QBUF)是把缓冲区加入队列的动作,由应用完成——这一点不要和 STREAMON 混淆。

启动视频流(VIDIOC_STREAMON

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

内核行为

  • 激活设备硬件(如启动摄像头传感器)
  • 初始化 DMA 传输通道
  • 开始让已入队的缓冲区被硬件填充(缓冲区是上一步 QBUF 放进队列的,不是 STREAMON 放进去的)

缓冲区循环处理(VIDIOC_DQBUF → 处理 → VIDIOC_QBUF

开流后进入核心循环:出队拿到一帧满缓冲区 → 处理 → 再入队循环利用。

struct v4l2_buffer buf = {
    .type   = V4L2_BUF_TYPE_VIDEO_CAPTURE,
    .memory = V4L2_MEMORY_MMAP
};
ioctl(fd, VIDIOC_DQBUF, &buf);            // 出队:拿到一帧

// buffers[buf.index].start 即该帧数据,buf.bytesused 为有效长度
process_frame(buffers[buf.index].start, buf.bytesused);

ioctl(fd, VIDIOC_QBUF, &buf);             // 处理完再入队
sequenceDiagram
    participant App as 应用(用户空间)
    participant Kernel as 内核空间(V4L2 驱动 / vb2_queue)
    participant HW as 硬件(传感器 / DMA)

    App->>Kernel: VIDIOC_QBUF(入队空缓冲区)
    Kernel->>HW: 触发 DMA 传输
    HW-->>Kernel: 填充数据完成
    App->>Kernel: VIDIOC_DQBUF(请求满缓冲区)
    Kernel-->>App: 返回满缓冲区
    Note over App: 处理视频数据
    App->>Kernel: 再次 VIDIOC_QBUF(回收缓冲区)

出队(DQBUF)要点

阻塞行为:默认会等待直到有数据就绪(设备以 O_NONBLOCK 打开时变为非阻塞,配合 select/poll 使用)

  • bytesused:实际填充的数据长度
  • sequence:帧序号(用于检测丢帧)
  • flags & V4L2_BUF_FLAG_ERROR:指示传输错误

循环设计优势

  • 零拷贝:数据直接从 DMA 缓冲区映射到用户空间,无需额外拷贝
  • 双缓冲机制:至少需要 2 个缓冲区保证连续采集(一个被硬件写,一个被应用读)
  • 动态调整:应用可动态调整处理速度(如跳过某些帧)

停止与释放

enum v4l2_buf_type 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);                                // 关闭设备

核心结构体

核心组件作用

组件功能
video_device设备节点注册(创建 /dev/videoX
v4l2_device设备逻辑管理(电源 / 子设备协调)
vb2_queue缓冲区队列管理(核心创新点)
v4l2_subdev子设备抽象(传感器 / ISP 等)

struct v4l2_buffer

用来描述"一块视频缓冲区"的所有元信息。它的常用字段大致有:

struct v4l2_buffer {
	__u32			index;//这块缓冲区在缓冲区队列里的编号(0、1、2...),对应 REQBUFS 申请到的第几块
	__u32			type;//缓冲区类型,V4L2_BUF_TYPE_VIDEO_CAPTURE(采集)
	__u32			bytesused;//实际有效数据长度(采集完成后填充)
	__u32			flags;//状态标志位,比如 V4L2_BUF_FLAG_DONE(已完成采集)等
	__u32			field;
	struct timeval		timestamp;//采集这一帧的时间戳
	struct v4l2_timecode	timecode;
	__u32			sequence;

	/* memory location */
    /*内存管理方式:V4L2_MEMORY_MMAP(内核分配,mmap映射)、V4L2_MEMORY_USERPTR(用户自己分配)、V4L2_MEMORY_DMABUF 等*/
	__u32			memory;
	union {
		__u32           offset;//当 memory = MMAP 时有效,表示这块缓冲区在设备内存映射空间里的偏移量,供 mmap() 使用
		unsigned long   userptr;//当 memory = USERPTR 时有效,指向用户自己分配的内存地址
		struct v4l2_plane *planes;
		__s32		fd;
	} m;
	__u32			length;//缓冲区长度(字节数)
	__u32			reserved2;
	union {
		__s32		request_fd;
		__u32		reserved;
	};
};