【V4L2】V4L2框架之驱动结构体

系列文章目录

【V4L2】V4L2框架简述

【V4L2】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_decicemdev 成员指向注册的 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 来完成。如果不是热拔插设备的话,就不必关注这些。

驱动设备使用

有些时候需要对驱动的所有设备进行迭代,这种情况通常发生在多个设备驱动使用同一个硬件设备的情况下,比如 ivtvfb 驱动就是个 framebuffer 驱动,它用到了 ivtv 这个硬件设备。可以使用以下方法来迭代所有的已注册设备:

c 复制代码
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;
}

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

c 复制代码
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 函数进行计数的加减操作)节点被创建的时候,可以人为调用以下函数对引用计数进行增减操作:

c 复制代码
    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。

v4l2_subdev 结构体

很多设备都需要与子设备进行交互,通常情况下子设备用于音视频的编解码以及混合处理,对于网络摄像机来说子设备就是 sensors 和 camera 控制器。通常情况下它们都是 I2C 设备,但也有例外。v4l2_subdev 结构体被用于子设备管理。

每一个子设备驱动都必须有一个 v4l2_subdev 结构体,这个结构体可以作为独立的简单子设备存在,也可以嵌入到更大的结构体(自定义的子设备结构体)里面。通常会有一个由内核设置的低层次结构体(i2c_client,也就是上面说的 i2c 设备),它包含了一些设备数据,要调用 v4l2_set_subdevdata 来设置子设备私有数据指针指向它,这样的话就可以很方便的从 subdev 找到相关的 I2C 设备数据(这个要编程实现的时候才能够了解它的用意)。另外也需要设置低级别结构的私有数据指针指向 v4l2_subdev 结构体,方便从低级别的结构体访问 v4l2_subdev 结构体,达到双向访问的目的,对于 i2c_client 来说,可以用 i2c_set_clientdata 函数来设置,其它的需使用与之相应的函数来完成设置。

桥驱动器需要存储每一个子设备的私有数据,v4l2_subdev 结构体提供了主机私有数据指针成员来实现此目的,使用以下函数可以对主机私有数据进行访问控制:

v4l2_get_subdev_hostdata();
v4l2_set_subdev_hostdata();

从桥驱动器的角度来看,我们加载子设备模块之后可以用某种方式获取子设备指针。对于 i2c 设备来说,调用 i2c_get_clientdata 函数即可完成,其它类型的设备也有与之相似的操作,在内核里面提供了不少的帮助函数来协助完成这部分工作,编程时可以多多使用。

每个 v4l2_subdev 结构体都包含有一些函数指针,指向驱动实现的回调函数,内核对这些回调函数进行了分类以避免出现定义了一个巨大的回调函数集,但是里面只有那么几个用得上的尴尬情况。最顶层的操作函数结构体内部包含指向各个不同类别操作函数结构体的指针成员,如下所示:

struct v4l2_subdev_core_ops {
    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_pad_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;
    const struct v4l2_subdev_vbi_ops    *vbi;
    const struct v4l2_subdev_ir_ops        *ir;
    const struct v4l2_subdev_sensor_ops    *sensor;
    const struct v4l2_subdev_pad_ops    *pad;
};

这部分的设计我个人觉得是非常实用的,linux 要想支持大量的设备的同时又要保持代码的精简就必须得这样去实现。core ops成员对于所有的子设备来说都是通用的,其余的成员不同的驱动会有选择的去使用,例如:video 设备就不需要支持 audio 这个 ops 成员。子设备驱动的初始化使用 v4l2_subdev_init 函数来完成(该函数只是初始化一些 v4l2_subdev 的成员变量,内容比较简单),在初始化之后需要设置子设备结构体的 name 和 owner 成员(如果是 i2c 设备的话,这个在 i2c helper 函数里面就会被设置)。该部分 ioctl 可以直接通过用户空间的 ioctl 命令访问到(前提是该子设备在用户空间生成了子设备节点,这样的话就可以操作子设备节点来进行 ioctl)。内核里面可以使用 v4l2_subdev_call 函数来对这些回调函数进行调用,这个在 pipeline 管理的时候十分受用。

