4. V4L2设备节点与IOCTL:/dev/videoX节点创建、核心ioctl命令详解

V4L2(Video for Linux 2)是Linux内核中为视频设备提供的标准驱动框架。在Camera HAL3架构中,HAL层通过操作V4L2设备节点与内核驱动交互,完成视频流的捕获与控制。本章将深入剖析V4L2设备节点的创建过程,并详细讲解最核心的ioctl命令,包括设备能力查询、格式枚举与设置、缓冲区管理以及流控制。

4.1 /dev/videoX设备节点创建机制

在Linux系统中,V4L2设备节点通常位于/dev/videoX(X为数字编号,如/dev/video0、/dev/video1)。这些节点由内核中的V4L2框架在驱动注册时动态创建,是用户空间程序与内核驱动通信的桥梁。

4.1.1 节点创建流程

  1. 驱动注册:摄像头驱动调用video_register_device()函数向V4L2核心注册一个video_device结构体实例。
  2. 主设备号分配:V4L2核心使用主设备号81(VIDEO_MAJOR),次设备号从0开始递增。
  3. udev规则触发:内核创建设备后,udev根据规则在/dev/目录下生成videoX节点,并设置适当的权限(通常为crw-rw----,属主为root,属组为video)。
  4. 用户空间访问:应用程序通过open("/dev/video0", O_RDWR)打开节点,获得文件描述符,后续通过ioctl进行控制。
提示: 在嵌入式系统中,如果缺少udev,可以手动使用mknod /dev/video0 c 81 0创建设备节点。主设备号81固定,次设备号与驱动注册顺序相关。

4.1.2 节点类型与功能

节点类型 典型路径 功能描述
视频捕获 /dev/video0 从摄像头传感器获取视频帧数据
视频输出 /dev/video1 向显示设备输出视频帧
视频元数据 /dev/video2 传输3A统计信息、ISP参数等元数据
视频覆盖 /dev/video3 硬件叠加层,用于预览合成
注意: 在Camera HAL3中,一个物理摄像头可能对应多个/dev/videoX节点。例如,高通平台通常使用/dev/video0作为主图像节点,/dev/video1作为统计信息节点。HAL层需要根据平台配置正确绑定这些节点。

4.2 核心ioctl命令详解

ioctl(Input/Output Control)是V4L2框架中用户空间与内核驱动通信的主要方式。每个ioctl命令对应一个特定的控制操作,通过ioctl(fd, VIDIOC_XXX, &arg)调用。下面按照典型的V4L2初始化流程,逐一讲解最核心的ioctl命令。

4.2.1 查询设备能力:VIDIOC_QUERYCAP

这是打开设备节点后第一个必须调用的ioctl,用于获取设备的基本能力信息,包括驱动名称、设备类型、总线信息以及支持的I/O方法。

struct v4l2_capability cap;
int ret = ioctl(fd, VIDIOC_QUERYCAP, &cap);
if (ret < 0) {
    // 处理错误
}

// 检查设备类型:必须支持视频捕获
if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) {
    // 不是捕获设备
}

// 检查是否支持流式I/O(mmap模式)
if (!(cap.capabilities & V4L2_CAP_STREAMING)) {
    // 不支持流式I/O,可能需要使用read/write
}

关键字段说明:

  • driver:驱动名称,如"uvcvideo"、"msm_camera"。
  • card:设备名称,如"UVC Camera (046d:0825)"。
  • bus_info:总线位置,如"usb-0000:00:14.0-1"。
  • capabilities:位掩码,标识设备支持的功能(捕获、输出、调谐器、流式I/O等)。

4.2.2 枚举与设置格式:VIDIOC_ENUM_FMT / VIDIOC_S_FMT

在确认设备能力后,下一步是枚举设备支持的像素格式,并设置当前使用的格式。

枚举格式:VIDIOC_ENUM_FMT
struct v4l2_fmtdesc fmt_desc;
fmt_desc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt_desc.index = 0;  // 从0开始递增

