【干货分享】Xilinx Linux V4L2视频管道(Video Pipeline)驱动程序分析

1. Author
作者:付汉杰,hankf@xilinx.com,文章转载自:赛灵思中文社区论坛

2. 概述
Xilinx提供了完整的V4L2的驱动程序,Xilinx V4L2 driver。处于最顶层的驱动程序是V4L2框架的视频管道(Video pipeline)驱动程序,也叫桥驱动程序(bridge driver),主要代码在文件xilinx-vipp.c中。在V4L2框架中,整个视频管道(Video pipeline)可以通过媒体设备(/dev/media)配置,流媒体可以通过视频设备(/dev/video)控制。这两种设备,都是在视频管道(Video pipeline)驱动程序里创建的。所以,理解V4L2的管道(pipeline)驱动程序是理解Xilinx所有Video IP 在Linux下工作情况的基础。

3. 文件

3.1. C文件

Xilinx的V4L2的管道(pipeline)驱动程序在下面四个文件中。
1. drivers\media\platform\xilinx\xilinx-vipp.c
2. drivers\media\platform\xilinx\xilinx-vipp.h
3. drivers\media\platform\xilinx\xilinx-dma.c
4. drivers\media\platform\xilinx\xilinx-dma.h

3.2.设备树(devicetree)

设备树(devicetree)里含有整个视频管道(video pipeline)的配置,对应的文档在Documentation/devicetree/bindings/media/xilinx/xlnx,video.txt。

下面是一个设备树(devicetree)的例子。

axi_video_cap {
compatible = "xlnx,axi-video";
dmas = , ;
dma-names = "port0", "port1";

ports {
#address-cells = ;
#size-cells = ;

port@0 {
reg = ;
direction = "input";
vcap0_in0: endpoint {
remote-endpoint = ;
};
};
port@1 {
reg = ;
direction = "input";
vcap0_in1: endpoint {
remote-endpoint = ;
};
};
};
};

3.3. 函数调用关系图

xvipp 函数调用关系图

xvipp 函数调用关系图

4.主要函数

4.1. 函数xvip_composite_probe()

函数xvip_composite_probe是整个驱动的入口,主要工作是初始化驱动的数据结构xvip_composite_device里的通用数据,包括lock、list(entities和dmas),再调用了xvip_composite_v4l2_init()和xvip_graph_init(),最后调用platform_set_drvdata设置平台设备platform_device里的当前设备的数据指针。

4.2. 函数xvip_composite_v4l2_init()

函数xvip_composite_v4l2_init做的事情比较简单,只是初始化了struct media_device,设置了设备版本和model名称、dev/mdev指针,就调用了v4l2_device_register()注册V4L2设备。

xvip_composite_v4l2_init的关键代码如下:
xdev->media_dev.dev = xdev->dev;
strlcpy(xdev->media_dev.model, "Xilinx Video Composite Device",
sizeof(xdev->media_dev.model));
xdev->media_dev.hw_revision = 0;
media_device_init(&xdev->media_dev);
xdev->v4l2_dev.mdev = &xdev->media_dev;
ret = v4l2_device_register(xdev->dev, &xdev->v4l2_dev);

4.3. 函数xvip_graph_init

函数xvip_graph_init是最重要的函数,函数调用层次也最深。它首先调用xvip_graph_dma_init根据设备树(devicetree)里的port信息初始化DMA通道,创建一个DMA的列表;再调用xvip_graph_parse在设备树(devicetree)里分析子设备节点,根据设备树里"remote-endpoint"属性创建一个Entity的列表;最后调用v4l2_async_notifier_register注册异步处理函数。系统发现各个子设备(subdev)后,调用异步处理函数xvip_graph_notify_bound获取子设备信息。所有子设备(subdev)都被发现后,调用xvip_graph_notify_complete,为每个entity创建Link和V4L2子设备,并注册media设备。

xvip_graph_init的关键代码如下:
/* Init the DMA channels. */
ret = xvip_graph_dma_init(xdev);

/* Parse the graph to extract a list of subdevice DT nodes. */
ret = xvip_graph_parse(xdev);

/* Register the subdevices notifier. */
num_subdevs = xdev->num_subdevs;
subdevs = devm_kcalloc(xdev->dev, num_subdevs, sizeof(*subdevs), GFP_KERNEL);

xdev->notifier.subdevs = subdevs;
xdev->notifier.num_subdevs = num_subdevs;
xdev->notifier.ops = &xvip_graph_notify_ops;
ret = v4l2_async_notifier_register(&xdev->v4l2_dev, &xdev->notifier);

4.4. 函数xvip_graph_dma_init

xvip_graph_dma_init()自身比较简单,先找到第一个"ports"子节点,再找其中的所有"port"节点,并为每个"port"子节点执行xvip_graph_dma_init_one(),从而将每个"port"子节点对应的DMA添加到链表dmas中。

