【V4L2】V4L2子设备

系列文章目录

【V4L2】V4L2框架简述
【V4L2】V4L2框架之驱动结构体
【V4L2】V4L2子设备



v4l2子设备用户空间API

可以在 /dev 文件夹下创建 v4l-subdevX 设备节点以供用户直接操作子设备硬件。如果需要在用户空间创建设备节点的话,就需要在子设备节点注册之前设置 V4L2_SUBDEV_FL_HAS_DEVNODE 标志,然后调用 v4l2_device_register_subdev_nodes() 函数,就可以在用户空间创建设备节点,设备节点会在子设备卸载的时候自动地被销毁。

    VIDIOC_QUERYCTRL
    VIDIOC_QUERYMENU
    VIDIOC_G_CTRL
    VIDIOC_S_CTRL
    VIDIOC_G_EXT_CTRLS
    VIDIOC_S_EXT_CTRLS
    VIDIOC_TRY_EXT_CTRLS

上述 ioctls 可以通过设备节点访问,也可以直接在子设备驱动里面调用。

   VIDIOC_DQEVENT
   VIDIOC_SUBSCRIBE_EVENT
   VIDIOC_UNSUBSCRIBE_EVENT

要使用上述事件,就必须设置 v4l2_subdevV4L2_SUBDEV_USES_EVENTS 标志位,实现 core_opssubscribe 相关的回调函数,回调函数里面需要初始化 events,然后注册 v4l2_subdev。一些私有的 ioctls 可以在 v4l2_subdevops->core->ioctl 里面实现。

I2C子设备驱动

要想在 I2C 驱动里面添加 v4l2_subdev 支持,就需要把 v4l2_subdev 结构体嵌入到每个 I2C 实例结构体里面,有一些比较简单的 I2C 设备不需要自定义的状态结构体,此时只需要创建一个单独的 v4l2_subdev 结构体即可。一个典型的驱动自定义状态结构体如下所示:

    struct chipname_state {
    
    
        struct v4l2_subdev sd;
        ...  /* additional state fields */
    };

使用 v4l2_i2c_subdev_init 去初始化一个 I2C 子设备,该函数会填充 v4l2_subdev 的所有成员并确保 v4l2_subdevi2c_client 互相指向对方。也可以添加内联函数来从 v4l2_subdev 的指针获取到 i2c_client 结构体:

struct i2c_client *client = v4l2_get_subdevdata(sd);
也可以从i2c_client结构体指针获取到v4l2_subdev结构体:
struct v4l2_subdev *sd = i2c_get_clientdata(client);
桥驱动可以使用以下帮助函数来创建一个I2C子设备:
struct v4l2_subdev *sd = v4l2_i2c_new_subdev
    (v4l2_dev, adapter,"module_foo", "chipid", 0x36, NULL);

该函数会加载给定的模块(可以为空)并且调用 i2c_new_device 根据传入的参数创建子设备结构体,最后注册 v4l2_subdev


video_device 结构体

video_device 可以动态的分配:

struct video_device *vdev = video_device_alloc();
if (vdev == NULL)
    return -ENOMEM;
vdev->release = video_device_release;

如果需要将 video_device 结构体嵌入到更大的结构体里面的话,就需要设置 vdev 的 release 成员。内核提供了两个默认的 release 回调函数,如下:

video_device_release()       // 仅仅调用kfree释放分配的内存,用于动态分配情况下
video_device_release_empty() // 不做任何事情,静态变量

以下的函数成员必须被设置:

  • v4l2_dev:必须指向v4l2_device父设备

  • vfl_dir:VFL_DIR_RX(capture设备)、VFL_DIR_TX(输出设备)、VFL_DIR_M2M(codec设备)

  • fops:设置v4l2_file_operations结构体

  • ioctl_ops:ioctls,可以通过设备节点被用户空间程序访问,需设置fops的.unlocked_ioctl指向video_ioctl2

  • lock:如果想要在驱动空间里做锁操作,可以设置为NULL。否则需要指向一个已经初始化的mutex_lock结构体

  • queue:指向一个vb2_queue结构体,如果queue->lock不为空,那么与队列相关的ioctls就会使用queue内部的锁,这样的话就不用等待其它类型的ioctls操作

  • prio:对优先级进行跟踪,用在VIDIOC_G/S_PRIORITY上,如果为空的话就会使用v4l2_device里面的v4l2_prio_state

  • dev_parent:指向v4l2_device即可