while (ioctl(fd, VIDIOC_ENUM_FMT, &fmt_desc) == 0) {
    printf("Format index %d: %s (fourcc: %c%c%c%c)\n",
           fmt_desc.index, fmt_desc.description,
           (fmt_desc.pixelformat >> 0) & 0xFF,
           (fmt_desc.pixelformat >> 8) & 0xFF,
           (fmt_desc.pixelformat >> 16) & 0xFF,
           (fmt_desc.pixelformat >> 24) & 0xFF);
    fmt_desc.index++;
}
设置格式:VIDIOC_S_FMT
struct v4l2_format fmt;
memset(&fmt, 0, sizeof(fmt));
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt.fmt.pix.width = 1920;
fmt.fmt.pix.height = 1080;
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_NV12;  // YUV 4:2:0 半平面
fmt.fmt.pix.field = V4L2_FIELD_NONE;

int ret = ioctl(fd, VIDIOC_S_FMT, &fmt);
if (ret < 0) {
    // 格式不支持,驱动会调整到最接近的格式
}

// 实际设置的格式可能不同于请求值,需要读取fmt结构体确认
printf("Actual resolution: %dx%d\n", fmt.fmt.pix.width, fmt.fmt.pix.height);
实战要点: 调用VIDIOC_S_FMT后,驱动可能会修改width、height和pixelformat字段。应用程序必须读取返回的结构体,使用实际值进行后续操作。常见调整包括:将宽度对齐到16像素、将不支持的格式转换为默认格式等。

4.2.3 请求缓冲区:VIDIOC_REQBUFS

设置格式后,需要向驱动请求用于视频帧传输的缓冲区。V4L2支持多种I/O模式,最常用的是mmap模式。

struct v4l2_requestbuffers req;
memset(&req, 0, sizeof(req));
req.count = 4;           // 请求4个缓冲区
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
req.memory = V4L2_MEMORY_MMAP;  // 使用mmap模式

int ret = ioctl(fd, VIDIOC_REQBUFS, &req);
if (ret < 0) {
    // 错误处理
}

// 实际分配的缓冲区数量可能少于请求值
printf("Allocated %d buffers\n", req.count);

4.2.4 查询与映射缓冲区:VIDIOC_QUERYBUF

缓冲区分配完成后,需要查询每个缓冲区的物理地址和大小,并通过mmap映射到用户空间。

struct v4l2_buffer buf;
memset(&buf, 0, sizeof(buf));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = 0;  // 查询第0个缓冲区

ioctl(fd, VIDIOC_QUERYBUF, &buf);

// 映射缓冲区到用户空间
void *buffer = mmap(NULL, buf.length, PROT_READ | PROT_WRITE,
                    MAP_SHARED, fd, buf.m.offset);

// 保存buffer地址和长度,用于后续帧处理

4.2.5 入队与出队缓冲区:VIDIOC_QBUF / VIDIOC_DQBUF

这是视频流传输的核心操作,构成一个生产者-消费者循环。

  • VIDIOC_QBUF:将缓冲区放入驱动队列,等待硬件填充数据。
  • VIDIOC_DQBUF:从驱动队列取出已填充数据的缓冲区。
// 入队所有缓冲区
for (int i = 0; i < req.count; 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;
    ioctl(fd, VIDIOC_QBUF, &buf);
}

// 开始流传输后,循环出队处理
struct v4l2_buffer buf;
memset(&buf, 0, sizeof(buf));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;

ioctl(fd, VIDIOC_DQBUF, &buf);  // 阻塞等待帧数据

// 处理buf.index对应的缓冲区数据
process_frame(buffers[buf.index], buf.bytesused);

// 处理完成后重新入队
ioctl(fd, VIDIOC_QBUF, &buf);

4.2.6 流控制:VIDIOC_STREAMON / VIDIOC_STREAMOFF

