Contents

V4l2loopback实现笔记(四):V4L2(Video for Linux 2) 框架入门

本模块演示 V4L2 (Video for Linux 2) 子系统的基础用法,创建一个可被 v4l2 工具识别的视频设备,支持基本的格式查询和设置操作。

实现的功能

  • V4L2 设备注册 (v4l2_device_register, video_register_device)
  • 设备能力查询 (VIDIOC_QUERYCAP)
  • 格式枚举和协商 (VIDIOC_ENUM_FMT, VIDIOC_G_FMT, VIDIOC_S_FMT, VIDIOC_TRY_FMT)
  • 多种像素格式支持
  • 设备能力标志设置

学习要点

  1. 理解 v4l2_devicevideo_device 的关系
  2. 理解 v4l2_ioctl_ops 的结构和用途
  3. 理解格式协商的 try/set 模式
  4. 理解设备能力标志的含义
  5. 理解 V4L2 文件操作与标准文件操作的区别

V4L2 核心概念

ioctl 操作分类

类别操作用途
查询QUERYCAP设备能力
格式ENUM_FMT, G/S/TRY_FMT格式协商
缓冲区REQBUFS, QUERYBUF, QBUF, DQBUF缓冲区管理
流控制STREAMON, STREAMOFF流控制
控制QUERYCTRL, G/S_CTRL设备参数

格式协商流程

应用程序                          驱动程序
    │                                │
    ├── ENUM_FMT ─────────────────►  │  枚举支持的格式
    │                                │
    ├── TRY_FMT  ─────────────────►  │  尝试格式(驱动调整)
    │  ◄───────────────────────────  │  返回调整后的格式
    │                                │
    ├── S_FMT    ─────────────────►  │  设置格式
    │                                │
    └── G_FMT    ─────────────────►  │  确认当前格式

问题与思考

  1. kzalloc是什么?和kmalloc有什么区别?什么时候用哪个? kzalloc = kmalloc + memset(…, 0, …), 用于分配并初始化为0的内存块。kmalloc分配的内存未初始化,可能包含垃圾值。通常在需要清零内存时使用kzalloc,否则使用kmalloc以节省初始化开销。
  2. v4l2_device和video_device的区别和联系是什么?
  3. v4l2_ioctl_ops结构体的作用是什么?如何实现自定义的ioctl操作?

V4L2 device structure

struct v4l2_framework_device {
    struct v4l2_device v4l2_dev;
    struct video_device vdev;
    struct v4l2_fh vfh;

    /* Format settings */
    struct v4l2_format current_format;
    struct mutex lock;  // Protection for format changes

    /* Frame dimensions */
    int width;
    int height;
    u32 format;
};

V4L2 框架的核心对象:

  • struct v4l2_device v4l2_dev: V4L2 设备的核心结构,包含设备的基本信息和状态。父设备对象,这是 V4L2 驱动的顶层实例。所有的 V4L2 驱动必须先注册它,用来管理底层的子设备(Sub-devices)。
  • struct video_device vdev: 视频设备结构,表示一个具体的视频设备实例。字符设备节点,它包含了设备的文件操作、IOCTL 操作等信息。表示一个具体的视频设备节点(如 /dev/video0)。
  • struct v4l2_fh vfh: V4L2 文件句柄(File Handle),表示一个打开的设备文件实例。文件句柄,用于管理每个打开的设备实例的状态和操作(比如事件订阅、优先级)。

设备的当前配置 (State):

  • struct v4l2_format current_format: 当前的视频格式设置,包含分辨率、像素格式等信息。
  • struct mutex lock: 互斥锁,用于保护对设备状态的并发访问,确保线程安全。

便捷访问的缓存数据:

这些数据包含于current_format中,但为了简化访问,通常会单独缓存:

  • int width: 当前视频帧的宽度。
  • int height: 当前视频帧的高度。
  • u32 format: 当前的像素格式四字符代码(FourCC),例如 V4L2_PIX_FMT_YUYV。

V4L2 ioctl operations

