前言
本文主要讲的是NXP的imx8mm,源码是由NXP提供的,不同的下游厂家的开发板也应该是一样的。
引入异步注册的原因
在SOC中的视频处理可能由多个IP组成,比如csi_bridge、csi_mipi接口、具体的sensor(ov5640等),甚至更多的IP,这样就导致了V4L2的复杂性。在v4l2中的视频数据流是有方向和顺序的,在linux中引入了异步注册的机制来解决这个问题。v4l2的bridge驱动需要在所有的子设备都已经加载后,再初始化所有的子设备(比如:stream on)。异步注册的核心在于设备树中引入port接口,在子设备中有一个或者多个port接口,port接口就是子设备的纽带。
v4l2设备注册
在imx8mm中包含了三个设备
csi1_bridge :这个是SOC端的总的外设,它是和mipi控制器连接的。
mipi_csi :mipi传输的控制器,它是连接csi1_bridge 和ov5640的通道。
ov5640:外接的摄像头,是一个标准的sensor。
连接方式:csi1_bridge -> mipi_csi_1-> ov5640
其中csi1_bridge不是子设备,另外两个mipi_csi 、ov5640在系统中是子设备。
设备树
ov5640_mipi: ov5640_mipi@3c {
compatible = "ovti,ov5640_mipi";
...
port {
ov5640_mipi1_ep: endpoint {
remote-endpoint = <&mipi1_sensor_ep>;
};
};
};
&mipi_csi_1 {
#address-cells = <1>;
#size-cells = <0>;
status = "okay";
port {
mipi1_sensor_ep: endpoint1 {
remote-endpoint = <&ov5640_mipi1_ep>;
data-lanes = <2>;
csis-hs-settle = <10>;
csis-clk-settle = <0>;
csis-wclk;
};
csi1_mipi_ep: endpoint2 {
remote-endpoint = <&csi1_ep>;
};
};
};
&csi1_bridge {
fsl,mipi-mode;
status = "okay";
port {
csi1_ep: endpoint {
remote-endpoint = <&csi1_mipi_ep>;
};
};
};
从上面的设备代码片段中可以看出:csi1_bridge 中的endpoint 连接到了mipi_csi_1 的endpoint2 ,mipi_csi_1 的endpoint1 连接到了ov5640_mipi的endpoint 。下面讲到的设备注册顺序就是根据这里来的。
所有设备注册顺序
请仔细看下面代码和注释,下面虽然只有5个函数,但却是理解v4l2中子设备注册过程的核心!
mx6s_capture.c:
//步骤1:注册了一个视频设备。
- video_register_device()
//步骤2:在notifier中注册了mipi_csi对应的异步子设备。
- mx6sx_register_subdevs(csi_dev)
-> v4l2_async_notifier_register();
mxc_mipi_csi.c:
//步骤3:首先注册了和上面匹配的子设备。
- mipi_csis_subdev_init(&state->mipi_sd, pdev, &mipi_csis_subdev_ops)
-> v4l2_async_register_subdev();
//步骤4:在notifier中注册了ov5640对应的异步子设备。
- mipi_csis_subdev_host(state) -> v4l2_async_notifier_register();
ov5640.c:
//步骤5:只注册了在上面noifier中对应的ov5640子设备。
- v4l2_async_register_subdev(&sensor->sd);
其中,步骤2(asd)和步骤3(sd)是对应的,步骤4(asd)和步骤5(sd)是对应的,并且步骤3和步骤4是联系在一起的。可以先看下小结部分解释再回来看这里,需要补充下:v4l2_async_notifier_register()和v4l2_async_register_subdev实际上并真正注册设备,只有在它们两个注册的设备信息匹配后,才会调用v4l2_device_register_subdev()函数向系统真正注册子设备。可以想象一下,在上面的步骤中,不管是哪个步骤先执行,或者哪个步骤后执行,最终会形成csi1_bridge -> mipi_csi_1-> ov5640这样的连接方式,这就是我们想要的。
子设备注册
重要的数据结构
子设备数据结构:这里只列举出了和异步注册相关的内容。
struct v4l2_subdev {
...
/* 子设备链表 */
struct list_head list;
struct v4l2_device *v4l2_dev;
struct list_head async_list;
struct v4l2_async_subdev *asd;
struct v4l2_async_notifier *notifier;
};
异步通知相关数据结构:
struct v4l2_async_notifier {
unsigned int num_subdevs;
struct v4l2_async_subdev **subdevs;
struct v4l2_device *v4l2_dev;
/* 通知链表的3种状态 */
struct list_head waiting;
struct list_head done;
struct list_head list;
int (*bound)(struct v4l2_async_notifier *notifier,
struct v4l2_subdev *subdev,
struct v4l2_async_subdev *asd);
...
};
驱动中定义的大涉及到整个驱动的数据结构。
struct mx6s_csi_dev {
struct device *dev;
struct video_device *vdev;
struct v4l2_subdev *sd;
struct v4l2_device v4l2_dev;
/* 核心结构体 */
struct vb2_queue vb2_vidq; //video queue
struct v4l2_ctrl_handler ctrl_handler;
/* 维护了3种链表 */
struct list_head capture;
struct list_head active_bufs;
struct list_head discard;
struct v4l2_async_subdev asd;
struct v4l2_async_notifier subdev_notifier;
struct v4l2_async_subdev *async_subdevs[2];
...
};
重要函数
主驱动中在notifier上注册一个异步子设备。
在csi1_bridge驱动中调用了下面的函数来注册mipi_csi异步子设备:
/* 1.遍历获取所有的endpoint
* 2.将所有的endpoint转换成async_subdev,并添加到notifier
*/
static int mx6sx_register_subdevs(struct mx6s_csi_dev *csi_dev)
{
struct device_node *parent = csi_dev->dev->of_node;
struct device_node *node, *port, *rem;
/* 只解析当前设备的节点 */
/* Attach sensors linked to csi receivers */
for_each_available_child_of_node(parent, node) {
/* The csi node can have only port subnode. */
port = of_get_next_child(node, NULL);
/* 获取和本地port关联的远程port的节点
* rem: remote 的缩写
*/
rem = of_graph_get_remote_port_parent(port);
csi_dev->asd.match_type = V4L2_ASYNC_MATCH_FWNODE;
/* rem 这里代表的mipi_csi */
csi_dev->asd.match.fwnode.fwnode = of_fwnode_handle(rem);
/* csi_dev->asd是上面才添加的信息,也是上面代码段的主要目的 */
csi_dev->async_subdevs[0] = &csi_dev->asd;
}
/* 将上面代码段中获取到的信息填充到notefier中 */
csi_dev->subdev_notifier.subdevs = csi_dev->async_subdevs;
csi_dev->subdev_notifier.num_subdevs = 1;
csi_dev->subdev_notifier.bound = subdev_notifier_bound;
/* 注册notifier */
ret = v4l2_async_notifier_register(&csi_dev->v4l2_dev,
&csi_dev->subdev_notifier);
return ret;
}
下面的函数是异步通知链的核心函数。
int v4l2_async_notifier_register(struct v4l2_device *v4l2_dev,
struct v4l2_async_notifier *notifier)
{
struct v4l2_subdev *sd, *tmp;
struct v4l2_async_subdev *asd;
notifier->v4l2_dev = v4l2_dev;
for (i = 0; i < notifier->num_subdevs; i++) {
asd = notifier->subdevs[i];
switch (asd->match_type) {
case V4L2_ASYNC_MATCH_CUSTOM:
case V4L2_ASYNC_MATCH_DEVNAME:
case V4L2_ASYNC_MATCH_I2C:
case V4L2_ASYNC_MATCH_FWNODE:
break;
default:
return -EINVAL;
}
/* 将asd的list添加到notifier的waiting中 */
list_add_tail(&asd->list, ¬ifier->waiting);
}
/* 在subdev_list上面遍历查找sub_dev */
list_for_each_entry_safe(sd, tmp, &subdev_list, async_list) {
asd = v4l2_async_belongs(notifier, sd);
/* 如果sd和asd是匹配的,将执行下面的函数 */
ret = v4l2_async_test_notify(notifier, sd, asd);
}
/* Keep also completed notifiers on the list */
/* notifier维护有3个链表,如果没有成功将对应的链表添加到notifier_list中 */
list_add(¬ifier->list, ¬ifier_list);
}
下面的函数主要调用了子设备注册函数和一些链表的操作。
static int v4l2_async_test_notify(struct v4l2_async_notifier *notifier,
struct v4l2_subdev *sd,
struct v4l2_async_subdev *asd)
{
/* 如果提供了bound函数,则调用此函数 */
if (notifier->bound) {
ret = notifier->bound(notifier, sd, asd);
}
/* 在V4L2_dev设备上注册子设备 */
ret = v4l2_device_register_subdev(notifier->v4l2_dev, sd);
/* Remove from the waiting list */
/* 执行到这里的时候已经匹配注册成功,需要在链表上删除 */
list_del(&asd->list);
sd->asd = asd;
sd->notifier = notifier;
/* Move from the global subdevice list to notifier's done */
/* 移动到notifier->done链表中 */
list_move(&sd->async_list, ¬ifier->done);
...
}
下面是实际的注册函数,主要是注册调用了一些操作相关的函数。
/* 将子设备添加到v4l2设备的子设备链表上 */
int v4l2_device_register_subdev(struct v4l2_device *v4l2_dev,
struct v4l2_subdev *sd)
{
...
err = v4l2_ctrl_add_handler(v4l2_dev->ctrl_handler, sd->ctrl_handler, NULL);
err = media_device_register_entity(v4l2_dev->mdev, entity);
err = sd->internal_ops->registered(sd);
...
/* 添加到链表上 */
list_add_tail(&sd->list, &v4l2_dev->subdevs);
}
小结:上面4个函数就完成了根据设备树在notifier上注册异步子设备的过程。
驱动注册子设备
/* init subdev */
static int mipi_csis_subdev_init(struct v4l2_subdev *mipi_sd,
struct platform_device *pdev,
const struct v4l2_subdev_ops *ops)
{
struct csi_state *state = platform_get_drvdata(pdev);
int ret = 0;
v4l2_subdev_init(mipi_sd, ops);
mipi_sd->owner = THIS_MODULE;
snprintf(mipi_sd->name, sizeof(mipi_sd->name), "%s.%d",
CSIS_SUBDEV_NAME, state->index);
mipi_sd->flags |= V4L2_SUBDEV_FL_HAS_DEVNODE;
mipi_sd->dev = &pdev->dev;
state->csis_fmt = &mipi_csis_formats[0];
state->format.code = mipi_csis_formats[0].code;
state->format.width = MIPI_CSIS_DEF_PIX_WIDTH;
state->format.height = MIPI_CSIS_DEF_PIX_HEIGHT;
/* This allows to retrieve the platform device id by the host driver */
v4l2_set_subdevdata(mipi_sd, pdev);
/* 异步注册子设备 */
ret = v4l2_async_register_subdev(mipi_sd);
...
}
下面是子设备端的注册函数。
int v4l2_async_register_subdev(struct v4l2_subdev *sd)
{
struct v4l2_async_notifier *notifier;
...
INIT_LIST_HEAD(&sd->async_list);
/* 在notifier_list上遍历查找notifier,list包含有notifier上所有的成员 */
list_for_each_entry(notifier, ¬ifier_list, list) {
struct v4l2_async_subdev *asd = v4l2_async_belongs(notifier, sd);
if (asd) {
int ret = v4l2_async_test_notify(notifier, sd, asd);
mutex_unlock(&list_lock);
return ret;
}
}
/* 将对应的链表添加到subdev_list中 */
/* None matched, wait for hot-plugging */
list_add(&sd->async_list, &subdev_list);
...
}
后续也是调用
– v4l2_async_belongs(notifier, sd);
– v4l2_async_test_notify(notifier, sd, asd);
– v4l2_device_register_subdev(notifier->v4l2_dev, sd);
– list_add(&sd->async_list, &subdev_list);
小结:
视频异步子设备注册可以分为两个部分,一部分为notifier管理的v4l2_async_subdev,另外一部分为子设备驱动的v4l2_subdev。notifier中的异步子设备都是根据设备树信息注册的。然后子设备驱动再注册自己对应的子设备。上面所涉及到的主要区别在于v4l2_async_notifier_register()和v4l2_async_register_subdev()函数。这两个函数需要仔细去分析下,原理和linux驱动中的其它总线和设备匹配非常相似。一般来说视频驱动会先解析设备树获取子设备信息后,再通过v4l2_async_notifier_register()来注册到notifier,将还没有匹配的子设备添加到waiting链表上。然后调用notifier->bound()函数来进行匹配,如果匹配成功,将调用v4l2_device_register_subdev()将subdev添加到V4L2的子设备链表上。最后调用notifier->complete()回调函数。如果匹配不成功,则将添加到v4l2自己维护的通知链表notifier_list。另一方面,在驱动中会调用v4l2_async_register_subdev()来注册子设备,将调用notifier->bound()函数来进行匹配,如果匹配成功,将调用v4l2_device_register_subdev()将subdev添加到V4L2的子设备链表上。最后调用notifier->complete()回调函数。如果匹配不成功,会将设备添加到v4l2自己维护的子设备链表subdev_list是上。
调用过程
在设备注册完成后,V4L2设备的调用过程举例如下:
相关内容阅读
链接: V4L2框架
链接: V4L2之设备注册
链接: V4L2之mmap()函数
链接: V4L2之events
链接: V4L2之buffer分配和映射