全志T7 v4l2框架概述

本文深入解析Linux内核中的V4L2框架,介绍了V4L2设备的多层次结构,包括v4l2_device作为核心管理结构,media_device用于数据流管理,以及v4l2_ctrl_handler提供的控制接口。文章以全志T7的Vin驱动为例,阐述了驱动注册、设备节点、子设备管理和数据流转等方面的知识,揭示了V4L2驱动的复杂性和灵活性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

下文主要参考linux-3.10内核文档对V4L2框架进行一次全局的介绍。

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 等接口进行图像数据传输。
本文档分析的是全志T7 的Vin驱动代码,这个代码是一个完整的输入设备代码(因为它包含了 ISP、CSI、video 等设备,并且有着一个完整的数据流 pipeline,几乎用到了 V4L2 框架的方方面面。如果只是为了学习,可参考linux-3.10\drivers\media\platform\vivi.c来编写自己的设备驱动代码 )。

V4L2框架蓝图

在这里插入图片描述

以下几个关键因素

  • vin_md:自定义的驱动结构体,全称是Video input_media device
  • v4l2_device:这个是整个输入设备的总结构体,充当驱动的管理者以及入口监护人。用于视频输入设备整体的管理,有多少输入设备就有多少个v4l2_device抽象(比如一个USB摄像头整体就可以看作是一个V4L2 device)。再往下分是输入子设备,对应的是例如 ISP、CSI、MIPI 等设备,它们是从属于一个 V4L2 device之下的,所有子设备组成一个list,被v4l2_device管理。
  • media_device:多媒体设备,用于运行时数据流的管理,嵌入在 V4L2 device 内部,运行时的意思就是:一个 V4L2 device 下属可能有非常多同类型的子设备(两个或者多个sensor、ISP 等),那么在设备运行的时候我怎么知道我的数据流需要用到哪一个类型的哪一个子设备呢。这个时候就轮到media_device出手了,每个子设备都有media_entity(多媒体设备节点,现在不需要了解它的具体含义,只需要知道它就是类似电路板上面的元器件一样的抽象体),被media_device.entites管理;每个media_entity间可以形成一个Media link连接线,几个Media link可构成一整条虚拟的连线,即一个运行时的pipeline(管道),它可以在运行时动态改变、管理接入的设备。
  • v4l2_ctrl_handler:控制模块,提供子设备(主要是video 和 ISP设备)在用户空间的特效操作接口,比如你想改变下输出图像的亮度、对比度、饱和度等等,都可以通过这个来完成。所以,操作子设备有两种方式,一种是通过主设备的ioctl来调用(上图粉红色)但他会遍历所有的子设备并调用相应ops结构体里面的方法,另一种是通过子设备的节点来调用(上图橄榄色),最终只调用该子设备ctrl_handler的方法,这些方法一般实现子设备特殊的功能。
  • vb2_queue:提供内核与用户空间的 buffer流转接口,输入设备产生了一坨图像数据,在内核里面应该放在哪里呢?能放几个呢?是整段连续的还是还是分段连续的又或者是物理不连续的?用户怎么去取用呢?都是它在管理。

层级结构

  • 可以看到图中的入口 vin_md,它是由T7定义的一个结构体,重要的不是它怎么定义的,重要的是它里面有一个 v4l2_device 结构体,这个结构体总览全局,运筹帷幄,相当于中央管理处的位置,那么中央决定了,它就是整个输入设备整体的抽象(比如整个 USB摄像头输入设备,比如整个 IPC 摄像头输入设备)。它还有一个 media_device结构体,上段文字说道,它是管数据流线路的,属于搞结构路线规划管理的。
  • 往后 v4l2_device 里面有一个链表,它维护了一个巨大的子设备链,所有的子设备都通过内核的双向循环链表结构以 v4l2_device 为中心紧紧团结在一起。另外 media_device 在往里面去就是一个个的media_entity,media_entity 之间建立了自己的小圈子,在它们这个小圈子里面数据流按照一定的顺序畅通无阻,恣意遨游。
  • 一般在程序的结尾处,会抽象出来了 /dev/videoX 设备节点,这个就是外交部的角色,它负责提供了一个内核与用户空间的交流枢纽。需要注意的是,该设备节点的本质还是一个字符设备,其内部的一套操作与字符设备是一样的,只不过是进行了一层封装而已。
  • 到此为止,一个 V4L2 大概的四层结构就抽象出来了,如下图所示:
    V4L2 层次结构

