Media与Subdev子系统

1. 从一个摄像头说起

在十几年前,一个摄像头可能就是一个简单的 USB 设备,内部有各类Unit或Terminal,这些Unit或Terminal的独立性不强,并且它们遵守UVC规范,没必要单独为它们编写驱动程序。在UVC驱动程序里,每一个Unit或Terminal都有一个struct uvc_entity结构体,每一个struct uvc_entity里面也都有一个struct v4l2_subdev (drivers/media/usb/uvc/uvcvideo.h)

/* 文件来自 */
struct uvc_entity {
	struct list_head list;		/* Entity as part of a UVC device. */
	struct list_head chain;		/* Entity as part of a video device
					 * chain. */
	unsigned int flags;

	u8 id;
	u16 type;
	char name[64];

	/* Media controller-related fields. */
	struct video_device *vdev;
	struct v4l2_subdev subdev;
	/* …………………… */
};

但是,这些subdev没有什么作用,它们的ops函数都是空的(drivers/media/usb/uvc/uvc_entity.c):

static const struct v4l2_subdev_ops uvc_subdev_ops = {
};

static int uvc_mc_init_entity(struct uvc_video_chain *chain,
			      struct uvc_entity *entity)
{
	int ret;

	if (UVC_ENTITY_TYPE(entity) != UVC_TT_STREAMING) {
		u32 function;

		v4l2_subdev_init(&entity->subdev, &uvc_subdev_ops);
		strlcpy(entity->subdev.name, entity->name,
			sizeof(entity->subdev.name));

		switch (UVC_ENTITY_TYPE(entity)) {
		case UVC_VC_SELECTOR_UNIT:
			function = MEDIA_ENT_F_VID_MUX;
			break;
		case UVC_VC_PROCESSING_UNIT:
		case UVC_VC_EXTENSION_UNIT:
			/* For lack of a better option. */
			function = MEDIA_ENT_F_PROC_VIDEO_PIXEL_FORMATTER;
			break;
		case UVC_COMPOSITE_CONNECTOR:
		case UVC_COMPONENT_CONNECTOR:
			function = MEDIA_ENT_F_CONN_COMPOSITE;
			break;
		case UVC_SVIDEO_CONNECTOR:
			function = MEDIA_ENT_F_CONN_SVIDEO;
			break;
		case UVC_ITT_CAMERA:
			function = MEDIA_ENT_F_CAM_SENSOR;
			break;
		case UVC_TT_VENDOR_SPECIFIC:
		case UVC_ITT_VENDOR_SPECIFIC:
		case UVC_ITT_MEDIA_TRANSPORT_INPUT:
		case UVC_OTT_VENDOR_SPECIFIC:
		case UVC_OTT_DISPLAY:
		case UVC_OTT_MEDIA_TRANSPORT_OUTPUT:
		case UVC_EXTERNAL_VENDOR_SPECIFIC:
		default:
			function = MEDIA_ENT_F_V4L2_SUBDEV_UNKNOWN;
			break;
		}

		entity->subdev.entity.function = function;

		ret = media_entity_pads_init(&entity->subdev.entity,
					entity->num_pads, entity->pads);

		if (ret < 0)
			return ret;

		ret = v4l2_device_register_subdev(&chain->dev->vdev,
						  &entity->subdev);
	} else if (entity->vdev != NULL) {
		ret = media_entity_pads_init(&entity->vdev->entity,
					entity->num_pads, entity->pads);
		if (entity->flags & UVC_ENTITY_FLAG_DEFAULT)
			entity->vdev->entity.flags |= MEDIA_ENT_FL_DEFAULT;
	} else
		ret = 0;

	return ret;
}

但现在的摄像头,比如手机里的,其实是一个高度复杂的系统,它至少包含:

  • 图像传感器 (Sensor): 捕捉光线,转换成最原始的电子信号。
  • 图像信号处理器 (ISP): 对原始信号进行加工,比如降噪、调整色彩和亮度,让照片更好看。
  • 镜头马达控制器: 负责自动对焦。
  • 闪光灯控制器: 负责补光。
  • 数据接口 (CSI): 负责把处理好的图像数据高速传输给 CPU。

这些部件可能来自不同的厂商,通过各种总线(I2C, MIPI CSI-2)连接在一起。现在问题来了:Linux 内核要如何高效、灵活地管理这么一个“硬件联合国”呢?

如果还用老办法,为整个摄像头系统写一个巨大无比的驱动,那将是一场灾难。驱动会变得极难维护,而且无法重用——换一个 Sensor 芯片,可能大半个驱动都要重写。

为了解决这个问题,Linux 内核引入了一套全新的框架——Media Controller Framework

2. 第一部分:核心思想 —— “化整为零,分而治之”