如果想忽略 ioctl_ops 中某个 ioctls 的话可以调用下面的函数:

   void v4l2_disable_ioctl(struct video_device *vdev, unsigned int cmd);

如果要集成到 media_framework 里面,就需要设置 video_device 里面的 media_entity 成员,同时需要提供 media_pad

struct media_pad *pad = &my_vdev->pad;
int err;
err = media_entity_init(&vdev->entity, 1, pad, 0);
  • video_device 的注册
    video_device 的注册函数如下:
  err = video_register_device(vdev, VFL_TYPE_GRABBER, -1);

该段代码会注册一个字符设备驱动程序并在用户空间生成一个设备节点。如果 v4l2_device 父设备的 mdev 成员不为空的话,video_device 的 entity 会被自动的注册到 media framework 里面。函数最后一个参数是设备节点索引号,如果是 -1 的话就取用第一个内核中可用的索引号值。注册的设备类型以及用户空间中的节点名称取决于以下标识:

VFL_TYPE_GRABBER: videoX 输入输出设备
VFL_TYPE_VBI: vbiX
VFL_TYPE_RADIO: radioX 硬件定义的音频调谐设备
VFL_TYPE_SDR: swradioX 软件定义的音频调谐设备

当一个设备节点被创建时,相关属性也会被创建,可以在 /sys/class/video4linux 里面看到这些设备文件夹,在文件夹里面可以看到 ‘name’,‘dev_debug’,‘index’,'uevent’等属性,可以使用 cat 命令查看。’dev_debug’ 可以用于 video 设备调试,每个 video 设备都会创建一个 ‘dev_debug’ 属性,该属性以文件夹的形式存在与 /sys/class/video4linux/<devX>/ 下面以供使能 log file operation。’dev_debug’是一个位掩码,以下位可以被设置:

  0x01:记录ioctl名字与错误码。设置0x08位可以只记录VIDIOC_(D)QBUF
  0x02:记录ioctl的参数与错误码。设置0x08位可以只记录VIDIOC_(D)QBUF
  0x04:记录file ops操作。设置0x08位可以只记录read&write成员的操作
  0x08:如上所示
  0x10:记录poll操作

当以上的位被设置的时候,发生相关的调用或者操作的时候内核就会打印出来相关的调用信息到终端上面。类似于

[173881.402120] video4: VIDIOC_DQEVENT: error -2
[173884.906633] video4: VIDIOC_UNSUBSCRIBE_EVENT

  • video设备的清理
    当 video 设备节点需要被移除或者USB设备断开时,需要执行以下函数:
  video_unregister_device(vdev);

来进行设备的卸载,该函数会移除 /dev 下的设备节点文件,同时不要忘记调用 media_entity_cleanup 来清理 entity。

ioctls 与 locking

V4L 核心层提供了可选的锁服务,最主要的就是 video_device 里面的锁,用来进行 ioctls 的同步。如果使用了 videobuf2 框架,那么 video_device->queue->lock 锁也会被用来做 queue 相关的 ioctls 同步。使用不同的锁有很多优点,比如一些设置相关的 ioctls 花费的时间比较长,如果使用独立的锁,VIDIOC_DQBUF就不用等待设置操作的完成就可以执行,这个在网络摄像机驱动中很常见。当然,也可以完全由驱动本身去完成锁操作,这时可以设置所有的锁成员为NULL并实现一个驱动自己的锁。

如果使用旧的 videobuf,需要将 video_device 的锁传递给 videobuf queue 初始化函数,如果 videobuf 正在等待一帧数据的到达,此时会将锁暂时释放,等数据到达之后再次加锁,否则别的处理程序就无法访问。所以不推荐使用旧的 videobuf。如果是在 videobuf2 框架下,需要实现 wait_prepare 与 wait_finish 回调函数去释放或者获取锁,如果使用了 queue->lock,可以使用 V4L2 提供的回调 vb2_ops_wait_prepare/finish 帮助函数来完成加锁与解锁的操作,它们会使用 queue->lock这个锁(此时一定要将该锁初始化)。

