Linux 驱动研究 —— V4L2 (2)

1. 案例解析

在上一篇文章中我们介绍了设备数和驱动代码四个层面的配置,接下来我们通过具体的 ov13850 摄像头驱动代码,深入分析这个过程:

以下为 ov13850_probe 代码:

c 复制代码
static int ov13850_probe(struct i2c_client *client,
			 const struct i2c_device_id *id)
{
	struct device *dev = &client->dev;
	struct device_node *node = dev->of_node;
	struct ov13850 *ov13850;
	struct v4l2_subdev *sd;
	char facing[2];
	int ret;

	dev_info(dev, "driver version: %02x.%02x.%02x",
		DRIVER_VERSION >> 16,
		(DRIVER_VERSION & 0xff00) >> 8,
		DRIVER_VERSION & 0x00ff);

	ov13850 = devm_kzalloc(dev, sizeof(*ov13850), GFP_KERNEL);
	if (!ov13850)
		return -ENOMEM;

	ret = of_property_read_u32(node, RKMODULE_CAMERA_MODULE_INDEX,
				   &ov13850->module_index);
	ret |= of_property_read_string(node, RKMODULE_CAMERA_MODULE_FACING,
				       &ov13850->module_facing);
	ret |= of_property_read_string(node, RKMODULE_CAMERA_MODULE_NAME,
				       &ov13850->module_name);
	ret |= of_property_read_string(node, RKMODULE_CAMERA_LENS_NAME,
				       &ov13850->len_name);
	if (ret) {
		dev_err(dev, "could not get module information!\n");
		return -EINVAL;
	}

	ov13850->client = client;
	ov13850->cur_mode = &supported_modes[0];

	ov13850->xvclk = devm_clk_get(dev, "xvclk");
	if (IS_ERR(ov13850->xvclk)) {
		dev_err(dev, "Failed to get xvclk\n");
		return -EINVAL;
	}

	ov13850->power_gpio = devm_gpiod_get(dev, "power", GPIOD_OUT_LOW);
	if (IS_ERR(ov13850->power_gpio))
		dev_warn(dev, "Failed to get power-gpios, maybe no use\n");

	ov13850->reset_gpio = devm_gpiod_get(dev, "reset", GPIOD_OUT_LOW);
	if (IS_ERR(ov13850->reset_gpio))
		dev_warn(dev, "Failed to get reset-gpios\n");

	ov13850->pwdn_gpio = devm_gpiod_get(dev, "pwdn", GPIOD_OUT_LOW);
	if (IS_ERR(ov13850->pwdn_gpio))
		dev_warn(dev, "Failed to get pwdn-gpios\n");

	ret = ov13850_configure_regulators(ov13850);
	if (ret) {
		dev_err(dev, "Failed to get power regulators\n");
		return ret;
	}

	ov13850->pinctrl = devm_pinctrl_get(dev);
	if (!IS_ERR(ov13850->pinctrl)) {
		ov13850->pins_default =
			pinctrl_lookup_state(ov13850->pinctrl,
					     OF_CAMERA_PINCTRL_STATE_DEFAULT);
		if (IS_ERR(ov13850->pins_default))
			dev_err(dev, "could not get default pinstate\n");

		ov13850->pins_sleep =
			pinctrl_lookup_state(ov13850->pinctrl,
					     OF_CAMERA_PINCTRL_STATE_SLEEP);
		if (IS_ERR(ov13850->pins_sleep))
			dev_err(dev, "could not get sleep pinstate\n");
	}

	mutex_init(&ov13850->mutex);

	sd = &ov13850->subdev;
	v4l2_i2c_subdev_init(sd, client, &ov13850_subdev_ops);
	ret = ov13850_initialize_controls(ov13850);
	if (ret)
		goto err_destroy_mutex;

	ret = __ov13850_power_on(ov13850);
	if (ret)
		goto err_free_handler;

	ret = ov13850_check_sensor_id(ov13850, client);
	if (ret)
		goto err_power_off;

#ifdef CONFIG_VIDEO_V4L2_SUBDEV_API
	sd->internal_ops = &ov13850_internal_ops;
	sd->flags |= V4L2_SUBDEV_FL_HAS_DEVNODE;
#endif
#if defined(CONFIG_MEDIA_CONTROLLER)
	ov13850->pad.flags = MEDIA_PAD_FL_SOURCE;
	sd->entity.function = MEDIA_ENT_F_CAM_SENSOR;
	ret = media_entity_pads_init(&sd->entity, 1, &ov13850->pad);
	if (ret < 0)
		goto err_power_off;
#endif

	memset(facing, 0, sizeof(facing));
	if (strcmp(ov13850->module_facing, "back") == 0)
		facing[0] = 'b';
	else
		facing[0] = 'f';

	snprintf(sd->name, sizeof(sd->name), "m%02d_%s_%s %s",
		 ov13850->module_index, facing,
		 OV13850_NAME, dev_name(sd->dev));
	ret = v4l2_async_register_subdev_sensor_common(sd);
	if (ret) {
		dev_err(dev, "v4l2 async register subdev failed\n");
		goto err_clean_entity;
	}

	pm_runtime_set_active(dev);
	pm_runtime_enable(dev);
	pm_runtime_idle(dev);

	return 0;

err_clean_entity:
#if defined(CONFIG_MEDIA_CONTROLLER)
	media_entity_cleanup(&sd->entity);
#endif
err_power_off:
	__ov13850_power_off(ov13850);
err_free_handler:
	v4l2_ctrl_handler_free(&ov13850->ctrl_handler);
err_destroy_mutex:
	mutex_destroy(&ov13850->mutex);

	return ret;
}

以下为 ov13850 设备树部分代码:

复制代码
//SPDX-License-Identifier: (GPL-2.0+ OR MIT)
/dts-v1/;

#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/pinctrl/rockchip.h>