Media Framework 的核心思想非常简单,就是我们熟悉的“分而治之”。

  1. 化整为零: 首先,它把一个复杂的视频设备,按照硬件功能,拆分成一个个独立的、最小的功能单元。每一个这样的单元,在内核里就被称为 subdev (子设备)

    • Sensor 芯片是一个 subdev
    • ISP 是一个 subdev
    • CSI 接口控制器也是一个 subdev

    这样做的好处是,每个 subdev 的驱动都变得非常简单,只用关心自己控制的那一小块硬件。而且,这些驱动可以被轻松重用。比如 A 厂商和 B 厂商的摄像头都用了同一款 Sensor 芯片,那么这个 Sensor 的 subdev 驱动就可以不做任何修改,在两个产品上通用。

  2. 分而治之 (Conquer): 拆开之后,就需要一个“总指挥”把它们重新组织起来,协同工作。这个总指挥就是 Media 子系统。它负责管理所有的 subdev,并根据需要,像搭建乐高积木一样,把它们连接起来,形成一条完整的数据处理路径。

我们可以把这个过程想象成组装一套家庭影院:播放器、功放、音响、投影仪都是独立的设备(相当于 subdev),而你就是那个“总指挥”(相当于 Media 子系统),用各种连接线把它们正确地连接起来,最终才能欣赏电影。

3. 硬件蓝图 —— 用 Entity, Pad, Link 描绘世界

Media 子系统这位“总指挥”是如何知道手下有哪些 subdev,以及它们之间该如何连接的呢?它需要一张“硬件蓝图”。这张蓝图由三个核心元素构成:

  • Entity (实体): 每一个 subdev 在蓝图上就是一个 entity。它代表了一个独立的功能模块。
  • Pad (端口): Entity 之间需要传输数据,pad 就是数据的出入口。每个 entity 都可以有多个输入或输出端口。
  • Link (链接): Link 就是连接两个 pad 的“线”,它定义了数据可能流动的方向。

我们来看一张真实的设备蓝图(拓扑图),这比文字描述要直观得多:
在这里插入图片描述
这张图告诉我们:

  • 图中每一个方框都是一个 entity

    • 绿色方框 (V4L2 Sub-device)是 V4L2 子设备,对应路径为 /dev/v4l-subdev*。是内部的、核心的图像处理单元,通常不能直接被应用程序用来读写视频流,而是作为管道的一部分来配置。方框内的小数字代表该节点的连接点(Pad),箭头表示数据流动的方向。
    • 黄色方框 (V4L2 Video Node):这些是 V4L2 视频设备节点,对应路径为 /dev/video*。这些是应用程序可以直接交互的接口,用来捕获视频帧、输出统计数据或读入数据
  • 连接 pad 之间的黑线就是 link (链接)

    • 黑色实线 (Solid Black Line)代表:直接、活动的标准数据链路 (Direct/Active Data Link)
      • 这是媒体管道中最标准的连接方式,表示一个处理单元(sub-device)的输出端(source pad)和另一个处理单元的输入端(sink pad)之间的直接、永久性连接。
      • 当管道运行时,视频数据帧会持续不断地通过这些实线从一个节点流向下一个节点。
      • ·可以将其理解为数据流的主干道。例如,从 rockchip-csi2-dphy1rkisp-csi-subdev 再到 rkisp-isp-subdev 的实线,构成了从摄像头传感器捕获实时画面的核心路径。
    • 黑色虚线 (Dashed Black Line)代表:间接或可选的数据链路 (Indirect or Optional Data Link)
      • 虚线通常表示一个非默认、可选的或可切换的连接。
      • 在这张图中,虚线从 rkisp_rawrd0_m /dev/video5 连接到 rkisp-isp-subdev 的输入端。请注意,这个输入端(Pad 0)已经有一条来自 rkisp-csi-subdev 的实线连接了。
      • 这里的含义是:ISP 处理单元的输入源不唯一
        • 默认情况(实线):它处理来自摄像头的实时数据流。
        • 可选情况(虚线):它可以被配置为不处理实时数据,而是从 /dev/video5 这个设备节点读取数据。rawrd (Raw Reader) 的名字也暗示了它的功能——从内存中读取之前保存的 RAW 格式图像,然后送回 ISP 进行处理。这在调试算法或对已捕获的图像进行后期处理时非常有用。

4. 搭建流水线 (Pipeline) —— 从静态蓝图到动态数据流

有了蓝图,我们就可以开始搭建一条真正的数据处理流水线了。Pipeline (流水线) 指的是在某一时刻,被激活的一条完整的数据处理路径。我们来看一个更具体的例子,它展示了物理硬件是如何被抽象成 entity 并连接成 pipeline 的:

