Skip to main content
 首页 » 操作系统

Linux V4L2驱动内核文档翻译

2022年07月19日26del

随着一些视频或者图像硬件的复杂化,V4L2驱动也越来越趋于复杂。许多硬件有多个IC,在/dev下生成多个video设备或者其他的诸如,DVB,ALSA,FB,I2C ,IR等等
非V4L2的设备。所以,V4L2驱动程序就要为这些硬件设备提供音视频的合成以及编解码的功能接口,另外,通常这些设备都通过多个I2C总线实现和CPU的通讯,不仅是I2C总
线,其他的也有可能被使用,比如SPI,1-wire,等等。挂在这些总线上的设备叫做sub-devices,即V4L2设备的子设备。
之前相当长的一段时间内,V4L2被限制在使用video_device来创建V4L2设备节点,使用videobuf来处理视频缓存。这就意味着,所有的驱动驱动程序除了创建一个设备实
例外,还要单独实现连接”子设备”的步骤。这个过程比较复杂,也容易产生错误。正是缺少这样一种框架,使得在代码重用方面做得不够好,驱动程序看起来很臃肿。所以V4L2
框架才被整理出来,提供一些基础的组件,通过一些共享的功能函数简化驱动的编写,使代码的重用性增强,硬件设备驱动只需要实现相关的操作而不必关心交互式的应用,同
时应用可以更加透明地使用硬件来驱动音视频的处理。而且这个框架也在不断地更新扩展,基础部分就是提供的v4l2API。但这里不讨论V4L2提供了哪些API和他们如何被使用,
我们只讨论和v4l2核心及驱动相关的知识。

首先来看看所有的v4l2驱动都必须要有的几个组成部分:
  – 用来描述每一个v4l2设备实例状态的结构(structv4l2_device)。
  – 用来初始化和控制子设备的方法(structv4l2_subdev)。
  – 要能创建设备节点并且能够对该节点所持有的数据进行跟踪(structvideo_device)。
  – 为每一个被打开的节点维护一个文件句柄(structv4l2_fh)。
  – 视频缓冲区的处理(videobuf或者videobuf2 framework)。

在linux3.0以上的内核对这些结构的定义,从定义当中就可以窥探整个v4l2的框架。这些结构体有:
  struct v4l2_device; 用来描述一个v4l2设备实例
  struct v4l2_subdev, 用来描述一个v4l2的子设备实例
  struct video_device; 用来创建设备节点/dev/videoX
  struct v4l2_fh; 用来跟踪文件句柄实例
我们把videobuf及videobuf2框架放到后面的系列来讨论。

用一个比较粗糙的图来表现他们之间的关系,大致为:
  设备实例(v4l2_device)
  |______子设备实例(v4l2_subdev)
  |______视频设备节点(video_device)
  |______文件访问控制(v4l2_fh)
  |______视频缓冲的处理(videobuf/videobuf2)
好了,接下来我们一一分析一下这些结构的定义。

1、v4l2_device
这个定义在linux/media/v4l2-device.h当中定义
struct v4l2_device {
struct device *dev; //指向设备模型的指针
#if defined(CONFIG_MEDIA_CONTROLLER)
struct media_device *mdev; //指向一个媒体控制器的指针
#endif
struct list_head subdevs; //管理子设备的双向链表,所有注册到的子设备都需要加入到这个链表当中
spinlock_t lock; //全局锁
char name[V4L2_DEVICE_NAME_SIZE]; //设备名称
void (*notify)(struct v4l2_subdev*sd, uint notification, void *arg); //通知回调函数,通常用于子设备传递事件,这些事件可以是自定义事件
struct v4l2_ctrl_handler*ctrl_handler; //控制句柄
struct v4l2_prio_state prio; //设备的优先级状态,一般有后台,交互,记录三种优先级,依次变高
struct mutex ioctl_lock; //ioctl操作的互斥量
struct kref ref; //本结构体的引用追踪
void (*release)(struct v4l2_device*v4l2_dev); //设备释放函数
};

