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 的核心思想非常简单,就是我们熟悉的“分而治之”。
-
化整为零: 首先,它把一个复杂的视频设备,按照硬件功能,拆分成一个个独立的、最小的功能单元。每一个这样的单元,在内核里就被称为
subdev(子设备)。- Sensor 芯片是一个
subdev。 - ISP 是一个
subdev。 - CSI 接口控制器也是一个
subdev。 - …
这样做的好处是,每个
subdev的驱动都变得非常简单,只用关心自己控制的那一小块硬件。而且,这些驱动可以被轻松重用。比如 A 厂商和 B 厂商的摄像头都用了同一款 Sensor 芯片,那么这个 Sensor 的subdev驱动就可以不做任何修改,在两个产品上通用。 - Sensor 芯片是一个
-
分而治之 (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*。这些是应用程序可以直接交互的接口,用来捕获视频帧、输出统计数据或读入数据
- 绿色方框 (V4L2 Sub-device)是 V4L2 子设备,对应路径为
-
连接
pad之间的黑线就是link(链接)- 黑色实线 (Solid Black Line)代表:直接、活动的标准数据链路 (Direct/Active Data Link)
- 这是媒体管道中最标准的连接方式,表示一个处理单元(sub-device)的输出端(source pad)和另一个处理单元的输入端(sink pad)之间的直接、永久性连接。
- 当管道运行时,视频数据帧会持续不断地通过这些实线从一个节点流向下一个节点。
- ·可以将其理解为数据流的主干道。例如,从
rockchip-csi2-dphy1到rkisp-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 进行处理。这在调试算法或对已捕获的图像进行后期处理时非常有用。
- 黑色实线 (Solid Black Line)代表:直接、活动的标准数据链路 (Direct/Active Data Link)
4. 搭建流水线 (Pipeline) —— 从静态蓝图到动态数据流
有了蓝图,我们就可以开始搭建一条真正的数据处理流水线了。Pipeline (流水线) 指的是在某一时刻,被激活的一条完整的数据处理路径。我们来看一个更具体的例子,它展示了物理硬件是如何被抽象成 entity 并连接成 pipeline 的:
1. 硬件与 Entity 的映射
-
物理硬件 (上图): 数据从“摄像头传感器”发出,经过“CSI-2 发射器”,再由主控的“CSI-2 接收器”接收,然后“格式解析”,最后送入“ISP0”处理。
-
逻辑抽象 (下图): Media 子系统将这些物理模块一一对应地抽象成了
sensor_subdev,csi_subdev,parser_subdev,isp0_subdev等entity。

2. Pipeline 的建立与数据流动
图中的link清晰地展示了数据流动的逻辑路径:sensor->csi->parser->isp0
这条由多个link串联起来的路径,就构成了一条完整的 pipeline。
这条 pipeline 并不是天生就激活的,而是由上层应用程序(比如相机 APP)来启动的。过程如下:- 应用发出指令: 相机 APP 通过 Linux 的
ioctl系统调用,告诉 Media 子系统:“我要激活从sensor到isp0的这条路径!” - Media 子系统调度: “总指挥”收到命令,沿着这条路径,依次通知路径上的每一个
subdev:“准备开始工作了!” - Subdev 配置硬件: 每个
subdev驱动收到通知后,就会去配置自己对应的物理硬件。比如sensor_subdev会通过 I2C 命令启动 Sensor 芯片。 - 数据开始流动: 一旦整条路径上的硬件都准备就绪,图像数据就开始在物理层面流动:Sensor 捕捉的图像信号,经过 CSI 接口,被 ISP 接收并处理。
- 应用获取数据: 最终,处理好的图像数据被放入内存,APP 从中读取,我们就看到了拍摄的画面。
通过这个过程可以看到,Media 子系统主要负责的是“控制流”,它搭建通路、下发命令。而真正的“数据流”则是在硬件层面自行流动的。
- 应用发出指令: 相机 APP 通过 Linux 的
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 时:
- 内核找到
link连接的源entity和目标entity。 - 内核找到对应的
pad。 - 内核调用
__media_entity_setup_link_notify,这个函数最终会分别调用源entity和目标entity的link_setup函数 (第 3.2 步)。
这样,subdev 驱动就得到了配置硬件的机会,从而完成了 link 的激活。
总结
现在,我们回顾一下:
- 为什么需要 Media Framework? 因为现代视频设备太复杂,需要一种“化整为零,分而治之”的管理方法。
- 核心概念是什么?
subdev:被拆分出来的、独立的硬件功能单元。media子系统:管理和连接subdev的“总指挥”。
- 它是如何工作的?
- 通过
entity,pad,link三个元素绘制出一张静态的“硬件蓝图”。 - 应用程序根据蓝图,请求激活一条或多条
link,从而搭建出一条动态的pipeline。 subdev驱动通过实现v4l2_subdev_ops里的函数,来响应“总指挥”的命令,完成最终的硬件配置。
- 通过

被折叠的 条评论
为什么被折叠?