1. 硬件与 Entity 的映射

  • 物理硬件 (上图): 数据从“摄像头传感器”发出,经过“CSI-2 发射器”,再由主控的“CSI-2 接收器”接收,然后“格式解析”,最后送入“ISP0”处理。

  • 逻辑抽象 (下图): Media 子系统将这些物理模块一一对应地抽象成了 sensor_subdev, csi_subdev, parser_subdev, isp0_subdeventity
    在这里插入图片描述
    2. Pipeline 的建立与数据流动
    图中的 link 清晰地展示了数据流动的逻辑路径:sensor->csi->parser->isp0
    这条由多个 link 串联起来的路径,就构成了一条完整的 pipeline
    这条 pipeline 并不是天生就激活的,而是由上层应用程序(比如相机 APP)来启动的。过程如下:

    1. 应用发出指令: 相机 APP 通过 Linux 的 ioctl 系统调用,告诉 Media 子系统:“我要激活从 sensorisp0 的这条路径!”
    2. Media 子系统调度: “总指挥”收到命令,沿着这条路径,依次通知路径上的每一个 subdev:“准备开始工作了!”
    3. Subdev 配置硬件: 每个 subdev 驱动收到通知后,就会去配置自己对应的物理硬件。比如 sensor_subdev 会通过 I2C 命令启动 Sensor 芯片。
    4. 数据开始流动: 一旦整条路径上的硬件都准备就绪,图像数据就开始在物理层面流动:Sensor 捕捉的图像信号,经过 CSI 接口,被 ISP 接收并处理。
    5. 应用获取数据: 最终,处理好的图像数据被放入内存,APP 从中读取,我们就看到了拍摄的画面。

    通过这个过程可以看到,Media 子系统主要负责的是“控制流”,它搭建通路、下发命令。而真正的“数据流”则是在硬件层面自行流动的

5. 第四部分:深入驱动 —— Subdev 如何响应命令?

我们已经知道 Media 子系统会向 subdev 下发命令,那么 subdev 驱动是如何接收并执行这些命令的呢?答案就在 v4l2_subdev_ops 这个结构体里。

  • struct v4l2_subdev: 可以看作是 subdev 的“身份证”,包含了它的名字等基本信息。
    在这里插入图片描述

  • struct v4l2_subdev_ops: 这是 subdev 的“操作手册”,里面全是函数指针,定义了它能做什么。

    里面有各类ops结构体,比如core、tuner、audio、video等等。编写subdev驱动程序时,核心就是实现各类ops结构体的函数。(drivers/media/i2c/ov5640.c):

    static const struct v4l2_subdev_core_ops ov5640_core_ops = {
    	.s_power = ov5640_s_power,
    };
    
    static const struct v4l2_subdev_video_ops ov5640_video_ops = {
    	.g_frame_interval = ov5640_g_frame_interval,
    	.s_frame_interval = ov5640_s_frame_interval,
    	.s_stream = ov5640_s_stream,
    };
    
    static const struct v4l2_subdev_pad_ops ov5640_pad_ops = {
    	.enum_mbus_code = ov5640_enum_mbus_code,
    	.get_fmt = ov5640_get_fmt,
    	.set_fmt = ov5640_set_fmt,
    	.enum_frame_size = ov5640_enum_frame_size,
    	.enum_frame_interval = ov5640_enum_frame_interval,
    };
    
    static const struct v4l2_subdev_ops ov5640_subdev_ops = {
    	.core = &ov5640_core_ops,
    	.video = &ov5640_video_ops,
    	.pad = &ov5640_pad_ops,
    };
    

    当 Media 子系统需要 subdev 做某件事时,就会调用这个“操作手册”里对应的函数。例如:

  • 需要启动视频流时,调用 .video 分类下的 s_stream() 函数。

  • 需要设置图像格式时,调用 .video 分类下的 s_fmt() 函数。

  • 最关键的,当应用程序要激活一条 link 时,Media 子系统会调用 .pad 分类下的 link_setup() 函数。

我们来看一下 link_setup 的幕后工作流程,当 APP 请求激活一个 link 时:

  1. 内核找到 link 连接的源 entity 和目标 entity
  2. 内核找到对应的 pad
  3. 内核调用 __media_entity_setup_link_notify,这个函数最终会分别调用源 entity 和目标 entitylink_setup 函数 (第 3.2 步)

这样,subdev 驱动就得到了配置硬件的机会,从而完成了 link 的激活。

总结

现在,我们回顾一下:

  1. 为什么需要 Media Framework? 因为现代视频设备太复杂,需要一种“化整为零,分而治之”的管理方法。
  2. 核心概念是什么?
    • subdev:被拆分出来的、独立的硬件功能单元。
    • media 子系统:管理和连接 subdev 的“总指挥”。
  3. 它是如何工作的?
    • 通过 entity, pad, link 三个元素绘制出一张静态的“硬件蓝图”。
    • 应用程序根据蓝图,请求激活一条或多条 link,从而搭建出一条动态的 pipeline
    • subdev 驱动通过实现 v4l2_subdev_ops 里的函数,来响应“总指挥”的命令,完成最终的硬件配置。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值