/ {
	/* =========================================================================
	 * 1. 摄像头核心电源控制节点
	 *    物理上直接决定 OV13850 的供电,通过 GPIO2_D7 进行硬使能
	 * ========================================================================= */
	vcc_camera: vcc-camera-regulator {
		compatible = "regulator-fixed";
		gpio = <&gpio2 RK_PD7 GPIO_ACTIVE_HIGH>;
		pinctrl-names = "default";
		pinctrl-0 = <&camera_pwr>;
		regulator-name = "vcc_camera";
		enable-active-high;
		regulator-always-on;
		regulator-boot-on;
	};
};

/* =========================================================================
 * 2. I2C2 总线配置与 OV13850 核心设备节点
 *    包含 V4L2 Subdev 驱动匹配及拓扑属性,驱动探针(probe)阶段最重要的信息源
 * ========================================================================= */
&i2c2 {
	status = "okay";
	pinctrl-names = "default";
	pinctrl-0 = <&i2c2m1_xfer>;

	ov13850: ov13850@10 {
		status = "okay";
		compatible = "ovti,ov13850";
		reg = <0x10>;                           /* I2C 物理从机地址 */
		
		/* 硬件时钟与电源控制 */
		clocks = <&cru CLK_CIF_OUT>;            /* 摄像头输入时钟(MCLK)来源 */
		clock-names = "xvclk";
		power-domains = <&power RK3568_PD_VI>;  /* 挂载在主控的视频输入电源域下 */
		pinctrl-names = "default";
		pinctrl-0 = <&cif_clk>;                 /* 引用系统定义的时钟引脚 */
		
		/* 硬件控制引脚 */
		reset-gpios = <&gpio3 RK_PD4 GPIO_ACTIVE_HIGH>; /* 硬件复位引脚 */
		pwdn-gpios = <&gpio3 RK_PD5 GPIO_ACTIVE_HIGH>;  /* 硬件休眠引脚 */
		
		/* 拓扑属性:驱动层用于动态拼接生成唯一的 Entity Name */
		rockchip,camera-module-index = <0>;
		rockchip,camera-module-facing = "back";
		rockchip,camera-module-name = "ZC-OV13850R2A-V1";
		rockchip,camera-module-lens-name = "Largan-50064B31";
		
		/* 数据输出端口(Media Pad / Link 核心:对应 Source Pad) */
		port {
			ov13850_out: endpoint {
				remote-endpoint = <&mipi_in_ucam3>; /* 远端指针:指向 MIPI D-PHY 的输入 */
				data-lanes = <1 2 3 4>;             /* 配置 4 条 MIPI 数据通道 */
			};
		};
	};
};

/* =========================================================================
 * 3. MIPI D-PHY 接收端配置
 *    在 csi2_dphy0 中建立对端节点,实现摄像头输出到主控 D-PHY 的 Media Link 绑定
 * ========================================================================= */
&csi2_dphy0 {
	status = "okay";

	ports {
		#address-cells = <1>;
		#size-cells = <0>;
		
		port@0 {
			reg = <0>;
			#address-cells = <1>;
			#size-cells = <0>;

			/* ucam3 输入通道:专门对接 ov13850_out */
			mipi_in_ucam3: endpoint@1 {
				reg = <1>;
				remote-endpoint = <&ov13850_out>; /* 远端反向指向 ov13850 的输出 */
				data-lanes = <1 2 3 4>;
			};
		};
	};
};

/* =========================================================================
 * 4. Pinctrl 专属引脚复用定义
 *    确保电源控制引脚被正确拉取和配置,避免引脚因状态未定义导致供电失败
 * ========================================================================= */
&pinctrl {
	cam {
		/* 电源使能引脚配置:复用为普通 GPIO,不设上下拉 */
		camera_pwr: camera-pwr {
			rockchip,pins = <2 RK_PD7 RK_FUNC_GPIO &pcfg_pull_none>;
		};
	};
};

现在有了这份代码,我们就能把上面那段 C 语言 probe 函数里的每一行逻辑,与设备树里的具体标签完成一次精准对齐。

下面我们直接将 ov13850_probe 的执行流程展开,看看它是如何顺着你提供的这套 DTS 图纸来构建 Entity 四大要素的:


1.1 Name

1. 要素一:提取自定义属性 →\rightarrow→ 动态组合出 Entity 的 Name

probe 函数前半段,驱动首先去"啃" &i2c2 总线下的 ov13850: ov13850@10 节点里的私有属性:

c 复制代码
    // 对应 DTS 中的:rockchip,camera-module-index = <0>;
    ret = of_property_read_u32(node, RKMODULE_CAMERA_MODULE_INDEX, &ov13850->module_index);

    // 对应 DTS 中的:rockchip,camera-module-facing = "back";
    ret |= of_property_read_string(node, RKMODULE_CAMERA_MODULE_FACING, &ov13850->module_facing);
    
    // 对应 DTS 中的:rockchip,camera-module-name = "ZC-OV13850R2A-V1";
    ret |= of_property_read_string(node, RKMODULE_CAMERA_MODULE_NAME, &ov13850->module_name);
    
    // 对应 DTS 中的:rockchip,camera-module-lens-name = "Largan-50064B31";
    ret |= of_property_read_string(node, RKMODULE_CAMERA_LENS_NAME, &ov13850->len_name);

1.1.1 底层 __of_find_property

以上涉及设备树内容提取的函数,最终都会调用底层的 __of_find_property

c 复制代码
/**
 * __of_find_property - 在指定的设备树节点中查找特定名称的属性
 * @np:    指向要查找的设备树节点结构体 (struct device_node) 的指针
 * @name:  要查找的属性名称字符串 (例如 "compatible", "reg", "status" 等)
 * @lenp:  输出参数指针,用于传出找到的属性数据的字节长度 (若不需要可传 NULL)
 *
 * 返回值: 找到对应的属性结构体指针 (struct property *);若未找到或参数无效则返回 NULL。
 * 注意:   该函数名字前缀带 '__',属于内核内部使用的未加锁版本,调用者需确保上下文安全。
 */
