本文对 V4L2 中比较容易理解的骨干结构进行介绍,涉及两个核心结构体:
v4l2_device
,v4l2_subdev
。文章围绕这两个结构体以 Linux-4.4 内核的 omap3isp 代码为例进行相关的介绍,所谓介绍还是起到辅助作用,真真儿的还是要靠 RTFSC、WTFSC。
下面「该例程」均指的是 omap3isp 这个例程。
V4L2 框架补充
首先看图:
这并不是官方的划分,是我自己根据自己感觉划分出来的,也就是那么回事儿吧。那么这篇文章里面秀的主题就是子设备系统这一块,就是上面图中的第三个「subdev」。由图也可知,本文主要是讲如何管理众多的输入设备。注意:是所有的设备,并不是运行时的数据流通路上面的设备。
主设备
主设备使用 v4l2_device
进行抽象化表示。该例程使用了设备树来进行设备解析,使用平台驱动进行相应的驱动 probe。在文件 drivers/media/platform/omap3isp/isp.c
中有 isp_probe
函数,该函数的第一个参数定义即是 isp_device
,回忆前面的文章,里面有提到大多数情况下,需要将 v4l2_device
结构体嵌入到一个更大的结构体里面使用,此为驱动自定义的结构体,那就是它了,那么它的定义如下(删去了与分析无关的部分):
struct isp_device { struct v4l2_device v4l2_dev; struct v4l2_async_notifier notifier; struct media_device media_dev; ... ...}复制代码
其中与本节有关的就属于是 struct v4l2_device v4l2_dev
这个了,它将作为串联管理整个 omap3isp 的管理者的存在。
同时它的用法契合前面讲的「嵌入式」,struct isp_device
是驱动自定义的结构体,这个结构体可以看作是整个 omap3isp 的设备抽象化结构体,也就是说:它就代表了 omap3isp 这个大的设备。下面将看到它的用法。
注册 v4l2_device
在 isp.c 的 isp_probe
函数中有调用 isp_register_entities
函数,里面开头的内容大致如下:
... ... isp->v4l2_dev.mdev = &isp->media_dev; ret = v4l2_device_register(isp->dev, &isp->v4l2_dev); if (ret < 0) { dev_err(isp->dev, "%s: V4L2 device registration failed (%d)\n", __func__, ret); goto done; }... ...复制代码
其实可以看到注册函数里面并没有把高设备加入到一个链表里面什么的,而是初始化结构体成员,比如子设备链表头初始化,增加 dev 的引用等。在一切子设备被串联起来之前首先要初始化注册 v4l2_device
这个总的设备。
v4l2_subdev
该结构体是抽象化的子设备,用于子设备管理之用。它有「初始化」、「注册子设备」、「注册子设备节点」等几个操作。通常情况下「初始化」的函数实例被定义在一个个的子设备驱动模块内部,「注册子设备」与「注册子设备节点」这两个函数的实体被定义在父设备模块内部,比如: isp.c 中。
上代码(isp_probe
函数中):
ret = isp_initialize_modules(isp);if (ret < 0) goto error_iommu;ret = isp_register_entities(isp);if (ret < 0) goto error_modules;复制代码
下面首先看下 isp_initialize_modules
函数中的实现:
static int isp_initialize_modules(struct isp_device *isp){ int ret; ret = omap3isp_csiphy_init(isp); ret = omap3isp_csi2_init(isp);... ... ret = omap3isp_h3a_aewb_init(isp); ret = omap3isp_h3a_af_init(isp);... ... return 0;... ...复制代码
再进去 omap3isp_xxxx_init
函数里面就可以看到有类似 v4l2_subdev_init
的函数调用了,文章尽量少贴代码,多讲实现、使用机理,相信你一定能自己找到相关代码的。
所以结构大体上是这样子的:
- 你有一个输入设备 omap3isp,它的管理者列在一个单独的代码文件里面名为
isp.c
; - 定义一个自定义的抽象化结构体代表 omap3isp 这个设备,名为
isp_device
,并把v4l2_device
嵌入内部作为子设备管理工具; - 把子设备-类似 csi、preview、3a 等抽象化为一个个子设备,每个子设备一个代码文件,名为
ispxxx.c
,分别有自己的抽象化结构体,名为isp_xxx_dev
,内部嵌入了v4l2_subdev
作为子设备的抽象工具使用。同时实现自己的设备初始化函数,名为xxx_init_eneities
; - 在管理者
isp.c
的 probe 函数里面调用子设备的xxx_init_entities
,子设备初始化函数里面会做好v4l2_subdev
的初始化工作; - 管理者的 probe 函数里面注册
v4l2_device
,注册子设备,必要时注册子设备节点在用户空间生成/dev/nodeX
; - 大功告成,此时你就可以通过
v4l2_device
来管理所有的子设备了,框架本身提供了很好用的管理方式与相关的回调函数、结构体成员等等。
设备的管理
设备的管理必然需要主设备与子设备之间能够互联互通,否则的话谈何去管理,本节就介绍如何实现主设备与子设备之间的数据互联互通。
主设备子设备互通
通过上面的步骤建立了连接之后怎么从主设备找到子设备呢?如何从子设备找到主设备?如何从 v4l2_device
到自定义的主设备抽象结构体?如何从 v4l2_subdev
到子设备自定义的结构体?
- 如何从主设备找到子设备 首先需要获取
v4l2_device
结构体,然后可以使用list_for_each_entry
来对子设备进行遍历,其中子设备的结构体内部有一个name
成员,长度为32个字节,这个字段要求是整个v4l2_device
下属唯一的,所以要想找到某一个指定的子设备完全可以在遍历的时候对比子设备的name
字段看是不是自己想要找的。 - 如何从子设备找到主设备
v4l2_subdev
的结构体里面有一个v4l2_dev
的指针成员,该成员会在子设备被注册的时候指向v4l2_device
成员,注册函数为v4l2_device_register_subdev
。在该步骤执行完毕之后就可以通过获取子设备结构体内部的v4l2_dev
成员来获得主设备结构体。 - 如何从主设备到主设备实例化结构体 可以看到
v4l2_device
内部并没有什么私有指针之类的东西,那怎么去找到主设备的实例化结构体呢,此时可以通过另一种偏门方法获取,比如在定义结构体的时候把v4l2_device
放在结构体成员的第一个,之后通过v4l2_subdev
获取到v4l2_device
之后就可以把其地址强制转换为主设备自定义的实例化结构体来实现访问。 - 如何从子设备到子设备实例化结构体 子设备内部有两个私有的指针:
dev_priv
,host_priv
。前一个好理解也很好使用,使用的时候就调用v4l2_set_subdevdata
函数将dev_priv
指向子设备实例化结构体即可,然后就可以用v4l2_get_subdevdata
来从v4l2_subdev
获取到子设备结构体实例化的结构体数据了。后一个不是很好理解其用处,但是也可以通过v4l2_set_subdev_hostdata/v4l2_get_subdev_hostdata
来进行设置/获取,host 也即主控端,比如一个 camera sensor 的 SOC 端的控制器就可以作为主控端,再比如使用 I2C 进行通信的 camera sensor 的 SOC 端的 I2C 控制器就可以作为host_priv
,必要时通过 I2C 来控制子设备的行为。或者干脆把主设备实例化的结构体作为 host data 也可以。
主子设备信息交流
本节使用多个实际的用例来深入解释下各种信息交流方式与情景。比如:如何控制访问指定类型的子设备?子设备如何向主设备回返通知?
- 访问所有的 sensor 设备并关闭其数据流
- 子设备注册的时候应该要提供了相关的操作函数,那就是
v4l2_subdev_ops
这个结构体了,在此例中我们就仅仅设置其video
成员的s_stream
成员。 - 提供子设备组 id,也就是
v4l2_subdev
的grp_id
成员,此处我们设置为一个我自己假定的枚举类型(你只要保证这个枚举类型是整个v4l2_device
下属唯一的就行),我假定为OMAP3ISP_CAMSENSOR
。 - 初始化并注册子设备,就不再多说了,初始化以及注册的方式前面都有提到过了。
- 执行
v4l2_device_call_all(v4l2_device, OMAP3ISP_CAMSENSOR, video, s_stream, 0);
,此时会遍历挂在v4l2_device
名下的所有的OMAP3ISP_CAMSENSOR
组的子设备,调用其s_stream
的模块函数进行数据流的关闭。
- 子设备数据流关闭后向主设备回返通知
- 需要提供主设备的
notify
成员操作函数。 - 定义好
notification
的格式,比如我自己的定义,高8位表示哪个子设备,次8位表示哪种类型的操作(此处是 video 类型的 ops),再次8位表示具体的操作函数(s_stream),低8位表示操作值(0关闭)。 - 子设备调用
v4l2_subdev_notify
函数进行正式通知的发送,此时也可以带一些参数,只需要传递其地址就可以了,主子设备端商定好数据的格式即可。 - 主设备收到通知之后进行相关的操作。
交通枢纽 video_device
该结构体整合了数据流管理的终端模块功能,负责提供从内核空间到用户空间的数据交流,属于非常重要的一个功能了,必不可少的那种。
通常情况下所有的子设备都可以注册一个 video_device
结构体来在用户空间生成一个设备节点以供用户进行操作,但是区别在于只有负责真正传递视频数据的那个模块用得着注册 video 类型的设备节点名称(比如内核输入设备数据链的 DMA 数据终端),其它的使用 v4l-subdev 类型的就可以了。
video_device
只与 v4l2_device
进行绑定关联,通过后者这层关系可以访问到整个子设备网络的资源。
如何注册 video 类型节点
使用 video_register_device
配合 VFL_TYPE_GRABBER
参数进行注册,此时该函数执行完毕并返回的时候就可以在用户空间看到形如 /dev/videoX
的设备节点了。
注意需要提供其操作函数,类似下面的:
static struct v4l2_file_operations isp_video_fops = { .owner = THIS_MODULE, .unlocked_ioctl = video_ioctl2, .open = isp_video_open, .release = isp_video_release, .poll = isp_video_poll, .mmap = isp_video_mmap,};复制代码
关于其成员如何实现本节不详细介绍,在后面的 videobuf2 一文中会进行详细介绍。
如何注册其它类型节点
对于 v4l2 输入设备来说,使用 v4l2_device_register_subdev_nodes
来进行批量的设备节点注册,它内部依然会调用 video_register_device
函数,只不过会使用 VFL_TYPE_SUBDEV
类型来代替上面 VFL_TYPE_GRABBER
,那么在用户空间生成的设备节点名称就是 v4l-subdevX
了。
这种情况下注册的设备节点的操作函数是在 v4l2-device.c
里面定义好的默认操作函数,它的定义如下:
const struct v4l2_file_operations v4l2_subdev_fops = { .owner = THIS_MODULE, .open = subdev_open, .unlocked_ioctl = subdev_ioctl,#ifdef CONFIG_COMPAT .compat_ioctl32 = subdev_compat_ioctl32,#endif .release = subdev_close, .poll = subdev_poll,};复制代码
结束语
到这里为止,基本上关于设备这块的基础操作已经介绍完毕,至于还有一些比较高级的操作就放在后续的文章里面进行介绍了。预告一下,下一篇文章是讲 media framework 的,循序渐进。该篇文章读完并实践之后就可以在用户空间看到有 /dev/video
设备节点了,可以写一个小的测试用例,当打开设备节点的时候遍历一遍子设备把子设备的名字打出来,也可以增加更详细的信息,总之,本文的目的是实现一个 v4l2_device
管理框架下的设备拓扑,你可以把这个拓扑结构打印出来就算是完美完成任务了。
以防万一有滴同学不知道: 「RTFSC」:Read The Fxxking Source Code 「WTFSC」:Dao Li Tong Shang