要注册一个实例,需要使用函数
v4l2_device_register(struct device*dev, struct v4l2_device *v4l2_dev);
该函数将会初始化v4l2_device结构,如果dev->driver_data为空,那么将把v4l2_dev赋值给这个driver_data。
如果驱动要集成媒体设备框架,就需要手动设置dev->driver_data来指向一个嵌入了v4l2_device结构的媒体设备结构,这个结构可以是指定驱动的状态描述。
在注册函数之前调用dev_set_drvdata来完成,这些都要在调用register函数之前就要设置好,同样,如果有媒体设备的话,必须也要在此之前就要初始化好,
并且设置v4l2_device的mdev域来指向一个已经初始化过的媒体设备实例。
如果v4l2_dev->name是空,注册函数也将根据dev->name来设置v4l2_dev的name,如果已经设置,那么注册函数将不再过问。如果dev是空,那么就必须在
调用注册函数之前设置v4l2_dev->name。当然,也可以使用v4l2_device_set_name()来设置设备实例名称。

要移除注册的话,调用函数:
v4l2_device_register(structv4l2_device *vd)
如果是可热插拔的设备,那么还需要调用
v4l2_device_disconnect(structv4l2_device *vd)
来断开设备的连接,否则会产生空指针的问题。

有的时候需要迭代驱动注册的所有设备,这个通常出现在多个设备驱动使用相同的硬件的情况,比如说ivtvfb是一个framebuffer驱动,它使用ivtv硬件,同时也是
一个tv驱动。相同的情况对于alsa驱动也是适用的。那么,迭代该怎么完成呢?看下面这个例程:
static int callback(struce device *dev,void *p)
{
  struct v4l2_device *vdev =dev_get_drvdata(dev);
  if (vdev == NULL) return 0;
  /*do something*/
  return 0;
}
int iterate(void *p)
{
  struct device_driver *drv;
  int err = 0;
  /*Find driver 'vivi' on the PCI bus*/
  drv = driver_find(“vivi”,&pci_bus_type);
  err = driver_for_each_device(drv,NULL, p, callback);
  put_driver(drv);
  return err;
}

有时候还需要维护一个运行时的设备实例计数,定义一个原子变量就可以了。如果一个v4l2设备上注册了很多设备节点,我们在移除注册v4l2_device的时候,
就要等到所有的设备节点移除之后,ref成员帮助我们记录v4l2_device的节点注册数,每次调用video_register_device都会加1,反之则减一。一旦这个值为0的时候,
我们才可以调用v4l2_device_unregister。如果不是视频节点,那么手动调用这两个函数来计数:
void v4l2_device_get(struct v4l2_device*vd) //ref +1
int v4l2_device_put(struct v4l2_device*vd) // ref -1

2、v4l2_subdev
struct v4l2_subdev {
  #if defined(CONFIG_MEDIA_CONTROLLER)
  struct media_entity entity; //媒体控制器的实体,和v4l2_device
  #endif
  struct list_head list;
  struct module *owner;
  u32 flags;
  struct v4l2_device *v4l2_dev; //指向一个v4l2设备
  const struct v4l2_subdev_ops *ops; //子设备的操作函数集
  const struct v4l2_subdev_internal_ops*internal_ops; //子设备的内部操作函数集
  struct v4l2_ctrl_handler*ctrl_handler; //控制函数处理器
  char name[V4L2_SUBDEV_NAME_SIZE]; //子设备的名称
  u32 grp_id; //子设备所在的组标识
  void *dev_priv; //子设备私有数据指针,一般指向总线接口的客户端
  void *host_priv; //子设备私有的数据指针,一般指向总线接口的host端
  struct video_device devnode; //设备节点
  unsigned int nevents; //子设备的事件
};

很多v4l2驱动程序都需要和子设备(sub_device)来进行通讯,这些设备实际上完成了所有的任务,比如说音视频的合成,编码,解码。对于webcam来说,子设备就
是sensor和camera控制器。通常这些都是I2C设备,但也不是必须的。为了给这些子设备提供一个一致的接口,v4l2_subdev结构才应运而生。