static struct property *__of_find_property(const struct device_node *np,
					   const char *name, int *lenp)
{
	struct property *pp;

	/* 防御性检测:如果传入的设备树节点指针为空,直接返回 NULL */
	if (!np)
		return NULL;

	/* 
	 * 核心链表遍历:
	 * 物理本质上,一个设备树节点下的所有属性是以"单向链表"的形式存储在内存中的。
	 * np->properties 指向链表的头节点。
	 * 每次循环通过 pp = pp->next 指向下一个属性,直到 pp 为 NULL 结束。
	 */
	for (pp = np->properties; pp; pp = pp->next) {
		
		/* 
		 * 字符串比对:
		 * 使用 of_prop_cmp 函数(底层通常等同于 strcasecmp 或 strcmp)
		 * 将当前遍历到的属性名 (pp->name) 与目标查找的属性名 (name) 进行比对。
		 * 如果返回值为 0,说明找到了匹配的属性。
		 */
		if (of_prop_cmp(pp->name, name) == 0) {
			
			/* 
			 * 传出长度:
			 * 如果调用者传入了有效的值长度指针 (lenp 不为空),
			 * 则将该属性在内存中的实际数据字节长度 (pp->length) 写入该指针所指向的地址。
			 */
			if (lenp)
				*lenp = pp->length;
			
			/* 既然已经找到了目标属性,立即跳出 for 循环 */
			break;
		}
	}

	/* 
	 * 返回结果:
	 * 如果中途 break 出来的,此时 pp 指向找到的属性结构体实体。
	 * 如果整个链表遍历完了都没找到,此时 pp 为 NULL。
	 */
	return pp;
}

1.1.2 of_property_read_u32

我们对其中一个函数进行分析:

of_property_read_u32(node, RKMODULE_CAMERA_MODULE_INDEX, &ov13850->module_index);

下面我们逐一拆解这三个属性的含义,并重点剖析第二个参数在内核底层究竟是怎么运作的:

  1. 第一个参数:node

    • 本质 :它是一个指向 struct device_node 结构体的指针。
    • 含义 :代表当前正在被解析的设备树节点本身 。在你的驱动代码里,它指向的就是设备树中的 ov13850: ov13850@10 { ... }; 这个节点的内存镜像。内核必须知道去哪个"房间"里翻箱倒柜。
  2. 第二个参数:RKMODULE_CAMERA_MODULE_INDEX

    最终就是在设备树里找这个名字

    它是一个宏定义(C 语言层面的替换)

    RKMODULE_CAMERA_MODULE_INDEX 并不是一个直接的字符串,它是瑞芯微内核多媒体框架定义的一个宏(Macro)。在内核头文件中,它被定义为:

    c 复制代码
    #define RKMODULE_CAMERA_MODULE_INDEX "rockchip,camera-module-index"

    与设备树代码中的属性名字一致

    rockchip,camera-module-index = <0>;

  3. 第三个参数:&ov13850->module_index("数据到手了,放哪?")

    • 本质:它是一个指针,指向驱动程序内部用来存放数据的内存地址。
    • 含义 :这是数据最终的归宿 。内核底层把设备树里的值读出来、转好大小端之后,会直接把这个最终的数字(在你的 DTS 里配置的是 <0>)"刷"进这个指针指向的变量里。

Name 要素落地:

代码利用读到的字符串进行拼接:

c 复制代码
    snprintf(sd->name, sizeof(sd->name), "m%02d_%s_%s %s",
             ov13850->module_index, "b", "ov13850", "1-0010");

结合设备树配置与 I2C 总线信息(I2C 总线 2,从机地址 0x10),最终在内核中注册的唯一 Entity Name 就会呈现为:"m00_b_ov13850 2-0010"


1.2 Function

2. 要素二:通过物理匹配 →\rightarrow→ 固化 Entity 的 Function

当系统启动时,内核的 I2C 核心层通过比对匹配上了设备树中的这一行:

dts 复制代码
compatible = "ovti,ov13850";

由于摄像头是挂载在 I2C 总线上的,所以I2C 总线会遍历挂载在它上面的驱动程序 ,拿着设备树节点里的 compatible = "ovti,ov13850"; 去和 ov13850 驱动代码里注册的匹配表(of_device_id 表)进行字符串比对,匹配成功后触发驱动的 ov13850_probe,内部就已经对 Function 进行了属性的配置:

c 复制代码
sd->entity.function = MEDIA_ENT_F_CAM_SENSOR;

Function 要素落地:

这不需要在设备树里额外配置 function = xxx。因为你的节点被挂载在摄像头相关的 I2C 通道上,驱动代码直接把这个实体的 Function 属性锁死为 MEDIA_ENT_F_CAM_SENSOR,告诉内核它是一个源头图像传感器。


1.3 Pad

3. 要素三:解析 port 标签 →\rightarrow→ 正式在内存中长出 Pad

1.3.1 设备树元素说明

设备树四种基本元素详解