这两个命令控制视频流的启动和停止。

int type = V4L2_BUF_TYPE_VIDEO_CAPTURE;

// 启动流
ioctl(fd, VIDIOC_STREAMON, &type);

// 停止流
ioctl(fd, VIDIOC_STREAMOFF, &type);
重要: 调用VIDIOC_STREAMOFF后,驱动会丢弃所有已入队但未处理的缓冲区。应用程序需要重新调用VIDIOC_QBUF入队所有缓冲区,才能再次启动流。

4.3 V4L2初始化完整流程

下面的SVG图展示了从打开设备节点到开始视频流捕获的完整初始化流程,以及每个步骤对应的核心ioctl命令。

V4L2初始化流程与核心ioctl命令 1. open("/dev/video0") 2. VIDIOC_QUERYCAP 3. VIDIOC_ENUM_FMT 4. VIDIOC_S_FMT 5. VIDIOC_REQBUFS 6. VIDIOC_QUERYBUF + mmap 7. QBUF循环 → STREAMON 关键说明: • 步骤1-6为初始化阶段 • 步骤7进入运行阶段 • 运行阶段循环:DQBUF→处理→QBUF ioctl分类: • 查询类:QUERYCAP, ENUM_FMT • 配置类:S_FMT, REQBUFS • 控制类:QBUF, DQBUF • 流控类:STREAMON/OFF

4.4 常用ioctl命令速查表

ioctl命令 功能 调用时机 关键结构体
VIDIOC_QUERYCAP 查询设备能力 打开设备后立即调用 v4l2_capability
VIDIOC_ENUM_FMT 枚举支持的像素格式 查询能力之后 v4l2_fmtdesc
VIDIOC_S_FMT 设置当前格式 枚举格式之后 v4l2_format
VIDIOC_G_FMT 获取当前格式 任意时刻 v4l2_format
VIDIOC_REQBUFS 请求分配缓冲区 设置格式之后 v4l2_requestbuffers
VIDIOC_QUERYBUF 查询缓冲区信息 请求缓冲区之后 v4l2_buffer
VIDIOC_QBUF 入队缓冲区 映射缓冲区后、流启动前 v4l2_buffer
VIDIOC_DQBUF 出队缓冲区 流启动后循环调用 v4l2_buffer
VIDIOC_STREAMON 启动视频流 所有缓冲区入队后 int(缓冲区类型)
VIDIOC_STREAMOFF 停止视频流 任意时刻 int(缓冲区类型)

4.5 实战:V4L2初始化代码框架

下面是一个完整的V4L2初始化代码框架,整合了上述所有核心ioctl命令,展示了从打开设备到开始流捕获的标准流程。

#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 DEVICE "/dev/video0"
#define WIDTH  1920
#define HEIGHT 1080
#define BUFFER_COUNT 4

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