每一个子设备都必须有一个v4l2_subdev结构。这个结构可以单独地使用或者被嵌入一个更大的结构。通常有一个更低级的设备结构(比如i2c_client),它包含设备的
一些初始化数据,所以建议v4l2_subdev->dev_priv指向该数据,可以通过函数:
  v4l2_set_subdevdata()
  v4l2_get_subdevdata()
来设置,然后调用v4l2_get_subdevdata()这样就会很方便的从v4l2_subdev找到实际的总线相关的设备数据。总之是一些私有的数据,可以是平台相关的数据,可以是自
己定义的包含了v4l2_subdev结构的设备实例等等。
同样也需要一个从总线相关的设备方便的找到v4l2_subdev,可以这样来实现例如:i2c_set_clientdata().调用这个函数来把v4l2_subdev结构指针赋给i2c_client的private
数据。然后调用i2c_get_clientdata()获得v4l2_subdev的指针。当然也可以通过container_of来操作,但是内核既然提供了这样的api,用之何乐不为呢?
每一个v4l2_subdev包含了子设备可以实现的函数指针。这些函数可以做很多很多不同的事情,它们根据不同的操作类别被放在不同的结构当中。最高一级的操作函数集涵盖
了各种操作大类。比如:
struct v4l2_subdev_core_ops {
  int (*g_chip_ident)(struct v4l2_subdev*, struct v4l2_dbg_chip_ident *);
  int (*log_status)(struct v4l2_subdev*sd);
  int (*init)(struct v4l2_subdev *sd,u32 val);
  ….
};
struct v4l2_subdev_tuner_ops {};
struct v4l2_subdev_audio_ops {};
struct v4l2_subdev_video_ops {};
struct v4l2_subdev_ops {
  const struct v4l2_subdev_core_ops*core;
  const struct v4l2_subdev_tuner_ops*tuner;
  const struct v4l2_subdev_audio_ops*audio;
  const struct v4l2_subdev_video_ops*video;
};
core操作对于所有子设备来说是共通的,其他的类型可以根据子设备的需要来实现,比如一个视频子设备不太可能实现音频的操作等等。
至此我们介绍了v4l2_subdev的一些成员及操作函数,那么下面就可以进行初始化了,初始化函数调用:
  v4l2_subdev_init(sd, &ops);
之后就需要初始化子设备的名字和owner等等。
如果需要集成媒体框架,那么我们就必须初始化这个media_entity结构,并将其嵌入到v4l2_subdev的结构当中,这个通过调用media_entity_init()来实现。
  struct media_pad *pads = &my_sd->pads;
  media_entity_init(&sd->entity,npads, pads, 0);

pads数组必须在之前就已经初始化好。这里不需要进行media_entity的类型和名字的手动初始化,但是revision域如果需要的话就必须要初始化。Entity的引用参数会
自动在子设备节点被打开和关闭的时候进行加减。在子设备被销毁之前不要忘了cleanup这个mediaentity.调用media_entity_cleanup(&sd->entity)。关于media_entity
的相关知识这里不做讨论。下面继续讨论v4l2_subdev的注册。

注册v4l2_subdev子设备实例到v4l2_device设备系统当中,用这个函数:
  v4l2_device_register_subdev(sd)
这个函数执行成功之后,subdev->dev将指向v4l2_device,如果v4l2_device的mdev是一个非空的值,那么subdev->entity也将会被自动注册为mdev。要移除注册的子设备,调用:
  v4l2_device_unregister_subdev(sd)
接下来介绍子设备提供的功能调用,如果要使用子设备提供的接口函数,有两种方法,第一种就是直接使用ops中的回调函数,但是不推荐这样做,一般是用第二种方法,调用函数:
  v4l2_subdev_call(sd, o, f, arg...)