在设备树文件中,所有的文本其实只由四种基本元素构成。通过提供的代码可以为它们分类:


  1. 节点(Node)------ 硬件的物理空间

    节点在代码中表现为带有大括号 { ... }; 的结构。它在内存中最终会被内核实例化为 struct device_node 结构体。

    节点有且只有两种状态:

    类型 示例 说明
    独立节点 port { ... }; / endpoint { ... }; 纯粹的空间容器,不携带地址信息
    带单元地址的节点 ov13850@10 这里的 @10单元地址(Unit Address) 。因为它是挂在 I2C 总线上的,这个 @10 必须与它内部的属性 reg = <0x10>;(I2C 从机地址)保持绝对一致,这是设备树的硬性语法规范。
  2. 标签(Label)------ 隔空定位的快捷指针

    在代码中表现为紧贴在节点名前面、带冒号的字符串,例如 ov13850:ov13850_out:

    要点 说明
    物理本质 标签只是写给编译器(DTC)看的别名 。编译成二进制格式(DTB)后,标签会变成一个全局唯一的 32 位数字,叫做 phandle(句柄)
    联系纽带 ov13850: 紧紧绑定了 ov13850@10 这个节点空间;ov13850_out: 紧紧绑定了 endpoint 这个节点空间。
  3. 引用(Reference)------ 隔空取物与追加配置

    在代码中表现为 & 符号后面跟着标签名。这里展现了引脚引用的两种高级用法:

    用法 示例 说明
    节点追加 &i2c2 { ... }; 这并不是在定义一个新的 I2C2,而是利用标签 i2c2 直接隔空锁定了芯片级文件(rk3568.dtsi)里厂商写好的那个 I2C2 节点,并往它的内部追加 一个名为 ov13850@10 的子节点。
    属性关联 <&power RK3568_PD_VI><&mipi_in_ucam3> 利用 & 符号,把当前属性的指针直接指向了另一个遥远的硬件节点,实现了硬件之间的跨时空对话
  4. 属性(Property)------ 节点的"全家当"

    凡是 键 = 值; 形式的键值对,都是属性 。它们在内核中是挂在所在节点 properties 单向链表上的 struct property 结构体。

    值通常有三种表现形式:

    表现形式 示例 用途
    字符串 compatible = "ovti,ov13850"; 供驱动去比对暗号(匹配设备树与驱动)
    整型数字/数组 reg = <0x10>; / data-lanes = <1 2 3 4>; 描述地址、引脚数量等硬件参数
    Phandle 引用句柄 clocks = <&cru CLK_CIF_OUT>; 代表时钟引脚接在了时钟控制单元(CRU)的 CLK_CIF_OUT 输水口上,实现跨节点引用

1.3.2 Port → Pad

设备树元素与驱动函数的映射关系:Port 到 Pad 的解析流程

静态的设备树配置需要通过驱动程序的解析,才能在内存中构建出对应的多媒体控制拓扑结构。下面以 ov13850@10 节点内的物理端口配置为例,说明其转换逻辑。

dts 复制代码
port {
    ov13850_out: endpoint {
        remote-endpoint = <&mipi_in_ucam3>;
        data-lanes = <1 2 3 4>;
    };
};
  1. 设备树元素的实际数据作用

    在这段拓扑描述中,四种基本元素为内核提供了离散的硬件参数:

    • 节点嵌套结构port 节点内嵌套了 endpoint 节点,这是 V4L2 异步注册框架规定的标准硬件层级结构,分别代表物理接口和具体的连线端点。
    • 属性存储参数endpoint 节点内的属性 data-lanes = <1 2 3 4>; 存储了当前硬件接口所使用的 MIPI 数据通道数量及通道映射信息。
    • 标签与引用提供单向索引endpoint 被标记为标签 ov13850_out:。其内部的属性通过引用 <&mipi_in_ucam3> 存储了远端接收芯片(MIPI D-PHY)节点的 phandle 编号,由此建立了单向的物理连接索引。
  2. 驱动读取设备树并创建内存实体的过程

    当驱动执行 probe 回调函数时,内核通过以下两阶段将设备树的静态数据转化为内存中的数据结构:

    • 阶段一:遍历节点并解析属性(读取设备树)

      驱动在执行到 v4l2_async_register_subdev_sensor_common 时,内核底层的图形解析框架会利用 __of_find_property 遍历设备树链表。它首先匹配到目标节点下的 "port" 子节点,并解析其内部包含的 "endpoint" 子节点数量。此时,内核在运行时确定了该设备的有效物理端口数量为 1

    • 阶段二:配置属性并开辟内存空间(Pad 结构体实例化)

      在获取到物理端口信息后,驱动程序通过代码对内存中的 media_pad 结构体进行显式赋值。由于该硬件在物理上仅作为图像数据发送端,驱动直接将其标志位设置为源端属性:

      c 复制代码
      ov13850->pad.flags = MEDIA_PAD_FL_SOURCE;

      随后,驱动调用核心层初始化函数,将当前实体的 media_entity 结构体指针、解析出的端口数量 1 以及 media_pad 结构体变量地址作为参数传入:

      c 复制代码
      ret = media_entity_pads_init(&sd->entity, 1, &ov13850->pad);
  3. 结果汇总

    执行完毕后,media_entity_pads_init 会在内核多媒体控制器(Media Controller)子系统中,将该图像传感器的 entity->num_pads 赋值为 1,并将传入的 pad 结构体的 index 序号初始化为 0

    至此,设备树中的 port 节点 成功转换为了 Linux 内核多媒体拓扑结构中的 Pad 0 内存对象,为后续与主控端 MIPI 驱动进行链路匹配(Link)提供了基础数据结构。

c 复制代码
&csi2_dphy0 {
	status = "okay";

	ports {
		#address-cells = <1>;
		#size-cells = <0>;
		port@0 {
			reg = <0>;
			#address-cells = <1>;
			#size-cells = <0>;

			mipi_in_ucam3: endpoint@1 {
				reg = <1>;
				remote-endpoint = <&ov13850_out>;
				data-lanes = <1 2 3 4>;
			};
		};
		port@1 {
			reg = <1>;
			#address-cells = <1>;
			#size-cells = <0>;

			csidphy_out: endpoint@0 {
				reg = <0>;
				remote-endpoint = <&isp0_in>;
			};
		};
	};
};

1.3.3 多 port 说明

一、 为什么 ports 节点下有两个 port 节点?

这是因为主控芯片的 MIPI D-PHY 控制器(csi2_dphy0)在硬件上是一个"数据中转站"。它拥有双向的物理硬件边界:

  1. 输入端(对接收硬件) :负责接收外部摄像头(如 ov13850)通过 MIPI 差分线发送过来的原始串行图像数据。
  2. 输出端(对处理硬件):负责将接收到的数据进行物理层解码后,打包传送给芯片内部的 ISP(图像信号处理器)进行图像色彩处理。

为了在 Linux 内核的多媒体图论框架(Graph Framework)中表达这种"一进一出"的硬件物理拓扑关系,设备树必须定义两个独立的 port 节点:

  • port@0(数据输入端口) :其内部的 mipi_in_ucam3 端点通过 remote-endpoint = <&ov13850_out>; 属性,与摄像头的输出端点进行数据对接。
  • port@1(数据输出端口) :其内部的 csidphy_out 端点通过 remote-endpoint = <&isp0_in>; 属性,与芯片内部的 ISP 接收端点进行数据对接。