如果需要与 media framework 进行集成,必须初始化 media_entity 结构体并将其嵌入到 v4l2_subdev 结构体里面,操作如下所示:

struct media_pad *pads = &my_sd->pads;
int err;

err = media_entity_init(&sd->entity, npads, pads, 0);

其中 pads 结构体变量必须提前初始化,media_entity 的 flags、name、type、ops 成员需要设置。entity 的引用计数在子设备节点被打开/关闭的时候会自动地增减。在销毁子设备的时候需使用 media_entity_cleanup 函数对 entity 进行清理。如果子设备需要处理 video 数据,就需要实现 v4l2_subdev_video_ops 成员,如果要集成到 media_framework 里面,就必须要实现 v4l2_subdev_pad_ops 成员,此时使用 pad_ops 中与 format 有关的成员代替 v4l2_subdev_video_ops 中的相关成员。

子设备驱动需要设置 link_validation 成员来提供自己的 link validation 函数,该回调函数用来检查 pipeline 上面的所有的 link 是否有效(是否有效由自己来做决定),该回调函数在 media_entity_pipeline_start 函数里面被循环调用。如果该成员没有被设置,那么 v4l2_subdev_link_validate_default 将会作为默认的回调函数被使用,该函数确保 link 的 source pad 和 sink pad 的宽、高、media 总线像素码是一致的,否则就会返回错误。