static const struct v4l2_ioctl_ops v4l2_fw_ioctl_ops = {
    .vidioc_querycap = v4l2_fw_querycap,

    .vidioc_enum_fmt_vid_out = v4l2_fw_enum_fmt,
    .vidioc_g_fmt_vid_out = v4l2_fw_g_fmt,
    .vidioc_s_fmt_vid_out = v4l2_fw_s_fmt,
    .vidioc_try_fmt_vid_out = v4l2_fw_try_fmt,

    .vidioc_enum_fmt_vid_cap = v4l2_fw_enum_fmt,
    .vidioc_g_fmt_vid_cap = v4l2_fw_g_fmt,
    .vidioc_s_fmt_vid_cap = v4l2_fw_s_fmt,
    .vidioc_try_fmt_vid_cap = v4l2_fw_try_fmt,
};
  • vidioc_querycap: 查询设备能力的 IOCTL 操作。驱动程序通过实现 v4l2_fw_querycap 函数来返回设备的能力信息,如支持的功能、驱动名称等。
  • vidioc_enum_fmt_vid_outvidioc_enum_fmt_vid_cap: 枚举输出和输入视频格式的 IOCTL 操作。驱动程序通过实现 v4l2_fw_enum_fmt 函数来列出设备支持的像素格式。
  • vidioc_g_fmt_vid_outvidioc_g_fmt_vid_cap: 获取当前输出和输入视频格式的 IOCTL 操作。驱动程序通过实现 v4l2_fw_g_fmt 函数来返回当前设置的视频格式。
  • vidioc_s_fmt_vid_outvidioc_s_fmt_vid_cap: 设置输出和输入视频格式的 IOCTL 操作。驱动程序通过实现 v4l2_fw_s_fmt 函数来更新设备的视频格式设置。
  • vidioc_try_fmt_vid_outvidioc_try_fmt_vid_cap: 尝试设置输出和输入视频格式的 IOCTL 操作。驱动程序通过实现 v4l2_fw_try_fmt 函数来验证所请求的视频格式是否被支持,而不实际应用该格式。

功能实现

0. 基础函数

  • strscpy (String Safe Copy) 这是 Linux 内核特有 的安全字符串复制函数(在标准 C 库中没有,标准库对应的是 strncpy,但 strscpy 更安全、更好用)。
#include <linux/string.h>

ssize_t strscpy(char *dest, const char *src, size_t count);

参数解释: - char *dest:目标字符串缓冲区的指针。 - const char *src:源字符串的指针。 - size_t count:目标缓冲区的大小(包括终止符,通常直接传入 sizeof(dest))。

  • snprintf (String N Print Formatted)

这是 标准 C 语言库函数,Linux 内核也实现了它。它用于格式化字符串

#include <linux/kernel.h> // 内核中
// 或者 #include <stdio.h> // 用户空间中

int snprintf(char *buf, size_t size, const char *fmt, ...);

参数解释: - char *buf:目标字符串缓冲区的指针。 - size_t size:目标缓冲区的大小(包括终止符)。 - const char *fmt:格式字符串,类似于 printf 的格式。 - ...:可变参数,根据格式字符串提供的值。

1. v4l2_fw_querycap —— 查询设备身份 (Capability)

  • 对应命令VIDIOC_QUERYCAP

  • 功能:这是应用程序连接设备后调用的第一个函数,相当于查阅设备的“身份证”和“简历”。

static int v4l2_fw_querycap(struct file *file, void *priv,
                           struct v4l2_capability *cap)
{
    strscpy(cap->driver, "v4l2_framework_v4l2", sizeof(cap->driver));
    strscpy(cap->card, "V4L2 Framework Learning Device", sizeof(cap->card));
    snprintf(cap->bus_info, sizeof(cap->bus_info), "platform:v4l2_framework_v4l2");

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