v4l2_fh 结构体

该结构体提供了一种简单的保存文件句柄特定数据的方法。v4l2_fh 的使用者-v4l2 framework 可以通过检查 video_device->flags 的 V4L2_FL_USES_V4L2_FH 位来知道驱动是否使用 v4l2_fh 作为 file->private_data 指针,该标志位通过调用函数 v4l2_fh_init 来设置。

v4l2_fh 结构体作为驱动自己的文件句柄存在,并且在驱动的 open 函数里面设置 file->private_data 指向它,v4l2_fh 有多个的时候会作为一个链表存在于 file->private_data 中,可以遍历访问。在大多数情况下 v4l2_fh 结构体都被嵌入到更大的结构体里面,此时需要在 open 函数里面调用
v4l2_fh_init+v4l2_fh_add 进行添加,在 release 函数里面调用 v4l2_fh_del+v4l2_fh_exit 进行退出。驱动可以使用 container_of 来访问自己的文件句柄结构体,如下所示:

struct my_fh {
    
    
    int blah;
    struct v4l2_fh fh;
};

int my_open(struct file *file)
{
    
    
    struct my_fh *my_fh;
    struct video_device *vfd;
    int ret;

    my_fh = kzalloc(sizeof(*my_fh), GFP_KERNEL);

    v4l2_fh_init(&my_fh->fh, vfd);

    file->private_data = &my_fh->fh;
    v4l2_fh_add(&my_fh->fh);
    return 0;
}

int my_release(struct file *file)
{
    
    
    struct v4l2_fh *fh = file->private_data;
    struct my_fh *my_fh = container_of(fh, struct my_fh, fh);

    v4l2_fh_del(&my_fh->fh);
    v4l2_fh_exit(&my_fh->fh);
    kfree(my_fh);
    return 0;
}

如以上代码所示,由于 open 函数可能会被多个应用 app 所调用,所以 fh 也会有多个,但是 file->private 永远指向最新的一个 v4l2_fh ,通过这个 v4l2_fh 可以找到整个 v4l2_fh 链表中的所有元素。一些驱动需要在第一个文件句柄打开后以及最后一个文件句柄关闭前的时候做一些其它的工作,下面两个帮助函数可以检查 v4l2_fh 结构体是否只剩下一个 entry:

int v4l2_fh_is_singular(struct v4l2_fh *fh)
如果是只有一个entry,返回1,否则返回0,如果fh为空也返回0int v4l2_fh_is_singular_file(struct file *filp)
和上面差不多,但是使用 filp->private_data 这一数据源,实际上它是指向最新的一个v4l2_fh的。

V4L2 events

V4L2 events 提供一种通用的方法来传递 events 到用户空间,驱动程序必须使用 v4l2_fh(设置 video_device 的 flags 位)才能够实现对 V4L2 events 的支持。events 用类型和 ID 作为区分标识,没有使用到的 events 的ID就是0。

当用户订阅 event 时,用户空间会相应地为每个 event 分配一个 kevent 结构体(如果 elems 参数为0的话只有一个,不为0就按照指定的数量分配),所以每个 event 都有一个或多个属于自己的 kevent 结构体,这就保证了如果驱动短时间内生成了非常多的 events 也不会覆盖到其它的同类型 events,可以看作是分了好几个篮子来放不同类型的水果。event 结构体是 v4l2_subscribed_event 结构体的最后一个成员,以数组的形式存在,并且是一个柔性数组(struct v4l2_kevent events[]),也就是说在分配 v4l2_subscribed_event 结构体空间的时候,events 并不占用空间,需要额外为指定数量的 events 分配空间,kzalloc(siezof(struct v4l2_subscribed_event) + sizeof(struct v4l2_kevent) * num, GFP_KERNEL);在使用的时候,完全可以按照数组的方式去对 kevent 进行寻址,很方便。

如果获得的 event 数量比 kevent 的还要多,那么旧的 events 就会被丢弃。可以设置结构体 v4l2_subscribed_event 的 merge、replace 回调函数(其实默认的函数就足够用了),它们会在 event 被捕获并且没有更多的空间来存放 event 时被调用。在 v4l2_event.c 里面有一个很好的关于 replace/merge 的例子,ctrls_replace()与ctrls_merge() 被作为回调函数使用。由于这两个函数可以在中断上下文被调用,因此必须得快速执行完毕并返回。