有两种方法可以注册子设备(注意是设备,不是设备驱动,常用的方式是通过设备树来注册

第一种(旧的方法,比如使用 platform_device_register 来进行注册)是使用桥驱动去注册设备。这种情况下,桥驱动拥有连接到它的子设备的完整信息,并且知道何时去注册子设备,内部子设备通常属于这种情况。比如 SOC 内部的 video 数据处理单元,连接到 USB 或 SOC 的相机传感器。

另一种情况是子设备必须异步地被注册到桥驱动上,比如基于设备树的系统,此时所有的子设备信息都独立于桥驱动器。使用这两种方法注册子设备的区别是 probing 的处理方式不同。也就是一种是设备信息结构体由驱动本身持有并注册,一种是设备信息结构体由设备树持有并注册。

设备驱动需要用 v4l2_device 信息来注册 v4l2_subdev ,如下所示:

int err = v4l2_device_register_subdev(v4l2_dev, sd);

如果子设备模块在注册之前消失的话,该操作就会失败,如果成功的话就会使得 subdev->dev 指向 v4l2_device。如果 v4l2_device 父设备的 mdev 成员不为空的话,子设备的 entity 就会自动地被注册到 mdev 指向的 media_device 里面。在子设备需要被卸载并且 sd->dev 变为NULL之后,使用如下函数来卸载子设备:

v4l2_device_unregister_subdev(sd);

如果子设备被注册到上层的 v4l2_device 父设备中,那么 v4l2_device_unregister 函数就会自动地把所有子设备卸载掉。但为了以防万一以及保持代码的风格统一,需要注册与卸载结对使用。

可以用以下方式直接调用ops成员:err = sd->ops->core->g_std(sd, &norm);

使用下面的宏定义可以简化书写:err = v4l2_subdev_call(sd, core, g_std, &norm);

该操作会检查 sd->dev 指针是否为空,如果是,返回 -ENODEV,同时如果 ops->core 或者 ops->core->g_std 为空,则返回 -ENOIOCTLCMD 。也可以通过以下函数调用来对 V4l2 下面挂载的所有子设备进行回调:

v4l2_device_call_all(v4l2_dev, 0, core, g_std, &norm);

该函数会跳过所有不支持该 ops 的子设备,并且所有的错误信息也被忽略,如果想捕获错误信息,可以使用下面的函数:

err = v4l2_device_call_until_err(v4l2_dev, 0, core, g_std, &norm);

该函数的第二个参数如果为 0,则所有的子设备都会被访问,如果非 0,则指定组的子设备会被访问。

组ID使得桥驱动能够更加精确的去调用子设备操作函数,例如:在一个单板上面有很多个声卡,每个都能够改变音量,但是通常情况下只访问一个,这时就可以设置子设备的组 ID 为 AUDIO_CONTROLLER 并指定它的值,这时 v4l2_device_call_all 函数就会只去访问指定组的子设备,提高效率。

如果子设备需要向 v4l2_device 父设备发送事件通知的话,就可以调用 v4l2_subdev_notify 宏定义来回调 v4l2->notify 成员(前文有提到过)。

使用 v4l2_subdev 的优点是不包含任何底层硬件的信息,它是对底层硬件的一个抽象,因此一个驱动可能包含多个使用同一条 I2C 总线的子设备,也可能只包含一个使用 GPIO 管脚控制的子设备,只有在驱动设置的时候才有这些差别,而一旦子设备被注册之后,底层硬件对驱动来说就是完全透明的。

/** 不清楚异步模式的用途 /** 在异步模式下,子设备 probing 可以被独立地被调用以检查桥驱动是否可用,子设备驱动必须确认所有的

probing 请求是否成功,如果有任意一个请求条件没有满足,驱动就会返回 -EPROBE_DEFER

来继续下一次尝试,一旦所有的请求条件都被满足,子设备就需要调用 v4l2_async_register_subdev 函数来进行注册(用

v4l2_async_unregister_subdev 卸载)。桥驱动反过来得注册一个 notifier

对象(v4l2_async_notifier_register),该函数的第二个参数类型是 v4l2_async_notifier

类型的结构体,里面包含有一个指向指针数组的指针成员,指针数组每一个成员都指向 v4l2_async_subdev 类型结构体。v4l2

核心层会利用上述的异步子设备结构体描述符来进行子设备的匹配,如果成功匹配,.bound()notifier回调函数将会被调用,当所有的子设备全部被加载完毕之后,.complete()

回调函数就会被调用,子设备被移除的时候 .unbind() 函数就会被调用。 /**

另外子设备还提供了一组内部操作函数,该内部函数的调用时机在下面有描述,原型如下所示:

c 复制代码
struct v4l2_subdev_internal_ops {
    int (*registered)(struct v4l2_subdev *sd);
    void (*unregistered)(struct v4l2_subdev *sd);
    int (*open)(struct v4l2_subdev *sd, struct v4l2_subdev_fh *fh);
    int (*close)(struct v4l2_subdev *sd, struct v4l2_subdev_fh *fh);
};

这些函数仅供 v4l2 framework 使用,驱动程序不应该显式的去调用这些回调

  • registered/unregister:在子设备被注册(v4l2_device_register_subdev)/反注册的时候被调用。

  • open/close:如果子设备在用户空间创建了设备节点,那么这两个函数就会在用户空间的设备节点被打开/关闭的时候调用到,主要是用来创建/关闭v4l2_fh以供v4l2_ctrl_handler等的使用。

相关推荐
Yan.love18 分钟前
开发场景中Java 集合的最佳选择
java·数据结构·链表
stm 学习ing22 分钟前
HDLBits训练5
c语言·fpga开发·fpga·eda·hdlbits·pld·hdl语言
冠位观测者28 分钟前
【Leetcode 每日一题】2545. 根据第 K 场考试的分数排序
数据结构·算法·leetcode
就爱学编程1 小时前
重生之我在异世界学编程之C语言小项目:通讯录
c语言·开发语言·数据结构·算法
北国无红豆2 小时前
【CAN总线】STM32的CAN外设
c语言·stm32·嵌入式硬件
单片机学习之路2 小时前
【C语言】结构
c语言·开发语言·stm32·单片机·51单片机
ALISHENGYA3 小时前
全国青少年信息学奥林匹克竞赛(信奥赛)备考实战之分支结构(实战项目二)
数据结构·c++·算法
DARLING Zero two♡3 小时前
【优选算法】Pointer-Slice:双指针的算法切片(下)
java·数据结构·c++·算法·leetcode
graceyun4 小时前
C语言初阶习题【9】数9的个数
c语言·开发语言