2. V4L2 核心数据结构:struct video_device、struct v4l2_device、struct v4l2_subdev
V4L2(Video for Linux 2)框架的核心在于三个关键数据结构:struct video_device、struct v4l2_device 和 struct v4l2_subdev。理解它们各自的职责、成员以及三者之间的协作关系,是掌握 V4L2 驱动开发的基础。本章将深入剖析这三个结构体,并通过初始化流程图和关系图帮助读者建立清晰的认知。
2.1 数据结构概览与职责
在 V4L2 框架中,这三个结构体分别代表了不同层次的硬件与软件抽象:
| 数据结构 | 所属头文件 | 核心职责 | 典型代表 |
|---|---|---|---|
struct v4l2_device |
include/media/v4l2-device.h |
顶层设备实例,管理整个视频硬件(如一个 USB 摄像头或 SoC ISP 模块) | 一个摄像头传感器 + 控制器 + 视频接口的整体 |
struct v4l2_subdev |
include/media/v4l2-subdev.h |
子设备抽象,代表一个独立的功能单元(如传感器、解码器、调焦马达) | OV5640 传感器、ADV7180 视频解码器 |
struct video_device |
include/media/v4l2-dev.h |
用户空间接口,通过 /dev/videoX 设备节点与应用程序交互 |
/dev/video0、/dev/video1 |
设计思想: 这种分层设计遵循了 Linux 内核“高内聚、低耦合”的原则。
v4l2_device 作为容器,v4l2_subdev 负责硬件控制逻辑,而 video_device 专注于字符设备操作。驱动开发者可以分别实现各层,最后通过注册机制将它们绑定在一起。
2.2 struct v4l2_device:顶层设备容器
struct v4l2_device 是整个 V4L2 设备的根节点。它通常嵌入在一个更大的设备结构体中(如 struct usb_device 或 struct platform_device),并负责管理所有关联的子设备。
关键成员:
struct list_head subdevs:链表头,用于挂载所有注册到该设备的v4l2_subdev实例。spinlock_t lock:自旋锁,保护子设备链表操作。char name[V4L2_DEVICE_NAME_SIZE]:设备名称,用于调试和日志。struct device *dev:指向底层 Linux 通用设备结构体,用于电源管理和 sysfs。void (*notify)(struct v4l2_subdev *sd, unsigned int notification, void *arg):通知回调,用于子设备向父设备发送事件(如传感器状态变化)。
初始化流程:
#include <media/v4l2-device.h>
struct my_camera_dev {
struct v4l2_device v4l2_dev;
/* 其他驱动私有成员 */
};
static int my_probe(struct platform_device *pdev) {
struct my_camera_dev *camera;
int ret;
camera = devm_kzalloc(&pdev->dev, sizeof(*camera), GFP_KERNEL);
if (!camera)
return -ENOMEM;
/* 1. 初始化 v4l2_device */
ret = v4l2_device_register(&pdev->dev, &camera->v4l2_dev);
if (ret < 0) {
dev_err(&pdev->dev, "Failed to register v4l2_device\n");
return ret;
}
/* 2. 后续注册子设备、video_device 等 */
return 0;
}
static int my_remove(struct platform_device *pdev) {
struct my_camera_dev *camera = platform_get_drvdata(pdev);
/* 注销顺序与注册相反 */
v4l2_device_unregister(&camera->v4l2_dev);
return 0;
}
2.3 struct v4l2_subdev:子设备抽象
struct v4l2_subdev 代表一个独立的硬件功能块。它封装了该子设备的操作函数集(通过 v4l2_subdev_ops)和内部状态。
关键成员:
struct v4l2_device *v4l2_dev:指向所属的父v4l2_device。struct module *owner:所属内核模块,防止模块被意外卸载。const struct v4l2_subdev_ops *ops:操作函数集,包括视频、音频、控制、传感器等回调。struct v4l2_subdev_internal_ops *internal_ops:内部操作,如注册/注销通知。struct device *dev:指向该子设备对应的物理设备结构体。struct list_head list:用于挂入v4l2_device.subdevs链表。char name[V4L2_SUBDEV_NAME_SIZE]:子设备名称。unsigned int flags:标志位,如V4L2_SUBDEV_FL_HAS_DEVNODE表示该子设备拥有独立的设备节点。
初始化与注册示例:
#include <media/v4l2-subdev.h>
static const struct v4l2_subdev_ops my_sensor_ops = {
.core = &my_sensor_core_ops,
.video = &my_sensor_video_ops,
.pad = &my_sensor_pad_ops,
};
static int my_sensor_probe(struct i2c_client *client) {
struct my_sensor *sensor;
int ret;
sensor = devm_kzalloc(&client->dev, sizeof(*sensor), GFP_KERNEL);
if (!sensor)
return -ENOMEM;
/* 1. 初始化 v4l2_subdev */
v4l2_subdev_init(&sensor->sd, &my_sensor_ops);
sensor->sd.owner = THIS_MODULE;
sensor->sd.dev = &client->dev;
snprintf(sensor->sd.name, sizeof(sensor->sd.name), "%s %d-%04x",
client->name, i2c_adapter_id(client->adapter), client->addr);
/* 2. 注册子设备到父 v4l2_device */
ret = v4l2_device_register_subdev(&camera->v4l2_dev, &sensor->sd);
if (ret < 0) {
dev_err(&client->dev, "Failed to register subdev\n");
return ret;
}
return 0;
}
2.4 struct video_device:用户空间接口
struct video_device 是用户空间通过 open()、ioctl() 等系统调用访问 V4L2 设备的入口。它负责将用户请求路由到正确的 v4l2_device 或 v4l2_subdev。
关键成员:
const struct v4l2_file_operations *fops:字符设备文件操作函数集(open、release、ioctl、mmap等)。struct video_device *queue:指向视频缓冲队列(videobuf2相关)。struct v4l2_device *v4l2_dev:指向所属的v4l2_device。struct device *dev:指向创建的字符设备结构体。int minor:次设备号。unsigned long flags:标志位,如V4L2_FL_REGISTERED。char name[32]:设备名称,显示在/sys/class/video4linux/中。
初始化与注册示例:
#include <media/v4l2-dev.h>
static const struct v4l2_file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.release = my_release,
.unlocked_ioctl = my_ioctl,
.mmap = my_mmap,
};
static int my_video_probe(struct platform_device *pdev) {
struct my_camera_dev *camera;
struct video_device *vdev;
int ret;
/* 假设 camera 和 v4l2_device 已初始化 */
/* 1. 分配 video_device */
vdev = video_device_alloc();
if (!vdev)
return -ENOMEM;
/* 2. 设置核心字段 */
vdev->v4l2_dev = &camera->v4l2_dev;
vdev->fops = &my_fops;
vdev->release = video_device_release;
vdev->device_caps = V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_STREAMING;
strscpy(vdev->name, "My Camera", sizeof(vdev->name));
/* 3. 注册 video_device */
ret = video_register_device(vdev, VFL_TYPE_VIDEO, -1);
if (ret < 0) {
video_device_release(vdev);
dev_err(&pdev->dev, "Failed to register video device\n");
return ret;
}
camera->vdev = vdev;
return 0;
}
2.5 三者关系与初始化流程
下图清晰地展示了 v4l2_device、v4l2_subdev 和 video_device 之间的层次关系与初始化顺序。
标准初始化顺序:
- 注册
v4l2_device:调用v4l2_device_register(),传入父设备指针和已分配的v4l2_device实例。此函数初始化内部锁和链表。 - 注册
v4l2_subdev:为每个硬件子模块调用v4l2_device_register_subdev(),将子设备挂入v4l2_device.subdevs链表,并建立双向引用。 - 注册
video_device:调用video_register_device(),指定设备类型(如VFL_TYPE_VIDEO)和次设备号。内核会创建/dev/videoX节点并关联v4l2_device。
注意事项:
- 注销顺序必须与注册顺序相反:先
video_unregister_device(),再v4l2_device_unregister_subdev(),最后v4l2_device_unregister()。 video_device的release回调必须正确设置,否则会导致内存泄漏。- 子设备可以通过
v4l2_subdev_call()宏调用其他子设备的操作,但必须确保父设备已注册。
2.6 典型拓扑结构
在实际的嵌入式摄像头系统中,这三个结构体通常形成以下拓扑:
在这个拓扑中:
- 一个
v4l2_device代表整个摄像头模组。 - 多个
v4l2_subdev分别控制传感器、ISP 和闪光灯,它们通过 I2C 或 SPI 与主控通信。 - 一个
video_device提供视频数据流接口,应用程序通过它获取图像帧。
进阶提示: 在复杂 SoC 中,一个
v4l2_device 可能注册多个 video_device(如主摄像头和深度摄像头),每个 video_device 可以共享或独占某些 v4l2_subdev。这种灵活性使得 V4L2 能够支持从简单 USB 摄像头到多传感器机器视觉系统的各种场景。