关于events的循环是一个比较有意思的操作,入队时:三个变量(first-下一个准备被dequeue的eventm,
elems-总kevent数量,in_use-已经使用的kevent数量)

若elems == in_use,说明队列成员已经用完。

取出第一个kevent,从available队列中删掉,first指向数组的下一个成员,in_use —。

找到上一步中(first指向数组的下一个成员),将上一步(取出第一个kevent)的changes位进行合并赋值给前者。
因为后者比前者更新,所以数值完全可以覆盖前者,同时又保留了前者的变化。

取出第in_use + first >= elems ? in_use + first - elems : in_use +
first;个数组kevent项作为新的填充项。

in_use ++

一些有用的函数:

int v4l2_event_subscribe(struct v4l2_fh *fh, struct v4l2_event_subscription *sub, 
        unsigned elems, const struct v4l2_subscribed_event_ops *ops)

当用户空间通过 ioctl 发起订阅请求之后,video_device->ioctl_ops->vidioc_subscribe_event
需要检查是否支持请求的 event,如果支持的话就调用上面的函数进行订阅。一般可以将 video 的相关 ioctl 指向内核默认的 v4l2_ctrl_subscribe_event() 函数。

int v4l2_event_unsubscribe(struct v4l2_fh *fh, struct v4l2_event_subscription *sub)
取消一个事件的订阅,V4L2_EVENT_ALL类型可以用于取消所有事件的订阅。一般可以将video的相关ioctl指向该函数。

void v4l2_event_queue(struct video_device *vdev, const struct v4l2_event *ev)
该函数用作events入队操作(由驱动完成),驱动只需要设置type以及data成员,其余的交由V4L2来完成。

int v4l2_event_dequeue(struct v4l2_fh *fh, struct v4l2_event *event,
               int nonblocking)
events出队操作,发生于用户空间的VIDIOC_DQEVENT调用,作用是从available队列中取出一个events。

v4l2_subscribed_event_ops 参数允许驱动程序设置以下四个回调函数成员:

  • add:添加一个事件订阅时被调用

  • del:取消一个事件订阅时被调用

  • replace:event以新换旧,队列满时被调用,下同,常用于只有一个elems的情况下,拷贝kevent.u.ctrl项。

  • merge:将旧的event合并到新的event中,用于多个elems的情况下,只合并changes项,原因见上面event循环过程描述。

events 通过 poll 系统调用传递到用户空间,驱动可以将 v4l2_fh->wait 作为 poll_wait() 的参数。子设备可以直接通过 notify 函数向 v4l2_device 发送 events(使用V4L2_DEVICE_NOTIFY_EVENT)。drivers/media/platform/omap3isp给出了如何使用event的实例。

注意事项:

注意v4l2_event_subscribe的elems参数,如果为0,则内核就默认分配为1,否则按照指定的参数值分配。

最好不要使用内核默认的v4l2_subscribed_event_ops,因为它的add函数会尝试在v4l2_ctrl里面查找相应id的ctrl,如果
是自定义的event id的话,有可能找不到相关的ctrl项,这样的话用户空间的VIDIOC_SUBSCRIBE_EVENT就会返回失败。

用户空间dqevent之后不必关心还回的操作,因为内核会自动获取用过的kevent,用柔性数组去管理而不是分散的链表。

子设备可以通过v4l2_subdev_notify_event函数调用来入队一个event并通知v4l2设备的notify回调。

v4l2_event_queue函数会遍历video_device上面所有的v4l2_fh,将event入队到每一个fh的列表当中。fh由用户打开video
设备节点的时候产生,每一个用户打开video节点时都会为其分配一个单独的v4l2_fh。

file->private永远指向最新的一个v4l2_fh,通过这个v4l2_fh可以找到整个v4l2_fh链表中的所有元素。

v4l2_fh_release函数会将所有挂载该fh上面的事件全部取消订阅。

猜你喜欢

转载自blog.csdn.net/qq_44710568/article/details/132604943