来获取子设备芯片的标识。其中,sd就是子设备实例,o是子设备下操作函数的大类,例如可以是core/video/audio/tuner,f是大类下面的功能回调函数,arg是传入的参数。
另外,还可以通过v4l2设备实例调用全部子设备的功能回调函数,使用这个函数:
  v4l2_device_call_all(v4l2, grp_id, o,f, arg...)
其中grp_id就是子设备的组标识。举个例子:
  v4l2_subdev_call(sd, video,g_chip_cap, &cap);
  v4l2_device_call_all(v4l2, 0, core,g_chip_id, &cap);
前者是调用子设备sd的video类下的g_chip_cap功能回调函数;后者是v4l2设备调用所有子设备的core类下的g_chip_id功能回调函数。grp_id非0则指定调用相同组标识的该方法。
子设备还需要通知它的v4l2父设备发生了什么事件,这个通过调用下面这个函数实现。
  v4l2_subdev_notify(sd, notification,arg)
但是父设备必须要有能够处理这些事件的能力,就是实现v4l2_device的notify功能。
除了通过v4l2_subdev_ops结构暴露给内核的API之外,v4l2子设备也同样可以被用户程序直接控制。设备节点名为v4l-subdevX创建在/dev目录下,这样就可以通过打开设备文件来
直接访问子设备。如果一个子设备支持直接的用户空间访问,那么它就必须在被注册之前就设置V4L2_SUBDEV_FL_HAS_DEVNODE标志。注册子设备之后,v4l2_device驱动就会为所有
持此标志的子设备创建设备节点。这个通过v4l2_device_register_subdev_nodes()来实现。这个设备节点可以处理一组标准的V4l2API子集,如下:
  VIDIOC_QUERYCTRL
  VIDIOC_QUERYMENU
  VIDIOC_G_CTRL
  VIDIOC_S_CTRL
  VIDIOC_G_EXT_CTRLS
  VIDIOC_S_EXT_CTRLS
  VIDIOC_TRY_EXT_CTRLS
所有上面这些控制调用都可以通过core:ioctl操作来完成。至此,关于v4l2子设备相关的知识就介绍完毕。

备注:
内核为我们提供了很多帮助函数,比如v4l2_i2c子设备驱动框架,使编写此类驱动变得容易很多。因为此类驱动有很多共通之处,所以可以将其抽象出来以便易于使用。这个抽象在
v4l2_common.h当中。
给一个I2C驱动添加v4l2_subdev支持的推荐方法是将v4l2_subdev结构嵌入I2C设备实例的state结构当中。非常简单的设备没有这个结构,那么就直接创建一个v4l2_subdev实例就好
了。一个典型的state结构就像这样:
struct chipname_state {
  struct v4l2_subdev sd;
  ….. /*这里存放额外的状态域*/
};
初始化一个v4l2_subdev并且将其和i2c总线设备连接起来
  v4l2_i2c_subdev_init(&state->sd,client, subdev_ops);
这个函数将填充所有v4l2_subdev结构的域,并且保证v4l2_subdev和i2c_client能够互相找到。最好是能够实现一个state和subdev互访的inline函数:
static inline struct chipname_state*to_state(struct v4l2_subdev *sd)
{
  return container_of(sd, structchipname_state, sd);
}
然后通过如下函数实现v4l2_subdev和i2c_client的互访:
  struct i2c_client *client =v4l2_get_subdevdata(sd);
  struct v4l2_subdev *sd =i2c_get_clientdata(client);
要确保在subdev的驱动被remove的时候调用如下函数:
  v4l2_device_unregister_subdev(sd);
另外还有一些帮助函数可以使用:
  struct v4l2_subdev *sd =v4l2_i2c_new_subdev(v4l2_dev, adapter, “module_foo”,“chipid”, 0x36, NULL);
这个函数会load一个i2c的adapter然后调用i2d_new_device并且根据chipid和i2c的地址(0x36)来创建一个新的i2c设备,之后会将模块名为module_foo的subdev注册到v4l2_dev
里面去。关于其他的帮助函数,请查阅v4l2-common.h文件。


本文参考链接:https://www.cnblogs.com/hellokitty2/p/7859497.html