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) - 多种像素格式支持
- 设备能力标志设置
学习要点
- 理解
v4l2_device和video_device的关系 - 理解
v4l2_ioctl_ops的结构和用途 - 理解格式协商的 try/set 模式
- 理解设备能力标志的含义
- 理解 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 ─────────────────► │ 确认当前格式问题与思考
- kzalloc是什么?和kmalloc有什么区别?什么时候用哪个? kzalloc = kmalloc + memset(…, 0, …), 用于分配并初始化为0的内存块。kmalloc分配的内存未初始化,可能包含垃圾值。通常在需要清零内存时使用kzalloc,否则使用kmalloc以节省初始化开销。
- v4l2_device和video_device的区别和联系是什么?
- 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_out和vidioc_enum_fmt_vid_cap: 枚举输出和输入视频格式的 IOCTL 操作。驱动程序通过实现v4l2_fw_enum_fmt函数来列出设备支持的像素格式。vidioc_g_fmt_vid_out和vidioc_g_fmt_vid_cap: 获取当前输出和输入视频格式的 IOCTL 操作。驱动程序通过实现v4l2_fw_g_fmt函数来返回当前设置的视频格式。vidioc_s_fmt_vid_out和vidioc_s_fmt_vid_cap: 设置输出和输入视频格式的 IOCTL 操作。驱动程序通过实现v4l2_fw_s_fmt函数来更新设备的视频格式设置。vidioc_try_fmt_vid_out和vidioc_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_devicevoid *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 调用。
- 第一步:用户空间 (User Space) 发起。程序声明一个变量,并调用 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: 声明支持支持mmap或userptr这种高效的零拷贝流。V4L2_CAP_READWRITE: 声明支持 read/write 模式。
vfl_dir = VFL_DIR_M2M:- 核心功能: 设定方向性。M2M 表示 Memory-to-Memory,它允许内核 IOCTL 层同时处理
CAPTURE和OUTPUT两类请求。没有这一行,内核默认只允许CAPTURE。
- 核心功能: 设定方向性。M2M 表示 Memory-to-Memory,它允许内核 IOCTL 层同时处理
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_device到fw_device的快捷链接。这样在 IOCTL 回调中通过video_drvdata(file)就能找回我们的自定义结构体。VFL_TYPE_VIDEO: 注册为视频流设备(生成/dev/videoX)。其他可选值如VFL_TYPE_VBI(电视广播数据)或VFL_TYPE_RADIO。video_nr: 请求的设备号。-1表示由内核自动分配第一个可用的数字。