xvip_graph_dma_init的关键代码如下:
ports = of_get_child_by_name(xdev->dev->of_node, "ports");

for_each_child_of_node(ports, port) {
ret = xvip_graph_dma_init_one(xdev, port);
}

4.5. 函数xvip_graph_dma_init_one

xvip_graph_dma_init_one()根据设备树(devicetree)的"port"子节点的配置,找到DMA,并添加到链表"xdev->dmas"中。
xvip_graph_dma_init_one先读取"port"子节点的属性"direction"和"reg"属性。Devicetre的"port"节点中,要含有属性“direction”和"reg"属性;如果没有属性“direction”,会返回错误;如果没有"reg"属性,代码会继续,但是功能会出错。属性"direction"的值是"input"或者"output"。
xvip_graph_dma_init_one还为每个port分配struct xvip_dma,再执行xvip_dma_init()。
接下来,xvip_graph_dma_init_one把struct xvip_dma加入到队列xdev->dmas。
xvip_graph_dma_init_one还根据"direction"的值是"input"或者"output",以及xvip_is_mplane的设置,选择buffer类型。

xvip_graph_dma_init_one的关键代码如下:
// Read direction and reg properties
ret = of_property_read_string(node, "direction", &direction);

of_property_read_u32(node, "reg", &index);

dma = devm_kzalloc(xdev->dev, sizeof(*dma), GFP_KERNEL);

ret = xvip_dma_init(xdev, dma, type, index);

list_add_tail(&dma->list, &xdev->dmas);

4.6. 函数xvip_dma_init

xvip_dma_init是核心的函数,完成了最关键的任务:初始化buffer队列、申请DMA设备、注册Video设备。
函数xvip_dma_init()先初始化struct xvip_dma的数据成员,包括dma->lock、dma->pipe.lock、dma->queued_bufs、dma->queued_lock;再根据buffer类型,初始化v4l2_format里的像数点格式,struct v4l2_pix_format pix 或者struct v4l2_pix_format_mplane pix_mp。接下来设置pad.flags为MEDIA_PAD_FL_SINK或者MEDIA_PAD_FL_SOURCE,调用media_entity_pads_init初始化初始化media entity。
函数xvip_dma_init()然后继续初始化video_device的各种成员和操作函数,包括fops、v4l2_dev、queue、vfl_type、vfl_dir、lock、和ioctl_ops。video_device的fops被设置为xvip_dma_fops,ioctl_ops被设置为xvip_dma_ioctl_ops。
函数xvip_dma_init()再接着初始化buffer队列struct vb2_queue queue,其中ops被设置为xvip_dma_queue_qops,mem_ops被设置为vb2_dma_contig_memops,执行vb2_queue_init。
函数xvip_dma_init()再接着执行dma_request_chan申请DMA设备,这是一个复杂和核心的函数。dma_request_chan()的第二个参数是DMA通道的名称,名字是格式是"port%u",比如"port0",也就是设备树的属性"dma-names"里的字符串。其中of_dma_request_slave_channel()调用的of_find_property(np, "dmas", NULL),通过属性"dmas",取得了dma。of_property_count_strings()计算属性"dma-names"里的字符串个数,也就是DMA的个数。然后根据DMA的个数,为每一个DMA执行of_dma_match_channel()。of_dma_match_channel (dev->of_node, "port%u", )取出"dma-names"的字符串,再和"port%u"对比,这是检查名字是否对应,如果一致,就用对应的DMA,取得struct of_phandle_args dma_spec,再把struct of_phandle_args dma_spec转换为struct of_dma *ofdma。of_dma_xlate接下来转换为struct dma_chan。of_dma_request_slave_channel使用了字符串"dmas"。of_dma_match_channel()使用了字符串"dmas"和"dma-names"。所以Xilinx-vipp.c实现的"xlnx,video"设备的设备树里的"dmas"和"dma-names","port0"是必须有的固定字符串,不能更改;只有dmas后面的DMA phandle才可以更改。
函数xvip_dma_init()最后执行video_register_device注册Video设备。

4.7. 函数xvip_graph_parse

函数xvip_graph_parse先调用xvip_graph_parse_one,找到直接的remote_port;然后再为每一个找到的remote_port执行一次xvip_graph_parse_one。

4.8. 函数xvip_graph_parse_one