    return 0;
}
  • 参数详解:

    • struct file *file:代表应用程序打开的文件句柄。 当应用程序调用 open("/dev/video0", …) 时,内核创建了这个结构体。如果 10 个不同的程序打开了同一个摄像头,会有 10 个不同的 file * 指针。驱动通常用 video_drvdata(file) 通过它来找到你的 struct v4l2_framework_device
    • void *priv:每文件句柄私有数据,通常用于存储与文件句柄相关的特定信息。当文件打开时,v4l2_fh_open 会创建一个 struct v4l2_fh,并把它挂在 file->private_data 上。调用 querycap 之前,V4L2 核心层自动把 file->private_data 取出来,转成 void * 传给你。在大多数简单驱动中,这个参数可能未被使用。
    • struct v4l2_capability *cap:这是输出参数,驱动通过填充这个结构体来告诉应用程序设备的能力和身份信息。这是一个指向内核栈(或内核堆)内存的指针,不是用户空间那个变量的地址。V4L2 核心层为了安全,先在内核里申请了一块空地。你的任务就是往这个内存里填数据(填 driver 名、支持什么功能等)。函数返回后,核心层会把这块内存里的内容拷贝回用户空间。
  • 调用路径:假设用户空间程序(比如 v4l2-ctl)想查询设备能力:

    • 第一步:用户空间 (User Space) 发起。程序声明一个变量,并调用 ioctl:
      struct v4l2_capability user_cap; // 1. 用户准备空结构体
      ioctl(fd, VIDIOC_QUERYCAP, &user_cap); // 2. 发起系统调用
      
    • 第二步:系统调用入口 (System Call)。Linux 内核捕获请求,找到该文件对应的驱动操作集 .unlocked_ioctl,也就是 video_ioctl2。
    • 第三步:中间人 video_ioctl2 (V4L2 Core)。负责准备参数,从从系统调用上下文拿到当前文件的 file 指针,读取 file->private_data 赋值给 priv。在内核内存中声明一个 struct v4l2_capability kern_cap,然后调用驱动的 v4l2_fw_querycap(file, priv, &kern_cap)。
    • 第四步:驱动实现 v4l2_fw_querycap (Driver Implementation)。驱动填充 kern_cap 结构体,告诉应用程序设备的能力和身份信息。函数返回后,控制权回到 video_ioctl2。
    • 第五步:返回用户空间 (Return to User Space)。video_ioctl2 调用copy_to_user(&user_cap, &kern_cap, …) 将 kern_cap 的内容复制回用户空间的 user_cap 变量,完成 ioctl 调用。
  • 代码实现细节

    • driver / card / bus_info:填写驱动名、设备名和总线位置,用于标识身份。

    • device_caps:最关键的部分。这里通过位掩码告诉应用程序:

      • V4L2_CAP_VIDEO_CAPTURE:我是个摄像头(能采集视频)。

      • V4L2_CAP_VIDEO_OUTPUT:我是个播放器(能输出视频)。

      • V4L2_CAP_STREAMING:我支持流式 I/O(可以用 mmap 传数据)。

    • 作用:应用程序根据这里返回的标志位,决定是否继续后续操作(比如,如果我想录像,但你没有 CAPTURE 标志,我就报错退出)。

2. v4l2_fw_enum_fmt —— 枚举支持的像素格式 (Format Enumeration)

  • 对应命令VIDIOC_ENUM_FMT

  • 功能:这是设备的“菜单”。应用程序通过不断增加 f->index(0, 1, 2…)循环调用此函数,直到返回错误,从而获知设备支持的所有像素格式。

static int v4l2_fw_enum_fmt(struct file *file, void *priv,
                           struct v4l2_fmtdesc *f)
{
    if (f->index >= ARRAY_SIZE(supported_formats) - 1) {
        return -EINVAL;
    }

    if (supported_formats[f->index] == 0) {
        return -EINVAL;
    }

    f->pixelformat = supported_formats[f->index];

    // Set description based on format
    switch (supported_formats[f->index]) {
        case V4L2_PIX_FMT_RGB24:
            strscpy(f->description, "RGB 24-bit", sizeof(f->description));
            break;
        case V4L2_PIX_FMT_BGR24:
            strscpy(f->description, "BGR 24-bit", sizeof(f->description));
            break;
        case V4L2_PIX_FMT_YUV420:
            strscpy(f->description, "YUV 4:2:0", sizeof(f->description));
            break;
        case V4L2_PIX_FMT_YUYV:
            strscpy(f->description, "YUYV 4:2:2", sizeof(f->description));
            break;
        case V4L2_PIX_FMT_UYVY:
            strscpy(f->description, "UYVY 4:2:2", sizeof(f->description));
            break;
        case V4L2_PIX_FMT_NV12:
            strscpy(f->description, "NV12", sizeof(f->description));
            break;
        default:
            snprintf(f->description, sizeof(f->description), "Format %u", f->pixelformat);
            break;
    }

    return 0;
}

3. v4l2_fw_g_fmt —— 获取当前格式 (Get Format)

  • 对应命令VIDIOC_G_FMT
  • 功能:应用程序调用此函数以查询当前设备的视频格式设置,包括分辨率和像素格式。
static int v4l2_fw_g_fmt(struct file *file, void *priv,
                        struct v4l2_format *fmt)
{
    struct v4l2_framework_device *dev = v4l2_get_device(file);

    mutex_lock(&dev->lock);
    fmt->fmt.pix = dev->current_format.fmt.pix;
    mutex_unlock(&dev->lock);