二、 每个 port 可以有多少个 endpoint

在设备树的语法规范与内核多媒体拓扑模型中,一个 port 节点下可以拥有任意多个(1个或多个) endpoint 节点。具体配置的数量完全取决于该物理端口的硬件设计和复用连接关系

  1. 单个 endpoint 场景(如你代码中的 port@1

    • 物理意义:该端口在电气连线上是固定的、点对点的。
    • 应用port@1(输出端)只接了一个内部的 isp0_in,不需要切换或复用。
  2. 多个 endpoint 场景(常见于多摄像头复用输入,如 port@0 的潜在扩展)

    • 物理意义:该端口在物理上可以连接多个不同的外部设备,但在同一时间通常只能激活其中一个(通过多路复用器 MUX 或软件切换)。

    • 示例 :如果你的开发板在同一个 MIPI 接口上,通过硬件走线既可以插摄像头 A,也可以插摄像头 B。那么 port@0 下就会长出两个端点:

      dts 复制代码
      port@0 {
          /* 端点 1:连摄像头 A */
          mipi_in_ucam3: endpoint@1 {
              reg = <1>;
              remote-endpoint = <&ov13850_out>;
          };
          /* 端点 2:连摄像头 B */
          mipi_in_camera_b: endpoint@2 {
              reg = <2>;
              remote-endpoint = <&camera_b_out>;
          };
      };

      在这种多端点配置下,节点内部必须引入 #address-cells = <1>;#size-cells = <0>; 规范,并且每个 endpoint 必须通过 reg 属性 来显式声明其唯一的端点编号(如 reg = <1>;reg = <2>;),以便内核驱动在运行时能够精确识别并切换控制。

1.3.4 reg 属性说明

一、 reg 属性的核心定义与本质

reg 是设备树(Device Tree)中标准的内置属性,全称为 Register 。其核心作用是为节点提供其在父节点环境下的物理基地址(Base Address)或逻辑识别编号(Index/ID) ,并指出该硬件或配置项所占用的空间大小(Size)

reg 属性的数据格式由其父节点中的两个控制属性共同决定:

  • #address-cells:决定 reg 属性中"地址/编号"字段占用多少个 32 位整型(cell)。
  • #size-cells:决定 reg 属性中"长度/大小"字段占用多少个 32 位整型(cell)。

二、 reg 在不同场景下的具体作用

根据节点在设备树层级中所代表的硬件特性,reg 主要表现为两种具体作用:

场景 1:总线或物理外设节点(作为"物理地址"与"空间映射大小")

当节点对应真实的片上外设(如 I2C 控制器、GPIO 控制器)或外接芯片时,reg 存储的是硬件级别的物理地址信息。

  1. 在外接芯片节点中(如 ov13850@10):

    dts 复制代码
    ov13850: ov13850@10 {
        reg = <0x10>;
    };
  • 作用 :此处的 reg 存储的是该图像传感器在 I2C 总线上的 7 位物理从机地址0x10)。
  • 驱动处理 :Linux 内核的 I2C 核心层在解析此节点时,会读取该值并赋值给 struct i2c_client 结构体中的 addr 变量。后续所有 I2C 通信都将寻址此物理地址。
  1. 在片上外设节点中(如主控芯片内的 i2c2 控制器基本定义):

    dts 复制代码
    i2c2: i2c@fe5b0000 {
        reg = <0x0 0xfe5b0000 0x0 0x1000>;
    };
  • 作用 :此处的 reg 声明了该控制器的物理内存地址区间。在父节点设置了地址和长度各占两个 cells 的情况下,它表示:物理基地址为 0x00000000fe5b0000,占用内存地址空间长度为 0x0000000000001000(即 4KB)。
  • 驱动处理 :平台驱动在 probe 时通过 platform_get_resource() 提取该物理区间,并通过 ioremap 映射为内核虚拟地址,以便直接读写硬件寄存器。

场景 2:多媒体拓扑节点(作为"逻辑编号/通道索引")

reg 出现在 portendpoint 这种由 V4L2 与 Media Controller 框架抽象出来的虚拟逻辑拓扑节点中时,它不再代表物理内存地址,而是退化为纯粹的逻辑通道识别索引(Index)

dts 复制代码
ports {
    #address-cells = <1>;
    #size-cells = <0>;
    port@0 {
        reg = <0>; // 逻辑编号 0
        
        mipi_in_ucam3: endpoint@1 {
            reg = <1>; // 逻辑编号 1
        };
    };
};
  • 作用 :由于父节点设置了 #size-cells = <0>;,此处的 reg 只有编号,没有大小。reg = <0>; 显式声明该节点是第 0 号逻辑端口,reg = <1>; 声明该节点是该端口下的第 1 号逻辑端点。
  • 驱动处理 :内核的多媒体图论框架(Graph Framework)在解析拓扑结构时,驱动程序通过读取 reg 的数值,在代码内部将其绑定到对应的通道数组或特定的硬件通道处理逻辑(如输入通道或输出通道的分流控制)中。

三、 reg@单元地址 的强制对应规则

在设备树语法标准(IEEE Std 1275 / Devicetree Specification)中,reg 属性与节点的命名存在强制性联动约束:

若节点名称中包含 @ 符号及后续的单元地址(Unit Address),则该节点内部必须包含 reg 属性,且 reg 属性的第一个数值(即基地址或首个编号)必须与 @ 后面的文本数字在数值上完全一致。

  • 例如:节点名为 port@0,则其内部必须包含 reg = <0>;
  • 例如:节点名为 ov13850@10,则其内部必须包含 reg = <0x10>;

若两者数据不匹配,或者存在单元地址 @ 但未定义 reg 属性,设备树编译器(DTC)在编译阶段会报错或输出语法警告(Warning)。


