3. V4L2 ioctl 接口:VIDIOC_QUERYCAP、VIDIOC_ENUM_FMT、VIDIOC_S_FMT 等关键 ioctl

V4L2 驱动层与应用层之间的通信核心是 ioctl 系统调用。应用程序通过 open() 打开视频设备节点(如 /dev/video0)后,使用 ioctl(fd, request, arg) 向驱动发送控制命令。本章聚焦帧采集前必须调用的三个关键 ioctl:查询设备能力、枚举支持的格式、设置采集格式。这些调用是 mmap 映射与 poll 机制的前提条件。

关键认知: 在 V4L2 框架中,ioctl 是同步操作。每个 ioctl 调用都会陷入内核,驱动完成对应操作后返回结果。应用层必须按“查询能力 → 枚举格式 → 设置格式 → 申请缓冲区 → 启动流”的顺序执行,不可跳跃。

3.1 VIDIOC_QUERYCAP:查询设备能力

VIDIOC_QUERYCAP 是应用层与 V4L2 设备交互的第一个 ioctl。它用于获取设备的基本信息,包括驱动名称、设备类型(capture / output / overlay 等)、设备能力标志(是否支持 streaming I/O、是否支持读写 I/O 等)。

调用时,应用层传入一个 struct v4l2_capability 结构体指针,驱动填充该结构体后返回。结构体定义如下:

struct v4l2_capability {
    __u8  driver[16];   // 驱动名称,如 "uvcvideo"
    __u8  card[32];     // 设备名称,如 "UVC Camera"
    __u8  bus_info[32]; // 总线信息,如 "usb-0000:00:14.0-1"
    __u32 version;      // 驱动版本号(内核版本)
    __u32 capabilities; // 设备能力标志位
    __u32 device_caps;  // 设备特定能力(内核 3.3+)
    __u32 reserved[3];
};

capabilities 字段的关键标志位:

标志位 值 含义
V4L2_CAP_VIDEO_CAPTURE 0x00000001 支持视频捕获(从摄像头采集帧)
V4L2_CAP_STREAMING 0x04000000 支持 streaming I/O(mmap / userptr / dmabuf)
V4L2_CAP_READWRITE 0x01000000 支持 read / write 方式读取帧
V4L2_CAP_EXT_PIX_FORMAT 0x00200000 支持扩展像素格式描述
实践要点: 在调用 VIDIOC_QUERYCAP 后,必须检查 capabilities 是否包含 V4L2_CAP_VIDEO_CAPTURE 和 V4L2_CAP_STREAMING。如果缺少 V4L2_CAP_STREAMING,则无法使用 mmap 映射方式采集帧,只能退化为 read 方式。
VIDIOC_QUERYCAP 调用流程 应用层 V4L2 核心层 设备驱动层 ioctl(fd, VIDIOC_QUERYCAP, &cap) 调用驱动回调 填充 v4l2_capability 返回用户空间 检查要点 cap.capabilities & V4L2_CAP_VIDEO_CAPTURE → 必须为真 cap.capabilities & V4L2_CAP_STREAMING → 必须为真(mmap 方式) driver="uvcvideo" card="USB Camera" bus_info="usb-0000:00:14.0-1" capabilities=0x84200001 → VIDEO_CAPTURE | STREAMING | EXT_PIX_FORMAT

3.2 VIDIOC_ENUM_FMT:枚举支持的格式

确认设备具备视频捕获能力后,下一步是查询设备支持的像素格式。V4L2 设备可能支持多种格式,如 YUYV、MJPEG、NV12、RGB565 等。应用层通过 VIDIOC_ENUM_FMT 逐个枚举,直到驱动返回 EINVAL 表示枚举结束。

调用时使用 struct v4l2_fmtdesc 结构体:

struct v4l2_fmtdesc {
    __u32 index;        // 枚举索引,从 0 开始递增
    __u32 type;         // 缓冲区类型:V4L2_BUF_TYPE_VIDEO_CAPTURE
    __u32 flags;        // 标志位:V4L2_FMT_FLAG_COMPRESSED 等
    __u8  description[32]; // 格式描述,如 "Motion-JPEG"
    __u32 pixelformat;  // 四字符码,如 V4L2_PIX_FMT_MJPEG
    __u32 reserved[4];
};

常见像素格式四字符码:

四字符码 数值 描述 是否压缩
V4L2_PIX_FMT_YUYV 0x56595559 YUYV 4:2:2 打包格式 否
V4L2_PIX_FMT_MJPEG 0x47504A4D Motion-JPEG 压缩流 是
V4L2_PIX_FMT_NV12 0x3231564E NV12 YUV 4:2:0 平面格式 否
V4L2_PIX_FMT_RGB565 0x20424752 RGB565 16 位 否
V4L2_PIX_FMT_H264 0x34363248 H.264 编码流 是
常见陷阱: 枚举时 type 字段必须与后续 VIDIOC_S_FMT 使用的类型一致。对于视频捕获设备,type 固定为 V4L2_BUF_TYPE_VIDEO_CAPTURE。如果设备同时支持视频输出,则需分别枚举 V4L2_BUF_TYPE_VIDEO_OUTPUT。
VIDIOC_ENUM_FMT 枚举流程 index = 0 ioctl 成功 记录 pixelformat index++ 继续 EINVAL(枚举结束) 枚举完成 枚举结果示例(UVC 摄像头) index=0: YUYV 4:2:2 (pixelformat=0x56595559) index=1: Motion-JPEG (pixelformat=0x47504A4D) flags=COMPRESSED

3.3 VIDIOC_S_FMT:设置采集格式

枚举完支持的格式后,应用层通过 VIDIOC_S_FMT 设置最终的采集参数,包括图像宽度、高度、像素格式、帧间隔等。这是帧采集前最关键的一步,因为它决定了后续 mmap 缓冲区的大小和布局。

调用时使用 struct v4l2_format 结构体:

struct v4l2_format {
    __u32 type;                 // V4L2_BUF_TYPE_VIDEO_CAPTURE
    union {
        struct v4l2_pix_format  pix;     // 单平面格式
        struct v4l2_pix_format_mplane  pix_mp; // 多平面格式
        // ... 其他类型
    } fmt;
};

struct v4l2_pix_format {
    __u32 width;        // 图像宽度(像素)
    __u32 height;       // 图像高度(像素)
    __u32 pixelformat;  // 像素格式四字符码
    __u32 field;        // 场序:V4L2_FIELD_NONE / V4L2_FIELD_INTERLACED
    __u32 bytesperline; // 每行字节数(对齐后)
    __u32 sizeimage;    // 一帧图像的总字节数
    __u32 colorspace;   // 色彩空间:V4L2_COLORSPACE_SRGB 等
    __u32 priv;         // 私有标志
};

设置格式的标准步骤:

  1. 先调用 VIDIOC_TRY_FMT(可选):测试驱动是否接受该格式,但不实际生效。
  2. 调用 VIDIOC_S_FMT:设置格式,驱动可能调整 width/height/bytesperline 等参数。
  3. 读取返回的 v4l2_format:驱动会回写实际采用的参数,应用层必须使用回写后的值。
重要原则: 驱动有权修改应用层传入的参数。例如,应用层请求 1920x1080,但硬件不支持,驱动可能调整为 1280x720。应用层必须读取 S_FMT 返回后的 width、height、bytesperline、sizeimage,这些值将直接决定 mmap 映射的缓冲区大小。
VIDIOC_S_FMT 参数协商流程 应用请求参数 1920x1080 YUYV 驱动协商调整 检查硬件能力 驱动回写参数 1280x720 YUYV 应用读取回写值,更新 width/height/sizeimage S_FMT 前后参数对比 参数 请求值 实际值(回写) 说明 width 1920 1280 硬件不支持 1920 height 1080 720 等比缩放 sizeimage 4147200 1843200 1280*720*2

