V4L2框架概述

32
发表时间:2023-06-27 14:34

V4L2是Video for linux2的简称,为linux中关于视频设备的内核驱动。在Linux中,视频设备是设备文件,可以像访问普通文件一样对其进行读写,摄像头在/dev/video*下,如果只有一个视频设备,通常为/dev/video0。V4L2在设计时,是要支持很多广泛的设备的,它们之中只有一部分在本质上是真正的视频设备。v4L2是针对uvc免驱usb设备的编程框架,主要用于采集usb摄像头等。V4L2 是专门为 linux 设备设计的一套视频框架,其主体框架在 linux 内核,可以理解为是整个 linux 系统上面的视频源捕获驱动框架。其广泛应用在嵌入式设备以及移动端、个人电脑设备上面,市面上的编码产品类如:SDV、手机、IPC、行车记录仪都会用到这个框架来进行视频采集。


V4L2框架简介


几乎所有的设备都有多个 IC 模块,它们可能是实体的(例如 USB 摄像头里面包含 ISP、sensor 等)、也可能是抽象的(如 USB 设备里面的抽象拓扑结构),它们在 /dev 目录下面生成了多个设备节点,并且这些 IC 模块还创建了一些非 v4l2 设备:DVB、ALSA、FB、I2C 和输入设备。正是由于硬件的复杂性,v4l2 的驱动也变得非常复杂。


特别是 v4l2 驱动要支持 IC 模块来进行音/视频的混合/编解码操作,这就更加使得 v4l2 驱动变得异常复杂。通常情况下,有些IC模块通过一个或者多个 I2C 总线连接到主桥驱动上面,同时其它的总线仍然可用,这些 IC 就称为 ‘sub-devices’,比如摄像头设备里面的 sensor 传感器就是使用 I2C 来进行命令沟通,同时使用 MIPI 或者 LVDS 等接口进行图像数据传输。


在很长一段时间内,该框架(指老旧的 V4L2 框架)仅限于通过 video_device 结构体创建 v4l 设备节点和 video_buf 来处理视频数据。这意味着所有的驱动都必须对设备实例进行设置并将其映射到子设备上。有些时候这些操作步骤十分复杂,很难正确完成,并且有些驱动程序从来没有正确的按照这些操作步骤编写。由于缺少一个框架,有很多通用代码就没有办法被重构,从而导致这部分代码被重复编写,效率比较低下。


因此,本框架抽象构建了所有驱动都需要的代码并封装为一个个的模块,简化了设备驱动通用代码的重构。v4l2-pci-skeleton.c 是个非常好的参考例程,它是一个PCI采集卡的驱动框架。该例程演示了如何使用 v4l2 驱动框架,并且该例程可以作为一个 PCI 视频采集卡的驱动模板使用。在最开始的时候也可以参照这个代码编写方式进行联系,当然最适合的代码还是 drivers/media/video/omap3isp 文件夹里面的代码,这个代码基本上可以作为一个完整的输入设备实例代码(因为它包含了 ISP、CSI、video 等设备,并且有着一个完整的数据流 pipeline,几乎用到了 V4L2 框架的方方面面,参考价值极大)来进行参考编写自己的设备驱动代码。


V4L2框架蓝图


这是一张非常大的图,但是我只选取了其中的一个,这张图对 V4L2 里面的子模块进行简化(简化到只有子模块的名字,没有内部实现的介绍),大图如下:

这张图怎么看呢?它有以下几个关键因素:


  • v4l2_device:这个是整个输入设备的总结构体,可以认为它是整个 V4L2 框架的入口,充当驱动的管理者以及入口监护人。由该结构体引申出来 v4l2_subdev。用于视频输入设备整体的管理,有多少输入设备就有多少个v4l2_device抽象(比如一个USB摄像头整体就可以看作是一个 V4L2 device)。再往下分是输入子设备,对应的是例如 ISP、CSI、MIPI 等设备,它们是从属于一个 V4L2 device 之下的。

  • media_device:用于运行时数据流的管理,嵌入在 V4L2 device 内部,运行时的意思就是:一个 V4L2 device 下属可能有非常多同类型的子设备(两个或者多个 sensor、ISP 等),那么在设备运行的时候我怎么知道我的数据流需要用到哪一个类型的哪一个子设备呢。这个时候就轮到 media_device 出手了,它为这一坨的子设备建立一条虚拟的连线,建立起来一个运行时的 pipeline(管道),并且可以在运行时动态改变、管理接入的设备。

  • v4l2_ctrl_handler:控制模块,提供子设备(主要是 video 和 ISP 设备)在用户空间的特效操作接口,比如你想改变下输出图像的亮度、对比度、饱和度等等,都可以通过这个来完成。

  • vb2_queue:提供内核与用户空间的 buffer 流转接口,输入设备产生了一坨图像数据,在内核里面应该放在哪里呢?能放几个呢?是整段连续的还是还是分段连续的又或者是物理不连续的?用户怎么去取用呢?都是它在管理。