4. 要素四:通过 remote-endpoint 对暗号 →\rightarrow→ 跨驱动连成 Links

这是整套多媒体流水线(Pipeline)能活过来的关键。你的设备树里有一组"两相呼应"的指针互连:

  1. 摄像头端: 我连着 MIPI 的 ucam3 端口。

    dts 复制代码
    ov13850_out: endpoint { remote-endpoint = <&mipi_in_ucam3>; };
  2. MIPI D-PHY 端: 我的输入端连着摄像头的输出。

    dts 复制代码
    mipi_in_ucam3: endpoint@1 { remote-endpoint = <&ov13850_out>; };

Link 要素落地:

ov13850_probe 运行到最后一步,执行了核心的异步注册函数:

c 复制代码
ret = v4l2_async_register_subdev_sensor_common(sd);

此时,ov13850 驱动就会把这一对连线暗号挂到内核的"悬赏榜"上。随后,主控内部的 &csi2_dphy0 驱动在初始化时也会解析自己的设备树,发现它的 mipi_in_ucam3 正好对应着摄像头。

内核在这一瞬间完成匹配,在内存里自动用指针画出了一条 Link ,将 m00_b_ov13850 2-0010 实体的 Pad 0 (Source),死死地扣在了 csi2_dphy0 实体的 Sink Pad 上!


2. 级联设备注册

 上一篇文章中,介绍了 V4L2 设备的级联关系,v4l2_device、video_device 和 v4l2_subdev 属于静态的框架概念。在 ov13850_probe 函数的末尾,驱动执行了核心的注册函数:

c 复制代码
ret = v4l2_async_register_subdev_sensor_common(sd);

这一步触发了内核的异步注册机制,实现了 v4l2_devicevideo_devicev4l2_subdev 三者之间的实例级联。


2.1 ov13850 初始化为 v4l2_subdev

在驱动的内存分配与初始化阶段,代码显式地将自定义结构体与框架结构体进行了解耦与初始化:

c 复制代码
struct ov13850 *ov13850;
struct v4l2_subdev *sd;

ov13850 = devm_kzalloc(dev, sizeof(*ov13850), GFP_KERNEL);
sd = &ov13850->subdev;
v4l2_i2c_subdev_init(sd, client, &ov13850_subdev_ops);
  • 硬件对应ov13850 本质上是一个挂载在 I2C 总线上的从设备。
  • 软件层级 :通过 v4l2_i2c_subdev_init,内核将该 I2C 客户端(i2c_client)的私有数据与标准 v4l2_subdev 结构体绑定,并配置了控制接口的操作集 ov13850_subdev_ops。该实例在此阶段成为一个独立的子设备节点。

2.2 异步匹配归属至 v4l2_device

在瑞芯微(Rockchip)平台的视频总驱动(如 VICAP 或 ISP 驱动)中,系统在初始化时会调用 v4l2_device_register 创建顶级管理结构体 v4l2_device

  1. 总驱动行为 :总驱动解析设备树中的 remote-endpoint 关系链,并将缺失的对端硬件(兼容性字符串为 "ovti,ov13850" 的 I2C 节点)注册到异步通知器(v4l2_async_notifier)的等待链表中。

  2. 子驱动行为 :当本案例中的 v4l2_async_register_subdev_sensor_common(sd) 被调用时,内核会将 ov13850v4l2_subdev 注册到全局的异步子设备链表。

  3. 级联触发 :内核的 V4L2 核心层比对两端的设备树节点,匹配成功后触发回调函数,执行 v4l2_device_register_subdev(v4l2_dev, sd)

    • 数据结构落地ov13850 实例的指针被正式插入到 v4l2_device->subdevs 双向链表中,建立了主设备对子设备的遍历控制接口。

2.3 与 video_device 的数据控制绑定

应用层(User Space)通过访问 /dev/videoX 字符设备节点实现数据流采集,该节点在内核中由 video_device 结构体进行抽象。

  • 子设备节点开启:案例代码中包含以下条件编译:

    c 复制代码
    #ifdef CONFIG_VIDEO_V4L2_SUBDEV_API
    	sd->internal_ops = &ov13850_internal_ops;
    	sd->flags |= V4L2_SUBDEV_FL_HAS_DEVNODE;
    #endif
  • 级联逻辑 :当 ov13850_subdev 被成功收编至 v4l2_device 链表后,内核检测到 V4L2_SUBDEV_FL_HAS_DEVNODE 标志,会自动为其在 /dev/ 下生成一个控制节点 /dev/v4l2-subdevX(专门用于应用层控制曝光、白平衡等底层参数)。

  • 控制流路径 :当应用层对主数据流节点 video_device(如 /dev/video0)下发 VIDIOC_STREAMON 指令时,总驱动的 v4l2_device 顺着 subdevs 链表遍历到本案例的 ov13850_subdev 实例,并调用 ov13850_subdev_ops 中的 .s_stream 函数,通过 I2C 协议向外设写入开流寄存器,完成控制闭环。

2.4 注册顺序

在实际的 Linux 内核运行中,这三者的实例化与级联并不是按照 v4l2_device →\rightarrow→ video_device →\rightarrow→ v4l2_subdev 这样从上到下的顺序线性执行的。

在硬件驱动的加载过程中,它们在时间线上是异步、并行初始化的,最终在某个时间点通过内核的"匹配机制"完成动态绑定。