    return 0;
}
  • 代码实现细节

    • 获取上下文:调用 v4l2_get_device 拿到设备结构体。

    • 加锁 (mutex_lock):这是一个良好的内核编程习惯。防止在读取参数时,另一个进程正在修改它,导致读到一半旧一半新的脏数据。

    • 返回数据:将 dev->current_format 中的内容复制给用户

4. v4l2_fw_try_fmt —— 尝试设置格式 (Try Format)

  • 对应命令VIDIOC_TRY_FMT
  • 功能:应用程序调用此函数以验证所请求的视频格式是否被支持,而不实际应用该格式。
  • 注意:此函数只计算和修正,不保存。它不会改变设备的实际状态。
static int v4l2_fw_try_fmt(struct file *file, void *priv,
                          struct v4l2_format *fmt)
{
    bool format_supported = false;
    int i;

    // Validate pixel format
    for (i = 0; supported_formats[i] != 0; i++) {
        if (supported_formats[i] == fmt->fmt.pix.pixelformat) {
            format_supported = true;
            break;
        }
    }

    if (!format_supported) {
        fmt->fmt.pix.pixelformat = supported_formats[0];  // Use first supported format
    }

    // Clamp width and height to reasonable values
    if (fmt->fmt.pix.width < 16) fmt->fmt.pix.width = 16;
    if (fmt->fmt.pix.height < 16) fmt->fmt.pix.height = 16;
    if (fmt->fmt.pix.width > 4096) fmt->fmt.pix.width = 4096;
    if (fmt->fmt.pix.height > 4096) fmt->fmt.pix.height = 4096;

    // Ensure bytesperline is calculated correctly
    if (fmt->fmt.pix.bytesperline == 0) {
        if (fmt->fmt.pix.pixelformat == V4L2_PIX_FMT_RGB24 ||
            fmt->fmt.pix.pixelformat == V4L2_PIX_FMT_BGR24) {
            fmt->fmt.pix.bytesperline = fmt->fmt.pix.width * 3;
        } else if (fmt->fmt.pix.pixelformat == V4L2_PIX_FMT_YUV420 ||
                   fmt->fmt.pix.pixelformat == V4L2_PIX_FMT_NV12) {
            fmt->fmt.pix.bytesperline = fmt->fmt.pix.width;
        } else {
            fmt->fmt.pix.bytesperline = fmt->fmt.pix.width * 2;
        }
    }

    // Calculate sizeimage if not set
    if (fmt->fmt.pix.sizeimage == 0) {
        fmt->fmt.pix.sizeimage = calculate_frame_size(fmt->fmt.pix.width,
                                                    fmt->fmt.pix.height,
                                                    fmt->fmt.pix.pixelformat);
    }

    // Set color space if not set
    if (fmt->fmt.pix.colorspace == V4L2_COLORSPACE_DEFAULT) {
        fmt->fmt.pix.colorspace = V4L2_COLORSPACE_SRGB;
    }

    printk(KERN_INFO "v4l2_framework: Format tried for %dx%d, format=0x%x\n",
           fmt->fmt.pix.width, fmt->fmt.pix.height, fmt->fmt.pix.pixelformat);

    return 0;
}
  • 代码实现细节

    • 验证格式:检查请求的像素格式是否在 supported_formats 数组里。如果不支持,通常会强制改为默认格式(如数组第一个)。

    • 参数钳位 (Clamping):检查宽和高。太小改到 16,太大改到 4096。这非常重要,硬件通常有极限,不能任由软件随便设。

    • 计算衍生参数:根据修正后的宽、高,重新计算 bytesperline(步长)和 sizeimage(帧大小)。

    • 返回修正后的值:告诉应用程序

5. v4l2_fw_s_fmt —— 设置格式 (Set Format)

  • 对应命令VIDIOC_S_FMT
  • 功能:应用程序调用此函数以设置设备的视频格式,包括分辨率和像素格式。
static int v4l2_fw_s_fmt(struct file *file, void *priv,
                        struct v4l2_format *fmt)
{
    struct v4l2_framework_device *dev = v4l2_get_device(file);
    bool format_supported = false;
    int i;

    // Validate pixel format
    for (i = 0; supported_formats[i] != 0; i++) {
        if (supported_formats[i] == fmt->fmt.pix.pixelformat) {
            format_supported = true;
            break;
        }
    }

    if (!format_supported) {
        fmt->fmt.pix.pixelformat = supported_formats[0];  // Use first supported format
    }