xvip_graph_parse_one()里先调用of_graph_get_next_endpoint(node, ep)取得下一个endpoint,然后再调用of_graph_get_remote_port_parent(ep) 得到remote_port的父节点。of_graph_get_next_endpoint(node, ep)会先尝试找子节点ports,再找到子节点port,然后再在子节点port里找endpoint;读取路径是 { ports { port {endpoint } } },其中ports是可选的。endpoint是port的子节点,不管其内部的属性名称,所以也可以用属性名称remote-endpoint。of_graph_get_remote_port_parent(ep)通过调用of_graph_get_remote_endpoint()得到remote_port,再调用of_graph_get_port_parent()得到父节点。of_graph_get_remote_endpoint()里读取了设备树里的"remote-endpoint"属性。
xvip_graph_parse_one()再调用xvip_graph_find_entity(),在xdev->entities里检查是否已经包含对应的entity,如果已经包含,则跳过后续处理;如果没没有,则找到的remote_port的父节点,存放到链表xdev->entities,并执行操作xdev->num_subdevs++。
最后xdev->entities里包含了所有entity。后来xvip_graph_notify_complet会根据xdev->entities,为每个entity创建一个设备。

4.9. 函数xvip_graph_notify_bound

系统发现子设备(subdev)后,调用异步处理函数xvip_graph_notify_bound获取子设备信息。xvip_graph_notify_bound根据设备树节点,匹配实体(entity)和子设备(subdev);匹配成功后,再把子设备(subdev)的指针保存到实体(entity)。

xvip_graph_notify_bound的关键代码如下:
entity->entity = &subdev->entity;
entity->subdev = subdev;

4.10. 函数xvip_graph_notify_complete

系统所有子设备(subdev)都被发现后,Linux会调用xvip_graph_notify_complete。xvip_graph_notify_complete调用xvip_graph_build_one为每个entity创建Link;调用xvip_graph_build_dma做Create links for DMA channels;并为每个实体(entity)注册V4L2子设备v4l2_device_register_subdev_nodes ;最后调用media_device_register注册media设备。

4.11. 函数xvip_graph_build_one

函数xvip_graph_build_dma在每个entity及其远端模块之间创建Link。
函数xvip_graph_build_one先调用函数of_graph_get_next_endpoint从设备的设备树里找到下一个endpoint(struct device_node)。与之前描述一样,of_graph_get_next_endpoint的读取路径是 { ports { port {endpoint} } },endpoint是port的子节点,不管其内部的属性名称,所以也可以用属性名称remote-endpoint。函数xvip_graph_build_dma接着调用v4l2_fwnode_parse_link,得到对应的link(struct v4l2_fwnode_link)。接着根据link中的端口号(local_port)取得pad信息,判断出是否是目的端(sink)端口;如果是目的端(sink)端口,则忽略。另外还忽略DMA,它由xvip_graph_build_dma处理。接下来再调用函数xvip_graph_find_entity,根据link中的远端节点(remote_node),得到远端的entity。最后调用media_create_pad_link,当前entity作为源端(source),远端entity作为目的端(sink)的media_entity和media_pad,创建媒体pad的链接(link)

4.12. 函数xvip_graph_build_dma

函数xvip_graph_build_dma为DMA模块及其远端模块之间创建Link。
函数xvip_graph_build_dma先调用函数of_graph_get_next_endpoint从设备的设备树里找到下一个endpoint(struct device_node)。函数xvip_graph_build_dma接着调用v4l2_fwnode_parse_link,得到对应的link(struct v4l2_fwnode_link),根据link中的端口号(local_port),找到对应的DMA(struct xvip_dma)。接下来再调用函数xvip_graph_find_entity,根据link中的远端节点(remote_node),得到远端的entity。然后设置源端(source)/目的端(sink)的media_entity和media_pad,最后调用media_create_pad_link创建媒体pad的链接(link)。

4.13. 函数xvip_graph_find_dma

函数xvip_graph_build_dma根据指定的port号,在xdev->dmas里找DMA,如果找到,就返回对应的的struct xvip_dma的指针。

4.14. 函数xvip_graph_find_entity

函数xvip_graph_find_entity根据指定的设备节点(struct device_node),在xdev->entities里找entity,如果找到,就返回对应的的struct xvip_graph_entity的指针。

5. 关键数据

5.1. subdevs

其中subdevs是一个指针,指向(struct v4l2_async_subdev *)的数组(是指针数组),包含了这个设备下的所有subdev的指针,根据遍历xdev->entities填满这个数组。xdev->entities由processedxvip_graph_parse_one()根据设备树找到相关的子设备填充。

5.2. group ID

组ID(struct v4l2_subdev里有成员grp_id。v4l2_subdev_init()里把其初始化为0。Xilinx没有设置grp_id,所以都是0.)

5.3. xvip_graph_notify_ops

xvip_graph_notify_ops指向bound和complete函数,如果成功匹配设备,.bound()回调函数将会被调用,当所有的子设备全部被加载完毕之后,.complete() 回调函数就会被调用:

6. 后续任务

还有很多代码可以分析,比如HDMI RX的驱动、TPG的驱动、m2m的驱动。

推荐阅读