为了理清这个顺序,我们可以将它们的实际创建与联机过程拆解为以下阶段:


  1. 级联顺序的"并列初始化"阶段

    在内核启动或加载驱动模块时,主板上的各个硬件驱动是独立运行自己的 probe 函数的:

    • 主控端(ISP / VICAP 驱动)
      执行自身的 probe,在内存中创建并初始化顶级管理者 v4l2_device。由于此时摄像头传感器(ov13850)还没来报到,主控端无法直接创建 /dev/video0video_device),因为数据源是不完整的。因此,主控端注册一个异步通知器(Notifier),并进入等待状态。
    • 外设端(OV13850 驱动)
      在 I2C 总线端执行自己的 probe,创建并初始化 v4l2_subdev。此时它与 v4l2_device 没有任何物理内存上的联系。它执行 v4l2_async_register_subdev_sensor_common,把自己挂载到内核的全局异步子设备链表上。

  1. 级联关系的"动态绑定"阶段

    ov13850 的子设备注册进入全局链表后,内核的 V4L2 异步框架会遍历当前所有挂起的通知器,进行匹配:

    复制代码
    [OV13850 的设备树节点句柄]  ◄─── (内核比对) ───►  [主控端 Notifier 记录的等待句柄]
    1. 匹配成功(Bound)
      内核确认 OV13850 就是主控端等待的组件,触发 bound 回调。
    2. 加入链表
      内核在此时调用 v4l2_device_register_subdev(v4l2_dev, sd),正式将 ov13850_subdev 的指针插入到 v4l2_device->subdevs 链表中。
    • 此时,v4l2_devicev4l2_subdev 之间的级联关系确立。

  1. 级联关系的"最终完整"阶段

    只有当主控端总驱动声明的所有子设备(例如 MIPI D-PHY、摄像头传感器、镜头马达等)全部匹配成功并被收编后,才会触发 complete 回调:

    1. 完整性检查 :主控端确认链路上的所有 v4l2_subdev 都已就位。
    2. 创建用户接口 :主控端在此阶段正式调用 video_register_device(),在内存中创建 video_device 结构体,并在 /dev/ 下生成 /dev/videoX 节点。
    • 此时,面向用户的 video_device 才被创建出来,并与已经就位的 v4l2_device 和整个 v4l2_subdev 链表完成最终的闭环级联。

总结

实际的建立顺序是:

独立创建 (v4l2_device 和 v4l2_subdev)→异步匹配绑定→最后创建 video_device\text{独立创建 (v4l2\_device 和 v4l2\_subdev)} \rightarrow \text{异步匹配绑定} \rightarrow \text{最后创建 video\_device}独立创建 (v4l2_device 和 v4l2_subdev)→异步匹配绑定→最后创建 video_device

这套机制保证了无论硬件以什么顺序加载,只要最终拼图完整,整个 V4L2 级联系统就能正常闭合工作。


2.5 如何具体绑定对应类型

在一个复杂的嵌入式系统或多路采集系统中,v4l2_devicevideo_device 确实都可以有多个实例

为了理清在多设备并存的情况下,底层的 v4l2_subdev(如 ov13850)是如何精准绑定到某一个具体的 v4l2_device,我们需要看设备树的描述内核的异步匹配机制是如何在数据结构层面落地的。


  1. 为什么会有多个 v4l2_devicevideo_device
  • 多个 v4l2_device:如果板子上有多个独立的图像输入硬件控制器(例如,RK3568 芯片上有两个独立的视频捕获控制器 VICAP0 和 VICAP1),每个控制器驱动在初始化时都会注册一个独立的 v4l2_device 实例。
  • 多个 video_device:一个 v4l2_device 内部也可以注册多个 /dev/videoX。例如,主控支持一路输入同时输出"原图分辨率数据流"和"缩放后的预览数据流",这时同一个 v4l2_device 下就会有两个 video_device 结构体,分别对应 /dev/video0/dev/video1

  1. v4l2_subdev 是如何精准绑定到某个具体 v4l2_device 的?

    底层子设备(v4l2_subdev)绝对不会盲目绑定。它的绑定过程完全是由设备树(DTS)中硬件拓扑连接关系唯一决定的。

    内核通过以下三个步骤完成精确绑定:

    步骤一:设备树(DTS)声明了唯一的物理连接路径

    在设备树中,每个外设和控制器的绑定关系通过 portendpoint 的引用(phandle)唯一确定。

    例如,板子上有两个控制总线和两个摄像头:

    • 摄像头 A(ov13850)接在 I2C1 上,物理 MIPI 接口接在 mipi_dphy0
    • 摄像头 B(另一个传感器)接在 I2C2 上,物理 MIPI 接口接在 mipi_dphy1
    • mipi_dphy0 的后端连接到主控的 vicap0 控制器(对应 v4l2_device_0)。

    设备树会写明这种点对点的连接:

    dts 复制代码
    // 摄像头 A 节点
    &i2c1 {
        ov13850: camera-sensor@10 {
            compatible = "ovti,ov13850";
            reg = <0x10>;
            port {
                ov13850_out: endpoint {
                    remote-endpoint = <&mipi_dphy0_in>; // 精准指向 D-PHY 0
                };
            };
        };
    };

    步骤二:主控驱动利用设备树构建"专属"等待队列

    当主控驱动 A(管理 vicap0)初始化自己的 v4l2_device 时,它会去解析自己 设备树节点下的 port 连接。

    1. vicap0 驱动发现自己的输入端连接了 mipi_dphy0
    2. 驱动继续往上游追溯,发现 mipi_dphy0 连接的是挂在 I2C1 上的 ov13850 节点。
    3. 主控驱动 A 将这个特定的设备树节点指针(struct device_node *)放进自己私有的 v4l2_async_notifier(异步通知器)等待名单里。

    关键点 :这个等待名单是绑定在主控驱动 A 的 v4l2_device_0 上的。只有属于这个名单的子设备,才能加入这个 v4l2_device

    步骤三:内核匹配设备树指针完成精准绑定

    当你的 ov13850 驱动执行 v4l2_async_register_subdev_sensor_common(sd) 时:

    1. 内核会将 ov13850 对应的 v4l2_subdev 放入全局链表,并携带它自身的设备树节点指针(即 sd->fwnode,指向 camera-sensor@10)。

    2. 内核的 V4L2 核心层开始比对:

      • 询问主控 v4l2_device_0 的通知器:"你名单里的设备树节点,和这个注册上来的 ov13850 节点一致吗?" →\rightarrow→ 一致
      • 询问主控 v4l2_device_1 的通知器:"你名单里有它吗?" →\rightarrow→ 没有
    3. 匹配成功后,内核直接调用:

      c 复制代码
      v4l2_device_register_subdev(v4l2_dev_0, sd);

      该函数内部会将 sd->v4l2_dev 指针直接指向 v4l2_dev_0

