Linux V4l2子系统分析4(基于Linux6.6)---设备注册及联系介绍
一、概述
1.1、V4L2 设备注册过程概述
V4L2 设备注册的过程通常包括以下几个主要步骤:
-
设备结构体初始化: 在 V4L2 中,所有设备通过
v4l2_device结构体进行管理。这个结构体定义了设备的基本信息和控制操作。每个视频设备都需要初始化一个v4l2_device结构体,并填充其中的字段。
-
struct v4l2_device { struct device dev; // 内核设备结构体 struct v4l2_device *parent; // 父设备(通常是设备总线) struct v4l2_ioctl_ops *ioctl_ops; // 用于设备控制的操作集 struct list_head videodev_list; // 设备列表 // 更多字段... }; -
创建
v4l2_device设备对象: 在设备驱动中,首先需要使用v4l2_device_register()函数来注册一个v4l2_device结构体,并将设备与硬件相关联。通过这个函数,V4L2 框架将设备暴露给内核的设备管理器。 -
int v4l2_device_register(struct device *dev, struct v4l2_device *v4l2_dev);该函数将设备注册到 V4L2 子系统中,使设备能够参与后续的视频流控制、事件处理等。
-
设备功能的实现: 对于每种类型的视频设备,V4L2 框架要求实现相应的功能接口。这些接口通常通过
v4l2_ioctl_ops结构体进行管理,它包含了设备支持的控制操作、查询操作等。 -
struct v4l2_ioctl_ops { int (*vidioc_querycap)(struct file *file, void *fh, struct v4l2_capability *cap); int (*vidioc_enum_fmt_vid_cap)(struct file *file, void *fh, struct v4l2_fmtdesc *f); // 更多 ioctl 操作函数指针... };每种设备(例如视频捕获设备、视频输出设备)会实现不同的操作集,以满足特定的功能需求。
-
注册
video_device设备: V4L2 框架中的每个视频设备(如摄像头、视频采集卡等)都需要通过video_device结构体进行注册。通过调用video_device_register()函数,将设备与内核的视频子系统绑定。 -
struct video_device { struct v4l2_device *v4l2_dev; // 设备的 V4L2 设备结构体 char name[32]; // 设备名称 struct v4l2_fmtdesc *format; // 支持的格式 // 更多字段... }; int video_device_register(struct video_device *vdev); -
设备初始化和控制接口: 一旦
v4l2_device和video_device被成功注册,设备就可以开始接收来自用户空间的控制命令(如通过 ioctl)。这时,应用程序可以通过 V4L2 提供的标准 API 来与设备进行交互。 -
事件注册和通知机制: V4L2 支持设备事件机制,允许设备向应用程序发送事件通知(如帧同步、错误报告等)。设备通过
v4l2_event结构体来管理事件的注册和通知。 -
struct v4l2_event { __u32 type; // 事件类型 __u32 id; // 事件的标识符 union { __u32 sequence; // 帧序列号等信息 // 更多字段... }; }; -
设备卸载与清理: 当设备不再需要时,需要通过
v4l2_device_unregister()和video_device_unregister()来注销设备,并清理相关的资源。
-
int v4l2_device_unregister(struct v4l2_device *v4l2_dev); int video_device_unregister(struct video_device *vdev);
1.2、V4L2 设备之间的联系
V4L2 框架中的设备关系是通过 v4l2_device 和 video_device 结构体来建立的。不同的设备之间的关系通常可以通过以下方式来理解:
-
父子设备关系: V4L2 设备可以通过
v4l2_device结构体的parent字段与父设备关联。例如,一个视频捕获设备(摄像头)可能与一个硬件加速的视频编码器(如 Rockchip 的硬件编码器)是父子关系,父设备提供硬件加速支持,子设备则负责捕获视频流。 -
视频流的上下游设备: 在多媒体处理链中,设备之间通常是上下游关系。例如,一个视频捕获设备将视频流提供给一个视频处理设备,再通过一个编码设备将视频流编码成特定格式,最后通过输出设备显示或存储。各个设备通过 V4L2 的控制和格式接口进行协调工作。
-
设备功能的扩展: V4L2 支持多种类型的设备,包括视频捕获设备、视频输出设备、编码解码设备等。每个设备可以根据需要实现不同的功能模块,通过
v4l2_device和video_device的注册接口,形成设备间的协作关系。具体的功能扩展,如设备间的数据流转、格式转换、硬件加速等,都是通过设备间的交互实现的。 -
设备之间的事件通信: V4L2 支持设备事件的注册和通知,多个设备可以通过事件机制进行通信。例如,一个视频捕获设备通过事件通知处理器设备,告知捕获成功,或者通知流结束,从而实现设备间的协调工作。
二、v4l2_device_register
v4l2的设备注册其实没有注册设备或者设备驱动,只是将v4l2的大结构体与其他设备进行捆绑。如下面是v4l2注册设备的过程。
drivers/media/v4l2-core/v4l2-device.c
int v4l2_device_register(struct device *dev, struct v4l2_device *v4l2_dev)
{
if (v4l2_dev == NULL)
return -EINVAL;
INIT_LIST_HEAD(&v4l2_dev->subdevs);
spin_lock_init(&v4l2_dev->lock);
v4l2_prio_init(&v4l2_dev->prio);
kref_init(&v4l2_dev->ref);
get_device(dev);
v4l2_dev->dev = dev;
if (dev == NULL) {
/* If dev == NULL, then name must be filled in by the caller */
if (WARN_ON(!v4l2_dev->name[0]))
return -EINVAL;
return 0;
}
/* Set name to driver name + device name if it is empty. */
if (!v4l2_dev->name[0])
snprintf(v4l2_dev->name, sizeof(v4l2_dev->name), "%s %s",
dev->driver->name, dev_name(dev));
if (!dev_get_drvdata(dev))
dev_set_drvdata(dev, v4l2_dev);
return 0;
}
EXPORT_SYMBOL_GPL(v4l2_device_register);
这里上面代码比较简单,这需要知道此函数功能是将当前设备和v4l2设备捆绑,并保存v4l2设备对象到当前设备的driverdata中。此函数的作用就是将当前设备驱动和v4l2对象进行捆绑,便于挂接子设备。
三、video_register_device注册过程
3.1、注册过程
include/media/v4l2-dev.h
static inline int __must_check video_register_device(struct video_device *vdev,
enum vfl_devnode_type type,
int nr)
{
return __video_register_device(vdev, type, nr, 1, vdev->fops->owner);
}
上面方法又封装了一层,转而调用下面的方法:
drivers/media/v4l2-core/v4l2-dev.c
int __video_register_device(struct video_device *vdev, int type, int nr,
int warn_if_nr_in_use, struct module *owner)
{
int i = 0;
int ret;
int minor_offset = 0;
int minor_cnt = VIDEO_NUM_DEVICES;
const char *name_base;
//这里省略若干查找空闲次设备号的过程--------------
/* Part 1: check device type */
switch (type) {
case VFL_TYPE_GRABBER:
name_base = "video";
break;
case VFL_TYPE_VBI:
name_base = "vbi";
break;
case VFL_TYPE_RADIO:
name_base = "radio";
break;
case VFL_TYPE_SUBDEV:
name_base = "v4l-subdev";
break;
default:
printk(KERN_ERR "%s called with unknown type: %d\n",
__func__, type);
return -EINVAL;
}
switch (type) {
case VFL_TYPE_GRABBER:
minor_offset = 0;
minor_cnt = 64;
break;
case VFL_TYPE_RADIO:
minor_offset = 64;
minor_cnt = 64;
break;
case VFL_TYPE_VBI:
minor_offset = 224;
minor_cnt = 32;
break;
default:
minor_offset = 128;
minor_cnt = 64;
break;
}
#ifdef CONFIG_VIDEO_FIXED_MINOR_RANGES//目前高通是没有打开这个宏
/* 1-on-1 mapping of device node number to minor number */
i = nr;//这里保存下空闲的次设备号
#else
/* The device node number and minor numbers are independent, so
we just find the first free minor number. */
/* 查找到第一个空闲的minor号,可以理解成是次设备号*/
for (i = 0; i < VIDEO_NUM_DEVICES; i++)
if (video_device[i] == NULL)//所有注册的video_device都会保存到这个数组中
break;
if (i == VIDEO_NUM_DEVICES) {
mutex_unlock(&videodev_lock);
printk(KERN_ERR "could not get a free minor\n");
return -ENFILE;
}
#endif
//minor_offset一般是0,i就是查找到的空闲次设备号,这里总的次设备支持到256个
vdev->minor = i + minor_offset;//这里记录下次设备号
//这里num会保存到session_id中,唯一表示一个任务。一般情况下nr == minor
vdev->num = nr;
devnode_set(vdev);//将标准位置为已用。
/* Should not happen since we thought this minor was free */
WARN_ON(video_device[vdev->minor] != NULL);
/* 下面这个方法字面意思看起来是获取一个index,但是查看源代码会发现
“这里会去查找不是直系的设备空闲号”,就是说只有video_device不为空,而且v4l2 parent
对象相同都会认为是同类,直接跳过相应的index号*/
vdev->index = get_index(vdev);
/* 下面要重点了,这里根据次设备号将当前video_device保存到video_device[]数组中*/
video_device[vdev->minor] = vdev;
mutex_unlock(&videodev_lock);
/* Part 3: Initialize the character device */
/* 分配字符设备文件*/
vdev->cdev = cdev_alloc();
//务必留意这个ioctl,后面子设备中的ioctl都是通过这里查找的。
vdev->cdev->ops = &v4l2_fops;
vdev->cdev->owner = owner;
ret = cdev_add(vdev->cdev, MKDEV(VIDEO_MAJOR, vdev->minor), 1);
/* Part 4: register the device with sysfs */
vdev->dev.class = &video_class;//根目录sys中有对应的属性可操作。
vdev->dev.devt = MKDEV(VIDEO_MAJOR, vdev->minor);//主设备号为81
vdev->dev.parent = vdev->dev_parent;//这里一般是v4l2_device对象
dev_set_name(&vdev->dev, "%s%d", name_base, vdev->num);
ret = device_register(&vdev->dev);//注册该video_device到kernel中。
vdev->dev.release = v4l2_device_release;
/* Increase v4l2_device refcount */
/* 下面这一步很关键,增加v4l2设备对象引用计数*/
if (vdev->v4l2_dev)
v4l2_device_get(vdev->v4l2_dev);
#if defined(CONFIG_MEDIA_CONTROLLER)
/* Part 5: Register the entity. */
if (vdev->v4l2_dev && vdev->v4l2_dev->mdev &&
vdev->vfl_type != VFL_TYPE_SUBDEV) {
vdev->entity.type = MEDIA_ENT_T_DEVNODE_V4L;
vdev->entity.name = vdev->name;
vdev->entity.info.v4l.major = VIDEO_MAJOR;
vdev->entity.info.v4l.minor = vdev->minor;
ret = media_device_register_entity(vdev->v4l2_dev->mdev,
&vdev->entity);
if (ret < 0)
printk(KERN_WARNING
"%s: media_device_register_entity failed\n",
__func__);
}
#endif
/* Part 6: Activate this minor. The char device can now be used. */
set_bit(V4L2_FL_REGISTERED, &vdev->flags);
由这里可以发现,创建video_device时,也创建了一个字符设备。并将该设备的parent节点指定为v4l2_device所依附的那个节点。主要需要注意下面几点。
- 1.根据设备类型确定设备名字和次设备数量
由于系统可能包含很多媒体设备,所以v4l2核心将0~255次设备编号划分了区域如下所示:其中VFL_TYPE_GRABBER一般表示提供数据的设备如camera。
| 类型 | 次设备号区间 | 设备基名称 |
|---|---|---|
| VFL_TYPE_GRABBER | 0~63 | “video” |
| VFL_TYPE_VBI | 224~255 | “vbi” |
| VFL_TYPE_RADIO | 64~127 | “radio” |
| 其它(含VFL_TYPE_SUBDEV) | 128~223 | 含“v4l-subdev” |
- 2.确定设备编号,注册字符设备驱动。这里我曾今迷糊了,我以为下面这会注册2个设备到kernel中,后来才发现我错了。这是注册字符设备的一种方式。
ret = cdev_add(vdev->cdev, MKDEV(VIDEO_MAJOR, vdev->minor), 1);
dev_set_name(&vdev->dev, "%s%d", name_base, vdev->num);
ret = device_register(&vdev->dev);
- 3.确定设备的入口
if (vdev->v4l2_dev && vdev->v4l2_dev->mdev &&
vdev->vfl_type != VFL_TYPE_SUBDEV) {
vdev->entity.type = MEDIA_ENT_T_DEVNODE_V4L;
vdev->entity.name = vdev->name;
vdev->entity.info.v4l.major = VIDEO_MAJOR;
vdev->entity.info.v4l.minor = vdev->minor;
ret = media_device_register_entity(vdev->v4l2_dev->mdev,&vdev->entity);
上面能发现,如果不是SUBDEV才能进来,注意上面name一般代表真实的设备名称,最后一行可以看到将入口注册到v4l2_device包含的media_device设备中了。而SUBDEV设备的注册一般都是设备驱动中赋值,如下是高通eeprom子设备。
e_ctrl->msm_sd.sd.entity.type = MEDIA_ENT_T_V4L2_SUBDEV;
e_ctrl->msm_sd.sd.entity.group_id = MSM_CAMERA_SUBDEV_EEPROM;
e_ctrl->msm_sd.sd.entity.name = video_device_node_name(vdev);
上面group_id在判断该设备类型时,起到确定作用。要注意。
3.2、video在系统中的位置
struct v4l2_device ------| struct video_device
| |------------引用----------> |----*v4l2_dev
| |
|-->ctrlhandler<------------挂载------------------|----ctrl_handler
|--->media->entry-list<------------挂载-----------|----->entry
四、subdev注册过程
4.1、v4l2_device_register_subdev()注册过程
这里只是将子设备挂接到v4l2_device的subdevs链表上,还没有真正的注册设备节点。真正注册设备节点的是v4l2_device_register_subdev_nodes方法。下面会介绍。
drivers/media/v4l2-core/v4l2-device.c
int v4l2_device_register_subdev(struct v4l2_device *v4l2_dev,
struct v4l2_subdev *sd)
{
#if defined(CONFIG_MEDIA_CONTROLLER)
//获取到子设备的入口对象,方面后面注册到media_device上面。
struct media_entity *entity = &sd->entity;
#endif
int err;
//此处省略一些错误检查代码
/*
* The reason to acquire the module here is to avoid unloading
* a module of sub-device which is registered to a media
* device. To make it possible to unload modules for media
* devices that also register sub-devices, do not
* try_module_get() such sub-device owners.
*/
sd->owner_v4l2_dev = v4l2_dev->dev && v4l2_dev->dev->driver &&
sd->owner == v4l2_dev->dev->driver->owner;
if (!sd->owner_v4l2_dev && !try_module_get(sd->owner))
return -ENODEV;
//下面v4l2_dev一般为系统根v4l2_device设备。
sd->v4l2_dev = v4l2_dev;
//这里对应具体的子设备,可以发现调用了registered()回调,如果有需要可以在对应的设备驱动中实现。
if (sd->internal_ops && sd->internal_ops->registered) {
err = sd->internal_ops->registered(sd);
if (err)
goto error_module;
}
//印证了之前说的,子设备的ctrl_handler都会挂载到根设备v4l2_device的ctrl_handler上面。
/* This just returns 0 if either of the two args is NULL */
err = v4l2_ctrl_add_handler(v4l2_dev->ctrl_handler, sd->ctrl_handler, NULL);
if (err)
goto error_unregister;
#if defined(CONFIG_MEDIA_CONTROLLER)
/* Register the entity. */
if (v4l2_dev->mdev) {
//上面将
err = media_device_register_entity(v4l2_dev->mdev, entity);
if (err < 0)
goto error_unregister;
}
#endif
spin_lock(&v4l2_dev->lock);
//将子设备链接到跟设备的,subdevs链表上。
list_add_tail(&sd->list, &v4l2_dev->subdevs);
spin_unlock(&v4l2_dev->lock);
return 0;
error_unregister:
if (sd->internal_ops && sd->internal_ops->unregistered)
sd->internal_ops->unregistered(sd);
error_module:
if (!sd->owner_v4l2_dev)
module_put(sd->owner);
sd->v4l2_dev = NULL;
return err;
}
EXPORT_SYMBOL_GPL(v4l2_device_register_subdev);
- 获取到子设备的entity对象,并将当前根设备对象赋值给subdev->v4l2_dev域。
- 将子设备的ctrl_handler对象,添加到根设备的
v4l2_dev->ctrl_handler域。 - 将子设备的entity注册到根设备上的media_device设备中。
4.2、v4l2_device_register_subdev_nodes()注册设备节点
drivers/media/v4l2-core/v4l2-device.c
int __v4l2_device_register_subdev_nodes(struct v4l2_device *v4l2_dev,
bool read_only)
{
struct video_device *vdev;
struct v4l2_subdev *sd;
int err;
/* Register a device node for every subdev marked with the
* V4L2_SUBDEV_FL_HAS_DEVNODE flag.
*/
list_for_each_entry(sd, &v4l2_dev->subdevs, list) {
if (!(sd->flags & V4L2_SUBDEV_FL_HAS_DEVNODE))
continue;
if (sd->devnode)
continue;
vdev = kzalloc(sizeof(*vdev), GFP_KERNEL);
if (!vdev) {
err = -ENOMEM;
goto clean_up;
}
video_set_drvdata(vdev, sd);
strscpy(vdev->name, sd->name, sizeof(vdev->name));
vdev->dev_parent = sd->dev;
vdev->v4l2_dev = v4l2_dev;
vdev->fops = &v4l2_subdev_fops;
vdev->release = v4l2_device_release_subdev_node;
vdev->ctrl_handler = sd->ctrl_handler;
if (read_only)
set_bit(V4L2_FL_SUBDEV_RO_DEVNODE, &vdev->flags);
sd->devnode = vdev;
err = __video_register_device(vdev, VFL_TYPE_SUBDEV, -1, 1,
sd->owner);
if (err < 0) {
sd->devnode = NULL;
kfree(vdev);
goto clean_up;
}
#if defined(CONFIG_MEDIA_CONTROLLER)
sd->entity.info.dev.major = VIDEO_MAJOR;
sd->entity.info.dev.minor = vdev->minor;
/* Interface is created by __video_register_device() */
if (vdev->v4l2_dev->mdev) {
struct media_link *link;
link = media_create_intf_link(&sd->entity,
&vdev->intf_devnode->intf,
MEDIA_LNK_FL_ENABLED |
MEDIA_LNK_FL_IMMUTABLE);
if (!link) {
err = -ENOMEM;
goto clean_up;
}
}
#endif
}
return 0;
clean_up:
list_for_each_entry(sd, &v4l2_dev->subdevs, list) {
if (!sd->devnode)
break;
video_unregister_device(sd->devnode);
}
return err;
}
EXPORT_SYMBOL_GPL(__v4l2_device_register_subdev_nodes);
static inline int __must_check
v4l2_device_register_subdev_nodes(struct v4l2_device *v4l2_dev)
{
#if defined(CONFIG_VIDEO_V4L2_SUBDEV_API)
return __v4l2_device_register_subdev_nodes(v4l2_dev, false);
#else
return 0;
#endif
}
上面这个方法才是真正注册设备节点的地方,不过一般请看下不这样用,一般稍微修改一下v4l2_device_register_subdev方法,或者直接重新写一个注册方法,在注册设备到v4l2_device根设备上的同时注册设备文件到系统中。
4.3、subdev在系统中的位置
struct v4l2_device
|
|-------->subdevs <-----> list <-----> list <-----> list
| | |
|---------------| |--------------|
|struct sub_dev | |struct sub_dev|
|---------------| |--------------|
子设备就这样一个一个挂在v4l2_device设备上了。
五、media_device_register()多媒体设备注册
其实这里又用宏转了一下,THIS_MODULE就是代表当前模块的struct module对象,也就代表当前驱动的实例对象。
#define media_device_register(mdev) __media_device_register(mdev, THIS_MODULE)
__media_device_register(mdev, THIS_MODULE)
drivers/media/mc/mc-device.c
int __must_check __media_device_register(struct media_device *mdev,
struct module *owner)
{
struct media_devnode *devnode;
int ret;
devnode = kzalloc(sizeof(*devnode), GFP_KERNEL);
if (!devnode)
return -ENOMEM;
/* Register the device node. */
mdev->devnode = devnode;
devnode->fops = &media_device_fops;
devnode->parent = mdev->dev;
devnode->release = media_device_release;
/* Set version 0 to indicate user-space that the graph is static */
mdev->topology_version = 0;
ret = media_devnode_register(mdev, devnode, owner);
if (ret < 0) {
/* devnode free is handled in media_devnode_*() */
mdev->devnode = NULL;
return ret;
}
ret = device_create_file(&devnode->dev, &dev_attr_model);
if (ret < 0) {
/* devnode free is handled in media_devnode_*() */
mdev->devnode = NULL;
media_devnode_unregister_prepare(devnode);
media_devnode_unregister(devnode);
return ret;
}
dev_dbg(mdev->dev, "Media device registered\n");
return 0;
}
EXPORT_SYMBOL_GPL(__media_device_register);
上面初始化了一些media_devnode的设备域。便于后续标准接口的调用。紧接着重量级的操作来了,注释部分写的也很精彩。
drivers/media/mc/mc-devnode.c
int __must_check media_devnode_register(struct media_device *mdev,
struct media_devnode *devnode,
struct module *owner)
{
int minor;
int ret;
/* Part 1: Find a free minor number */
mutex_lock(&media_devnode_lock);
minor = find_first_zero_bit(media_devnode_nums, MEDIA_NUM_DEVICES);
if (minor == MEDIA_NUM_DEVICES) {
mutex_unlock(&media_devnode_lock);
pr_err("could not get a free minor\n");
kfree(devnode);
return -ENFILE;
}
set_bit(minor, media_devnode_nums);
mutex_unlock(&media_devnode_lock);
devnode->minor = minor;
devnode->media_dev = mdev;
/* Part 1: Initialize dev now to use dev.kobj for cdev.kobj.parent */
devnode->dev.bus = &media_bus_type;
devnode->dev.devt = MKDEV(MAJOR(media_dev_t), devnode->minor);
devnode->dev.release = media_devnode_release;
if (devnode->parent)
devnode->dev.parent = devnode->parent;
dev_set_name(&devnode->dev, "media%d", devnode->minor);
device_initialize(&devnode->dev);
/* Part 2: Initialize the character device */
cdev_init(&devnode->cdev, &media_devnode_fops);
devnode->cdev.owner = owner;
kobject_set_name(&devnode->cdev.kobj, "media%d", devnode->minor);
/* Part 3: Add the media and char device */
ret = cdev_device_add(&devnode->cdev, &devnode->dev);
if (ret < 0) {
pr_err("%s: cdev_device_add failed\n", __func__);
goto cdev_add_error;
}
/* Part 4: Activate this minor. The char device can now be used. */
set_bit(MEDIA_FLAG_REGISTERED, &devnode->flags);
return 0;
cdev_add_error:
mutex_lock(&media_devnode_lock);
clear_bit(devnode->minor, media_devnode_nums);
devnode->media_dev = NULL;
mutex_unlock(&media_devnode_lock);
put_device(&devnode->dev);
return ret;
}
上面代码注册了一个字符设备驱动,上面代码注释可以发现函数分成4部分功能.
- part1:这一部分功能就是找一个空闲的次设备号。
1.MEDIA_NUM_DEVICES:该宏表示系统最大支持256个子设备.
2.media_devnode_nums:该全局变量用来表示所有位号是否已经使用了,这里可以理解成这个变量有256位,每一位都是一个标志。例如:我们得到一个次设备号是5,则该变量的第5位就会被置位。 - part2:这里会根据主设备号media_dev_t和次设备号,创建字符设备。并将该字符设备的owner赋值为当前模块,然后添加到字符设备集合中。
- part3:注册设备
- part4:置标志变量,表示该字符设备可以使用了。
六、举例应用
上面media_device在v4l2中只用于遍历子设备,就没有描述出media_device在系统中的位置,等后面需要时在分析吧。总结到目前了解到v4l2需要一个根节点v4l2_device来管理子设备,然后上层用户空间,根据根节点,来查找所有子设备。
1. 示例背景
假设我们有一个虚拟视频设备(例如一个虚拟摄像头),我们需要通过 V4L2 在 Linux 内核中注册该设备,以便用户空间的应用程序(如 ffmpeg、mplayer 等)可以通过 /dev/video0 访问它。
2. V4L2 设备注册代码示例
以下是一个简单的示例代码,展示了如何创建和注册一个虚拟的视频设备。
a) 包含必要的头文件
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/videodev2.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/device.h>
#include <linux/errno.h>
#include <linux/platform_device.h>
#include <linux/videodev2.h>
b) 定义设备操作函数
在注册设备之前,我们需要实现与设备交互的操作函数。这些操作函数包括打开设备、关闭设备、处理 IO 控制命令等。
static int my_video_open(struct file *file)
{
printk(KERN_INFO "My video device opened\n");
return 0;
}
static int my_video_release(struct file *file)
{
printk(KERN_INFO "My video device released\n");
return 0;
}
static long my_video_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
switch (cmd) {
case VIDIOC_QUERYCAP:
{
struct v4l2_capability cap;
memset(&cap, 0, sizeof(cap));
strlcpy(cap.driver, "my_v4l2_device", sizeof(cap.driver));
strlcpy(cap.card, "My Virtual Video Device", sizeof(cap.card));
cap.capabilities = V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_STREAMING;
return copy_to_user((void __user *)arg, &cap, sizeof(cap)) ? -EFAULT : 0;
}
default:
return -EINVAL;
}
}
c) 定义设备操作结构体 v4l2_file_operations
static const struct v4l2_file_operations my_video_fops = {
.owner = THIS_MODULE,
.open = my_video_open,
.release = my_video_release,
.unlocked_ioctl = my_video_ioctl,
};
d) 设备结构体定义与初始化
设备的核心结构体是 video_device,它描述了设备的基本信息,如设备名称、操作函数等。
static struct video_device *vdev;
static int __init my_video_device_init(void)
{
int ret;
// 分配视频设备结构体
vdev = video_device_alloc();
if (!vdev) {
printk(KERN_ERR "Failed to allocate video device\n");
return -ENOMEM;
}
// 初始化视频设备
snprintf(vdev->name, sizeof(vdev->name), "My Virtual Video Device");
vdev->fops = &my_video_fops;
vdev->release = video_device_release;
// 注册设备
ret = video_register_device(vdev, VFL_TYPE_GRABBER, -1);
if (ret < 0) {
printk(KERN_ERR "Failed to register video device\n");
video_device_release(vdev);
return ret;
}
printk(KERN_INFO "Video device registered as %s\n", vdev->devnode);
return 0;
}
e) 清理函数(卸载设备)
当设备驱动卸载时,我们需要注销视频设备并释放相关资源。
static void __exit my_video_device_exit(void)
{
video_unregister_device(vdev);
video_device_release(vdev);
printk(KERN_INFO "Video device unregistered\n");
}
f) 模块入口和出口
最后,我们需要定义模块的入口和出口函数。
module_init(my_video_device_init);
module_exit(my_video_device_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple V4L2 video device driver example");
3. 代码解析
video_device_alloc():分配一个video_device结构体,该结构体包含了设备的基本信息和操作函数。vdev->name:设置视频设备的名称。vdev->fops:设置设备的操作函数,包括打开、释放和 IO 控制等操作。video_register_device():将设备注册到 V4L2 子系统,并在/dev目录下创建设备节点(如/dev/video0)。video_unregister_device():注销设备并释放相关资源。video_device_release():释放video_device结构体。
4. 编译与加载
将上述代码保存为 my_v4l2_device.c 文件,编写 Makefile 来编译模块:
a) 编写 Makefile
obj-m += my_v4l2_device.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
b) 编译和加载模块
make
sudo insmod my_v4l2_device.ko
c) 查看设备
设备成功注册后,可以在 /dev 目录下看到设备节点 /dev/video0。使用 v4l2-ctl 等工具进行测试:
ls /dev/video*
v4l2-ctl --list-devices
5. 测试
此时,用户空间的应用程序(如 ffmpeg、mplayer)可以通过 V4L2 接口访问该虚拟设备进行视频捕捉、播放等操作。
6037

被折叠的 条评论
为什么被折叠?