    // Clamp width and height to reasonable values
    if (fmt->fmt.pix.width < 16) fmt->fmt.pix.width = 16;
    if (fmt->fmt.pix.height < 16) fmt->fmt.pix.height = 16;
    if (fmt->fmt.pix.width > 4096) fmt->fmt.pix.width = 4096;
    if (fmt->fmt.pix.height > 4096) fmt->fmt.pix.height = 4096;

    // Ensure bytesperline is calculated correctly
    if (fmt->fmt.pix.bytesperline == 0) {
        fmt->fmt.pix.bytesperline = (fmt->fmt.pix.width *
                                   fmt->fmt.pix.pixelformat == V4L2_PIX_FMT_RGB24 ? 3 :
                                   fmt->fmt.pix.pixelformat == V4L2_PIX_FMT_BGR24 ? 3 :
                                   fmt->fmt.pix.pixelformat == V4L2_PIX_FMT_YUV420 ? 1 :
                                   fmt->fmt.pix.pixelformat == V4L2_PIX_FMT_NV12 ? 1 : 2);
        fmt->fmt.pix.bytesperline *= fmt->fmt.pix.width;
    }

    // Calculate sizeimage if not set
    if (fmt->fmt.pix.sizeimage == 0) {
        fmt->fmt.pix.sizeimage = calculate_frame_size(fmt->fmt.pix.width,
                                                    fmt->fmt.pix.height,
                                                    fmt->fmt.pix.pixelformat);
    }

    // Set color space if not set
    if (fmt->fmt.pix.colorspace == V4L2_COLORSPACE_DEFAULT) {
        fmt->fmt.pix.colorspace = V4L2_COLORSPACE_SRGB;
    }

    mutex_lock(&dev->lock);
    dev->current_format.fmt.pix = fmt->fmt.pix;
    dev->width = fmt->fmt.pix.width;
    dev->height = fmt->fmt.pix.height;
    dev->format = fmt->fmt.pix.pixelformat;
    mutex_unlock(&dev->lock);

    // Copy result back to caller
    fmt->fmt.pix = dev->current_format.fmt.pix;

    printk(KERN_INFO "v4l2_framework: Format set to %dx%d, format=0x%x, size=%u\n",
           fmt->fmt.pix.width, fmt->fmt.pix.height,
           fmt->fmt.pix.pixelformat, fmt->fmt.pix.sizeimage);

    return 0;
}
  • 代码实现细节

    • 验证格式:与 try_fmt 类似,检查请求的像素格式是否被支持。

    • 参数钳位 (Clamping):检查宽和高,确保在合理范围内。

    • 计算衍生参数:根据修正后的宽、高,重新计算 bytesperline 和 sizeimage。

    • 更新设备状态:加锁后,将新的格式设置保存到设备结构体中。

    • 返回结果:将最终设置的格式返回给应用程序,确认实际应用的参数。

模块初始化

在 V4L2 框架中,初始化主要是为了建立起三者之间的逻辑联系:

  • v4l2_device: 抽象的父设备。管理驱动的全局资源(如锁、子设备列表)。

  • video_device: 暴露给用户空间的节点。即 /dev/videoX 的本体。

  • fw_device (自定义): 这种模式叫 Container 模式,通过包装内核结构体来存储自定义的状态信息(如当前的分辨率)。