2.6 设备类型分配

设备树(DTS)节点本身在被内核解析前,没有任何 V4L2 角色(如 v4l2_subdevv4l2_device)的默认属性。设备树只负责描述硬件的物理参数(如 I2C 地址、引脚、兼容性字符串)。

这些 V4L2 角色,完全是由匹配到该节点的 C 语言驱动代码 在初始化(probe)阶段决定并设置的。

具体在何处设置、如何设置,解析如下:


  1. v4l2_subdev(子设备)是在哪里设置的?

    ov13850 为例,它成为 v4l2_subdev 是在 摄像头驱动的 C 源码 中决定的。

    • 对应的驱动文件drivers/media/i2c/ov13850.c

    • 核心设置代码 :在 ov13850_probe 函数中:

      c 复制代码
      struct ov13850 {
          struct v4l2_subdev sd; // 1. 结构体中直接嵌入 v4l2_subdev
          ...
      };
      
      static int ov13850_probe(struct i2c_client *client)
      {
          struct ov13850 *sensor;
          ...
          // 2. 初始化为 v4l2_subdev,并绑定控制操作集
          v4l2_i2c_subdev_init(&sensor->sd, client, &ov13850_subdev_ops);
      
          // 3. 声明该子设备需要暴露用户空间控制节点 (/dev/v4l2-subdevX)
          sensor->sd.flags |= V4L2_SUBDEV_FL_HAS_DEVNODE;
          ...
      }
    • 决定因素 :因为它是传感器、不直接负责将图像写入系统内存(DDR),所以编写该驱动的工程师在代码中将其定义并注册为了 v4l2_subdev


  1. v4l2_device(总管理设备)是在哪里设置的?

    主控芯片的视频采集控制器(如瑞芯微的 VICAP 或 ISP)在内核中被定义为 v4l2_device

    • 对应的驱动文件 :例如 drivers/media/platform/rockchip/cif/capture.crkisp.c

    • 核心设置代码 :在主控视频控制器的 probe 函数中:

      c 复制代码
      struct rk_cif_device {
          struct v4l2_device v4l2_dev; // 1. 结构体中嵌入 v4l2_device
          ...
      };
      
      static int rk_cif_probe(struct platform_device *pdev)
      {
          struct rk_cif_device *cif_dev;
          ...
          // 2. 将自身注册为顶级 v4l2_device 管理器
          ret = v4l2_device_register(&pdev->dev, &cif_dev->v4l2_dev);
          ...
      }
    • 决定因素 :因为该硬件是整个视频输入系统的中央控制器,负责协调所有的子设备(如 PHY、Sensor),所以它的驱动代码将其注册为 v4l2_device


  1. video_device(用户数据接口)是在哪里设置的?

    video_device 对应用户空间看到的 /dev/videoX 节点,专门用于应用层传输图像数据。它同样是由主控视频控制器驱动设置并注册的。

    • 对应的驱动文件 :同上(主控视频控制器驱动,如 rkisp.ccif/capture.c

    • 核心设置代码 :通常在主控驱动匹配完所有子设备后的 complete 回调中:

      c 复制代码
      struct rk_cif_stream {
          struct video_device vdev; // 1. 结构体中嵌入 video_device
          ...
      };
      
      static int rk_cif_register_stream_vdev(struct rk_cif_stream *stream)
      {
          struct video_device *vdev = &stream->vdev;
      
          // 2. 配置该节点的操作函数(如 open, read, ioctl)
          vdev->fops = &rk_cif_fops;
          vdev->ioctl_ops = &rk_cif_ioctl_ops;
          vdev->v4l2_dev = &cif_dev->v4l2_dev; // 关联到所属的 v4l2_device
      
          // 3. 注册字符设备,生成 /dev/videoX
          ret = video_register_device(vdev, VFL_TYPE_VIDEO, -1);
          ...
      }
    • 决定因素 :主控芯片内部有 DMA 引擎,能够真正将接收到的像素数据写入 DDR 内存。因此,主控驱动代码会为每个输出通道(Stream)创建一个 video_device 实例。


逻辑总结关系表

设备树节点与 V4L2 角色并非一一对应的死板关系,而是通过驱动代码中调用的 API 来动态确立的:

设备树(DTS)节点 绑定的 C 驱动 驱动中调用的注册 API 最终生成的 V4L2 内存实体
ov13850@10 ov13850.c v4l2_i2c_subdev_init() v4l2_subdev (子设备)
mipi_dphy0 mipi_dphy_drv.c v4l2_subdev_init() v4l2_subdev (子设备)
rk_cif (主控控制器) capture.c v4l2_device_register() v4l2_device (总管设备)
(无对应 DTS 节点,属于主控的 DMA 管道) capture.c video_register_device() video_device (/dev/videoX)
相关推荐
DB哥讲数据库1 小时前
MySQL 8.4 安装教程:超详细图文讲解(附mysql安装包)
linux·数据库·mysql·centos
阿米亚波1 小时前
【C++ STL】std::list
c++·windows·笔记·stl·list
xlxxy_1 小时前
sap获取批次特性报表
java·linux·开发语言·前端·数据库·abap·mm
木卫二号Coding2 小时前
CentOS 系统 Shell 脚本开机自启动方法详解
linux
l1t2 小时前
DeepSeek总结的RegreSQL 2.0测试通过了。计划却没通过。
linux·数据库·postgresql
~kiss~2 小时前
大模型的PEFT~ 参数高效微调 Parameter-Efficient Fine-Tuning
学习
蝶恋舞者2 小时前
怎样设置软件开机自启动(用的最多,最简单)
linux·运维·服务器
大明者省2 小时前
‌Docker Desktop - Virtual Machine Platform not enabled
笔记
Dawn-bit2 小时前
Linux打包压缩与用户权限管理详解
linux·服务器·数据库