层级解构


  1. 可以看到图中的入口 custom_v4l2_dev,它是由用户定义的一个结构体,重要的不是它怎么定义的,重要的是它里面有一个 v4l2_device 结构体,上文说到,这个结构体总览全局,运筹帷幄,相当于中央管理处的位置,那么中央决定了,它就是整个输入设备整体的抽象(比如整个 USB 摄像头输入设备,比如整个 IPC 摄像头输入设备)。它还有一个 media_device 结构体,上文也说道,,它是管数据流线路的,属于搞结构路线规划管理的。

  2. 往后 v4l2_device 里面有一个链表,它维护了一个巨大的子设备链,所有的子设备都通过内核的双向循环链表结构以 v4l2_device 为中心紧紧团结在一起。另外 media_device 在往里面去就是一个个的 media_entity(现在不需要了解它的具体含义,只需要知道它就是类似电路板上面的元器件一样的抽象体),media_entity 之间建立了自己的小圈子,在它们这个小圈子里面数据流按照一定的顺序畅通无阻,恣意遨游。

  3. 到结尾处,抽象出来了 /dev/videoX 设备节点,这个就是外交部的角色,它负责提供了一个内核与用户空间的交流枢纽。需要注意的是,该设备节点的本质还是一个字符设备,其内部的一套操作与字符设备是一样的,只不过是进行了一层封装而已。

  4. 到此为止,一个 V4L2 大概的四层结构就抽象出来了,如下图所示:




驱动结构体


所有的 V4L2 驱动都有以下结构体类型:

  • 每个设备都有一个设备实例结构体(上面的 custom_v4l2_dev),里面包含了设备的状态;

  • 一种初始化以及控制子设备(v4l2_subdev)的方法;

  • 创建v4l2设备节点并且对设备节点的特定数据(media_device)保持跟踪;

  • 含有文件句柄的文件句柄结构体(v4l2_fh 文件句柄与句柄结构体一一对应);

  • 视频数据处理(vb2_queue);


结构体实例


  • 框架结构体(media_device)

与驱动结构体非常类似,参考上面的解释,这里不再赘述。v4l2 框架也可以整合到 media framework 里面。如果驱动程序设置了 v4l2_device 的 mdev 成员,那么子设备与 video 节点都会被自动当作 media framework 里的 entitiy 抽象。

  • v4l2_device 结构体

每一个设备实例都被抽象为一个 v4l2_device 结构体。一些简单的设备可以仅分配一个 v4l2_device 结构体即可,但是

大多数情况下需要将该结构体嵌入到一个更大的结构体(custom_v4l2_dev)里面。必须用 v4l2_device_register(struct device *dev, struct v4l2_device *v4l2_dev); 来注册设备实例。该函数会初始化传入的 v4l2_device 结构体,如果 dev->driver_data 成员为空的话,该函数就会设置其指向传入的 v4l2_dev 参数。

  • 集成 media framework

如果驱动想要集成 media framework 的话,就需要人为地设置 dev->driver_data 指向驱动适配的结构体(该结构体由

驱动自定义- custom_v4l2_dev,里面嵌入 v4l2_device 结构体)。在注册 v4l2_device 之前就需要调用 dev_set_drvdata 来完成设置。并且必须设置 v4l2_decice 的 mdev 成员指向注册的 media_device 结构体实例。

  • 设备节点的命名

如果 v4l2_device 的 name 成员为空的话,就按照 dev 成员的名称来命名,如果 dev 成员也为空的话,就必须在注册 v4l2_device 之前设置它的 name 成员。可以使用 v4l2_device_set_name 函数来设置 name 成员,该函数会基于驱动名以及驱动实例的索引号来生成 name 成员的名称,类似于 ivtv0、ivtv1 等等,如果驱动名的最后一个字母是整数的话,生成的名称就类似于cx18-0、cx18-1等等,该函数的返回值是驱动实例的索引号。

  • 回调函数与设备卸载

还可以提供一个 notify() 回调函数给 v4l2_device 接收来自子设备的事件通知。当然,是否需要设置该回调函数取决于子设备是否有向主设备发送通知事件的需求。v4l2_device 的卸载需调用到 v4l2_device_unregister 函数。在该函数被调用之后,如果 dev->driver_data 指向 v4l2_device 的话,该指针将会被设置为NULL。该函数会将所有的子设备全部卸载掉。如果设备是热拔插属性的话,当 disconnect 发生的时候,父设备就会失效,同时 v4l2_device 指向父设备的指针也必须被清除,可以调用 v4l2_device_disconnect 函数来清除指针,该函数并不卸载子设备,子设备的卸载还是需要调用到 v4l2_device_unregister 来完成。如果不是热拔插设备的话,就不必关注这些。





分享到:
微信公众号:
蓝松文档:
地址:浙江省杭州市余杭区仓前街道龙园路88号3号楼A1318室(创鑫时代广场)