static int __init v4l2_fw_init(void)
{
    int ret;

    printk(KERN_INFO "v4l2_framework: Initializing V4L2 framework module\n");

    /* Allocate device structure */
    fw_device = kzalloc(sizeof(*fw_device), GFP_KERNEL);
    if (!fw_device) {
        printk(KERN_ERR "v4l2_framework: Failed to allocate device structure\n");
        return -ENOMEM;
    }

    /* Initialize mutex for format protection */
    mutex_init(&fw_device->lock);

    /* Initialize V4L2 device */
    strscpy(fw_device->v4l2_dev.name, "v4l2_framework_v4l2",
            sizeof(fw_device->v4l2_dev.name));

    ret = v4l2_device_register(NULL, &fw_device->v4l2_dev);
    if (ret) {
        printk(KERN_ERR "v4l2_framework: Failed to register V4L2 device\n");
        goto error_v4l2_device;
    }

    /* Initialize video device */
    fw_device->vdev.v4l2_dev = &fw_device->v4l2_dev;
    fw_device->vdev.fops = &v4l2_fw_fops;
    fw_device->vdev.ioctl_ops = &v4l2_fw_ioctl_ops;
    fw_device->vdev.release = video_device_release_empty;
    strscpy(fw_device->vdev.name, "v4l2-framework-device",
            sizeof(fw_device->vdev.name));
    fw_device->vdev.device_caps = V4L2_CAP_VIDEO_OUTPUT | V4L2_CAP_VIDEO_CAPTURE |
                                V4L2_CAP_READWRITE | V4L2_CAP_STREAMING;

    /* Set default format */
    fw_device->current_format.type = V4L2_BUF_TYPE_VIDEO_OUTPUT;
    fw_device->current_format.fmt.pix.width = 640;
    fw_device->current_format.fmt.pix.height = 480;
    fw_device->current_format.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
    fw_device->current_format.fmt.pix.field = V4L2_FIELD_NONE;
    fw_device->current_format.fmt.pix.bytesperline = 640 * 2;  // YUYV is 2 bytes per pixel
    fw_device->current_format.fmt.pix.sizeimage = calculate_frame_size(
        fw_device->current_format.fmt.pix.width,
        fw_device->current_format.fmt.pix.height,
        fw_device->current_format.fmt.pix.pixelformat);
    fw_device->current_format.fmt.pix.colorspace = V4L2_COLORSPACE_SRGB;
    fw_device->current_format.fmt.pix.priv = 0;

    fw_device->width = 640;
    fw_device->height = 480;
    fw_device->format = V4L2_PIX_FMT_YUYV;

    /* 显式声明该设备节点支持双向数据流 */
    fw_device->vdev.vfl_dir = VFL_DIR_M2M;

    /* Register video device */
    video_set_drvdata(&fw_device->vdev, fw_device);
    ret = video_register_device(&fw_device->vdev, VFL_TYPE_VIDEO, video_nr);
    if (ret) {
        printk(KERN_ERR "v4l2_framework: Failed to register video device\n");
        goto error_video_register;
    }

    printk(KERN_INFO "v4l2_framework: Registered video device %s\n",
           video_device_node_name(&fw_device->vdev));

    return 0;

error_video_register:
    v4l2_device_unregister(&fw_device->v4l2_dev);
error_v4l2_device:
    kfree(fw_device);
    return ret;
}

第一步:内存分配与基础准备

  • 使用 kzalloc 分配并初始化 fw_device 结构体的内存。
  • mutex: 必须在注册前初始化。它用于保护 current_format 等共享资源,防止两个进程同时调用 S_FMT 导致竞争。

第二步:v4l2_device_register 注册 V4L2 设备(父设备)

  • 作用: 初始化父设备引用计数。
  • 参数 1 (NULL): 指向 struct device。对于真实的物理硬件(PCI/USB),这里应传入硬件的 dev。对于虚拟设备,传 NULL 后 V4L2 会创建一个默认的。

第三步:配置视频设备(子设备)

这是最关键的一步,决定了设备在 /dev 下的表现:

  • fops & ioctl_ops:

    • fops 处理文件系统层操作(open, read, ioctl)。

    • ioctl_ops 处理 V4L2 特有的指令。

  • device_caps: 告诉内核和用户空间“我能干什么”。

    • V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_VIDEO_OUTPUT: 声明支持双向。

    • V4L2_CAP_STREAMING: 声明支持支持 mmapuserptr 这种高效的零拷贝流。

    • V4L2_CAP_READWRITE: 声明支持 read/write 模式。

  • vfl_dir = VFL_DIR_M2M:

    • 核心功能: 设定方向性。M2M 表示 Memory-to-Memory,它允许内核 IOCTL 层同时处理 CAPTUREOUTPUT 两类请求。没有这一行,内核默认只允许 CAPTURE
  • type:默认选择输出流类型。

  • width/height:默认 640x480 分辨率。

  • pixelformat:默认 YUYV 格式。

  • bytesperline & sizeimage:非常重要。驱动必须根据分辨率算出每行占多少字节($640 \times 2 = 1280$)和整张图片占多少字节($640 \times 480 \times 2 = 614400$)。应用层会根据这些值来申请内存缓冲区。

  • colorspace:告知色彩空间(sRGB),防止图像颜色显示偏差。

第四步:video_register_device 注册视频设备节点

  • video_set_drvdata: 建立 video_devicefw_device 的快捷链接。这样在 IOCTL 回调中通过 video_drvdata(file) 就能找回我们的自定义结构体。

  • VFL_TYPE_VIDEO: 注册为视频流设备(生成 /dev/videoX)。其他可选值如 VFL_TYPE_VBI(电视广播数据)或 VFL_TYPE_RADIO

  • video_nr: 请求的设备号。-1 表示由内核自动分配第一个可用的数字。