int main() {
    int fd;
    struct v4l2_capability cap;
    struct v4l2_format fmt;
    struct v4l2_requestbuffers req;
    struct v4l2_buffer buf;
    struct buffer buffers[BUFFER_COUNT];
    int ret;

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

    // 2. 查询设备能力
    ret = ioctl(fd, VIDIOC_QUERYCAP, &cap);
    if (ret < 0 || !(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) {
        fprintf(stderr, "Not a capture device\n");
        close(fd);
        return -1;
    }

    // 3. 设置格式
    memset(&fmt, 0, sizeof(fmt));
    fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    fmt.fmt.pix.width = WIDTH;
    fmt.fmt.pix.height = HEIGHT;
    fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_NV12;
    fmt.fmt.pix.field = V4L2_FIELD_NONE;

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

    // 4. 请求缓冲区
    memset(&req, 0, sizeof(req));
    req.count = BUFFER_COUNT;
    req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    req.memory = V4L2_MEMORY_MMAP;

    ret = ioctl(fd, VIDIOC_REQBUFS, &req);
    if (ret < 0) {
        perror("VIDIOC_REQBUFS");
        close(fd);
        return -1;
    }

    // 5. 查询并映射缓冲区
    for (int i = 0; i < req.count; i++) {
        memset(&buf, 0, sizeof(buf));
        buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        buf.memory = V4L2_MEMORY_MMAP;
        buf.index = i;

        ret = ioctl(fd, VIDIOC_QUERYBUF, &buf);
        if (ret < 0) {
            perror("VIDIOC_QUERYBUF");
            close(fd);
            return -1;
        }

        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");
            close(fd);
            return -1;
        }
    }

    // 6. 入队所有缓冲区
    for (int i = 0; i < req.count; i++) {
        memset(&buf, 0, sizeof(buf));
        buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        buf.memory = V4L2_MEMORY_MMAP;
        buf.index = i;
        ioctl(fd, VIDIOC_QBUF, &buf);
    }

    // 7. 启动流
    int type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    ret = ioctl(fd, VIDIOC_STREAMON, &type);
    if (ret < 0) {
        perror("VIDIOC_STREAMON");
        close(fd);
        return -1;
    }

    printf("V4L2 initialized successfully. Capturing %dx%d NV12...\n",
           fmt.fmt.pix.width, fmt.fmt.pix.height);

    // 8. 帧捕获循环(示例:捕获10帧)
    for (int frame = 0; frame < 10; frame++) {
        memset(&buf, 0, sizeof(buf));
        buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        buf.memory = V4L2_MEMORY_MMAP;

        ret = ioctl(fd, VIDIOC_DQBUF, &buf);
        if (ret < 0) {
            perror("VIDIOC_DQBUF");
            break;
        }

        // 处理帧数据:buffers[buf.index].start, buf.bytesused
        printf("Frame %d captured, size: %d bytes\n",
               frame, buf.bytesused);

        // 重新入队
        ioctl(fd, VIDIOC_QBUF, &buf);
    }

    // 9. 停止流
    ioctl(fd, VIDIOC_STREAMOFF, &type);

    // 10. 清理
    for (int i = 0; i < req.count; i++) {
        munmap(buffers[i].start, buffers[i].length);
    }
    close(fd);

    return 0;
}
实战要点总结:
  • 每个ioctl调用后必须检查返回值,驱动可能调整参数。
  • VIDIOC_S_FMT后读取实际格式,不要假设请求值被完全接受。
  • 缓冲区映射使用MAP_SHARED标志,确保内核与用户空间共享同一物理内存。
  • 流停止后,所有缓冲区状态被重置,需要重新入队才能再次启动。
  • 在Camera HAL3中,上述流程通常由HAL层的configureStreams()和processCaptureRequest()方法触发。

4.6 常见问题与调试技巧

问题现象 可能原因 排查方法
VIDIOC_QUERYCAP返回ENOTTY 设备节点不是V4L2设备 检查/dev/videoX主设备号是否为81
VIDIOC_S_FMT返回EINVAL 格式或分辨率不支持 先调用VIDIOC_ENUM_FMT和VIDIOC_ENUM_FRAMESIZES确认支持列表
VIDIOC_REQBUFS返回EBUSY 流正在运行或缓冲区已分配 先调用VIDIOC_STREAMOFF,或检查是否重复调用
VIDIOC_DQBUF阻塞超时 驱动未产生帧数据 检查传感器时钟、MIPI链路、ISP配置;使用O_NONBLOCK打开设备测试
mmap返回ENODEV 设备不支持mmap模式 检查cap.capabilities是否包含V4L2_CAP_STREAMING
调试利器: 使用v4l2-ctl工具可以快速验证设备节点是否正常工作。例如:v4l2-ctl -d /dev/video0 --list-formats枚举格式,v4l2-ctl -d /dev/video0 --set-fmt-video=width=1920,height=1080,pixelformat=NV12设置格式。这些工具底层使用的正是本章讲解的ioctl命令。