驱动结构体

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

  • 每个设备都有一个设备实例结构体(上面的 vin_md),里面包含了设备的状态;
  • 一种初始化以及控制子设备(v4l2_subdev)的方法;
  • 创建v4l2设备节点并且对设备节点的特定数据(media_device)保持跟踪; 含有文件句柄的文件句柄结构体(v4l2_fh文件句柄与句柄结构体一一对应);
  • 视频数据处理(vb2_queue);

结构体实例

  • Media框架结构体(media_device)
    与驱动结构体非常类似,参考上面的解释,这里不再赘述。v4l2 框架也可以整合到 media framework 里面。如果驱动程序设置了 v4l2_device 的 mdev 成员,那么子设备与 video 节点都会被自动当作 media framework 里的 entitiy 抽象。
  • v4l2_device 结构体
    每一个设备实例都被抽象为一个 v4l2_device 结构体。一些简单的设备可以仅分配一个 v4l2_device 结构体即可,但是 大多数情况下需要将该结构体嵌入到一个更大的结构体(例如T7的vin_md)里面。必须用 v4l2_device_register(struct device *dev, struct v4l2_device *v4l2_dev); 来注册设备实例。该函数会初始化传入的 v4l2_device 结构体,如果 dev->driver_data成员为空的话,该函数就会设置其指向传入的 v4l2_dev 参数。
  • 设备节点的命名
    如果 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 来完成。如果不是热拔插设备的话,就不必关注这些。

驱动设备使用

这部分不重要,要理解的是 v4l2_device 引入了引用计数机制防止对设备过多次的调用和卸载。
有些时候需要对驱动的所有设备进行迭代,这种情况通常发生在多个设备驱动使用同一个硬件设备的情况下,比如 ivtvfb 驱动就是个 framebuffer 驱动,它用到了 ivtv 这个硬件设备。可以使用以下方法来迭代所有的已注册设备:

static int callback(struct device *dev, void *p)
{
    struct v4l2_device *v4l2_dev = dev_get_drvdata(dev);

    /* test if this device was inited */
    if (v4l2_dev == NULL)
        return 0;
    ...
    return 0;
}

int iterate(void *p)
{
    struct device_driver *drv;
    int err;

    /* Find driver 'ivtv' on the PCI bus.
    * pci_bus_type is a global. For USB busses use usb_bus_type.
    */
    drv = driver_find("ivtv", &pci_bus_type);
    /* iterate over all ivtv device instances */
    err = driver_for_each_device(drv, NULL, p, callback);
    put_driver(drv);
    return err;
}

有时候需要对设备实例进行计数以将设备实例映射到模块的全局数组里面,可以使用以下步骤来完成计数操作:

static atomic_t drv_instance = ATOMIC_INIT(0);

static int drv_probe(struct pci_dev *pdev, const struct pci_device_id *pci_id)
{
    ...
    state->instance = atomic_inc_return(&drv_instance) - 1;
}

如果一个热拔插设备有很多个设备节点(比如一个USB摄像头可以产生多路视频输出,虽然它的视频源是一个),那么很难知道在什么时候才能够安全地卸载 v4l2_device 设备。基于以上问题, v4l2_device 引入了引用计数机制,当 video_register_device 函数被调用的时候,引用计数会加一,当 video_device 被释放的时候,引用计数会减一,直到 v4l2_device 的引用计数到0的时候,v4l2_device的 release 回调函数就会被调用,可以在该回调函数里面做一些清理工作。当其它的设备(alsa,因为这个不属于 video 设备,所以也就不能使用上面的 video 函数进行计数的加减操作)节点被创建的时候,可以人为调用以下函数对引用计数进行增减操作:

    void v4l2_device_get(struct v4l2_device *v4l2_dev);
    int v4l2_device_put(struct v4l2_device *v4l2_dev);

需要注意的是,v4l2_device_register 函数将引用计数初始化为1,所以需要在 remove 或者 disconnect 回调方法里面调用 v4l2_device_put 来减少引用计数,否则引用计数将永远不会达到0。

驱动代码分析

这里对T7 sun_vin驱动进行分析:各个模块的代码、应用程序,然后再了解ioctl调用过程、v4l2 videobuf如何传递视频数据。
vin驱动入口
nvp6134驱动
vin core驱动
vin_video驱动
csi&isp等驱动

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值