3.4 其他关键 ioctl 简介

除了上述三个核心 ioctl,帧采集流程中还会用到以下 ioctl:

ioctl 用途 调用时机
VIDIOC_TRY_FMT 测试格式是否被支持,不实际设置 S_FMT 之前(可选)
VIDIOC_G_FMT 获取当前格式参数 任意时刻
VIDIOC_REQBUFS 申请视频缓冲区(mmap 的前提) S_FMT 之后
VIDIOC_QUERYBUF 查询缓冲区信息(用于 mmap) REQBUFS 之后
VIDIOC_QBUF 将缓冲区放入驱动队列 流启动前后
VIDIOC_DQBUF 从驱动队列取出已填满的缓冲区 流启动后
VIDIOC_STREAMON 启动视频流 QBUF 之后
VIDIOC_STREAMOFF 停止视频流 采集结束

3.5 完整调用序列与代码示例

以下代码片段展示了从打开设备到设置格式的完整流程:

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

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

    // 1. VIDIOC_QUERYCAP:查询设备能力
    struct v4l2_capability cap;
    memset(&cap, 0, sizeof(cap));
    if (ioctl(fd, VIDIOC_QUERYCAP, &cap) < 0) {
        perror("VIDIOC_QUERYCAP");
        close(fd);
        return -1;
    }

    if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) {
        fprintf(stderr, "设备不支持视频捕获\n");
        close(fd);
        return -1;
    }
    if (!(cap.capabilities & V4L2_CAP_STREAMING)) {
        fprintf(stderr, "设备不支持 streaming I/O\n");
        close(fd);
        return -1;
    }

    // 2. VIDIOC_ENUM_FMT:枚举格式(示例只枚举前两个)
    struct v4l2_fmtdesc fmt;
    memset(&fmt, 0, sizeof(fmt));
    fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    fmt.index = 0;
    while (ioctl(fd, VIDIOC_ENUM_FMT, &fmt) == 0) {
        printf("格式 %d: %s (0x%08x)\n",
               fmt.index, fmt.description, fmt.pixelformat);
        fmt.index++;
    }

    // 3. VIDIOC_S_FMT:设置格式为 640x480 YUYV
    struct v4l2_format format;
    memset(&format, 0, sizeof(format));
    format.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    format.fmt.pix.width = 640;
    format.fmt.pix.height = 480;
    format.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
    format.fmt.pix.field = V4L2_FIELD_NONE;

    if (ioctl(fd, VIDIOC_S_FMT, &format) < 0) {
        perror("VIDIOC_S_FMT");
        close(fd);
        return -1;
    }

    // 读取驱动回写的实际参数
    printf("实际分辨率: %dx%d\n",
           format.fmt.pix.width, format.fmt.pix.height);
    printf("每帧大小: %d bytes\n", format.fmt.pix.sizeimage);
    printf("每行字节数: %d\n", format.fmt.pix.bytesperline);

    // 后续:VIDIOC_REQBUFS → VIDIOC_QUERYBUF → mmap → ...
    close(fd);
    return 0;
}
调试技巧: 使用 v4l2-ctl --list-formats 和 v4l2-ctl --get-fmt-video 命令行工具可以快速查看设备支持的格式和当前格式,方便对比应用层代码的行为是否正确。

3.6 常见错误与排查

错误码 常见原因 排查方向
EINVAL type 字段错误、pixelformat 不支持、设备节点错误 检查 type 是否为 V4L2_BUF_TYPE_VIDEO_CAPTURE;确认枚举结果
EBUSY 设备已被其他进程占用 使用 fuser /dev/video0 查看占用进程
ENODEV 设备不存在或驱动未加载 检查 ls /dev/video* 和 dmesg
ENOMEM 内核内存不足 减少缓冲区数量或分辨率