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);
下面我们逐一拆解这三个属性的含义,并重点剖析第二个参数在内核底层究竟是怎么运作的:
-
第一个参数:
node- 本质 :它是一个指向
struct device_node结构体的指针。 - 含义 :代表当前正在被解析的设备树节点本身 。在你的驱动代码里,它指向的就是设备树中的
ov13850: ov13850@10 { ... };这个节点的内存镜像。内核必须知道去哪个"房间"里翻箱倒柜。
- 本质 :它是一个指向
-
第二个参数:
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>;
-
第三个参数:
&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 设备树元素说明
设备树四种基本元素详解
在设备树文件中,所有的文本其实只由四种基本元素构成。通过提供的代码可以为它们分类:
-
节点(Node)------ 硬件的物理空间
节点在代码中表现为带有大括号
{ ... };的结构。它在内存中最终会被内核实例化为struct device_node结构体。节点有且只有两种状态:
类型 示例 说明 独立节点 port { ... };/endpoint { ... };纯粹的空间容器,不携带地址信息 带单元地址的节点 ov13850@10这里的 @10是单元地址(Unit Address) 。因为它是挂在 I2C 总线上的,这个@10必须与它内部的属性reg = <0x10>;(I2C 从机地址)保持绝对一致,这是设备树的硬性语法规范。 -
标签(Label)------ 隔空定位的快捷指针
在代码中表现为紧贴在节点名前面、带冒号的字符串,例如
ov13850:和ov13850_out:。要点 说明 物理本质 标签只是写给编译器(DTC)看的别名 。编译成二进制格式(DTB)后,标签会变成一个全局唯一的 32 位数字,叫做 phandle(句柄)。联系纽带 ov13850:紧紧绑定了ov13850@10这个节点空间;ov13850_out:紧紧绑定了endpoint这个节点空间。 -
引用(Reference)------ 隔空取物与追加配置
在代码中表现为
&符号后面跟着标签名。这里展现了引脚引用的两种高级用法:用法 示例 说明 节点追加 &i2c2 { ... };这并不是在定义一个新的 I2C2,而是利用标签 i2c2直接隔空锁定了芯片级文件(rk3568.dtsi)里厂商写好的那个 I2C2 节点,并往它的内部追加 一个名为ov13850@10的子节点。属性关联 <&power RK3568_PD_VI>或<&mipi_in_ucam3>利用 &符号,把当前属性的指针直接指向了另一个遥远的硬件节点,实现了硬件之间的跨时空对话。 -
属性(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>;
};
};
-
设备树元素的实际数据作用
在这段拓扑描述中,四种基本元素为内核提供了离散的硬件参数:
- 节点嵌套结构 :
port节点内嵌套了endpoint节点,这是 V4L2 异步注册框架规定的标准硬件层级结构,分别代表物理接口和具体的连线端点。 - 属性存储参数 :
endpoint节点内的属性data-lanes = <1 2 3 4>;存储了当前硬件接口所使用的 MIPI 数据通道数量及通道映射信息。 - 标签与引用提供单向索引 :
endpoint被标记为标签ov13850_out:。其内部的属性通过引用<&mipi_in_ucam3>存储了远端接收芯片(MIPI D-PHY)节点的phandle编号,由此建立了单向的物理连接索引。
- 节点嵌套结构 :
-
驱动读取设备树并创建内存实体的过程
当驱动执行
probe回调函数时,内核通过以下两阶段将设备树的静态数据转化为内存中的数据结构:-
阶段一:遍历节点并解析属性(读取设备树)
驱动在执行到
v4l2_async_register_subdev_sensor_common时,内核底层的图形解析框架会利用__of_find_property遍历设备树链表。它首先匹配到目标节点下的"port"子节点,并解析其内部包含的"endpoint"子节点数量。此时,内核在运行时确定了该设备的有效物理端口数量为1。 -
阶段二:配置属性并开辟内存空间(Pad 结构体实例化)
在获取到物理端口信息后,驱动程序通过代码对内存中的
media_pad结构体进行显式赋值。由于该硬件在物理上仅作为图像数据发送端,驱动直接将其标志位设置为源端属性:cov13850->pad.flags = MEDIA_PAD_FL_SOURCE;随后,驱动调用核心层初始化函数,将当前实体的
media_entity结构体指针、解析出的端口数量1以及media_pad结构体变量地址作为参数传入:cret = media_entity_pads_init(&sd->entity, 1, &ov13850->pad);
-
-
结果汇总
执行完毕后,
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)在硬件上是一个"数据中转站"。它拥有双向的物理硬件边界:
- 输入端(对接收硬件) :负责接收外部摄像头(如
ov13850)通过 MIPI 差分线发送过来的原始串行图像数据。 - 输出端(对处理硬件):负责将接收到的数据进行物理层解码后,打包传送给芯片内部的 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 节点。具体配置的数量完全取决于该物理端口的硬件设计和复用连接关系:
-
单个
endpoint场景(如你代码中的port@1)- 物理意义:该端口在电气连线上是固定的、点对点的。
- 应用 :
port@1(输出端)只接了一个内部的isp0_in,不需要切换或复用。
-
多个
endpoint场景(常见于多摄像头复用输入,如port@0的潜在扩展)-
物理意义:该端口在物理上可以连接多个不同的外部设备,但在同一时间通常只能激活其中一个(通过多路复用器 MUX 或软件切换)。
-
示例 :如果你的开发板在同一个 MIPI 接口上,通过硬件走线既可以插摄像头 A,也可以插摄像头 B。那么
port@0下就会长出两个端点:dtsport@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 存储的是硬件级别的物理地址信息。
-
在外接芯片节点中(如
ov13850@10):dtsov13850: ov13850@10 { reg = <0x10>; };
- 作用 :此处的
reg存储的是该图像传感器在 I2C 总线上的 7 位物理从机地址 (0x10)。 - 驱动处理 :Linux 内核的 I2C 核心层在解析此节点时,会读取该值并赋值给
struct i2c_client结构体中的addr变量。后续所有 I2C 通信都将寻址此物理地址。
-
在片上外设节点中(如主控芯片内的
i2c2控制器基本定义):dtsi2c2: i2c@fe5b0000 { reg = <0x0 0xfe5b0000 0x0 0x1000>; };
- 作用 :此处的
reg声明了该控制器的物理内存地址区间。在父节点设置了地址和长度各占两个 cells 的情况下,它表示:物理基地址为0x00000000fe5b0000,占用内存地址空间长度为0x0000000000001000(即 4KB)。 - 驱动处理 :平台驱动在
probe时通过platform_get_resource()提取该物理区间,并通过ioremap映射为内核虚拟地址,以便直接读写硬件寄存器。
场景 2:多媒体拓扑节点(作为"逻辑编号/通道索引")
当 reg 出现在 port 或 endpoint 这种由 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)。
1.4 Link
4. 要素四:通过 remote-endpoint 对暗号 →\rightarrow→ 跨驱动连成 Links
这是整套多媒体流水线(Pipeline)能活过来的关键。你的设备树里有一组"两相呼应"的指针互连:
-
摄像头端: 我连着 MIPI 的 ucam3 端口。
dtsov13850_out: endpoint { remote-endpoint = <&mipi_in_ucam3>; }; -
MIPI D-PHY 端: 我的输入端连着摄像头的输出。
dtsmipi_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_device、video_device 与 v4l2_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。
-
总驱动行为 :总驱动解析设备树中的
remote-endpoint关系链,并将缺失的对端硬件(兼容性字符串为"ovti,ov13850"的 I2C 节点)注册到异步通知器(v4l2_async_notifier)的等待链表中。 -
子驱动行为 :当本案例中的
v4l2_async_register_subdev_sensor_common(sd)被调用时,内核会将ov13850的v4l2_subdev注册到全局的异步子设备链表。 -
级联触发 :内核的 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 这样从上到下的顺序线性执行的。
在硬件驱动的加载过程中,它们在时间线上是异步、并行初始化的,最终在某个时间点通过内核的"匹配机制"完成动态绑定。
为了理清这个顺序,我们可以将它们的实际创建与联机过程拆解为以下阶段:
-
级联顺序的"并列初始化"阶段
在内核启动或加载驱动模块时,主板上的各个硬件驱动是独立运行自己的
probe函数的:- 主控端(ISP / VICAP 驱动) :
执行自身的probe,在内存中创建并初始化顶级管理者v4l2_device。由于此时摄像头传感器(ov13850)还没来报到,主控端无法直接创建/dev/video0(video_device),因为数据源是不完整的。因此,主控端注册一个异步通知器(Notifier),并进入等待状态。 - 外设端(OV13850 驱动) :
在 I2C 总线端执行自己的probe,创建并初始化v4l2_subdev。此时它与v4l2_device没有任何物理内存上的联系。它执行v4l2_async_register_subdev_sensor_common,把自己挂载到内核的全局异步子设备链表上。
- 主控端(ISP / VICAP 驱动) :
-
级联关系的"动态绑定"阶段
当
ov13850的子设备注册进入全局链表后,内核的 V4L2 异步框架会遍历当前所有挂起的通知器,进行匹配:[OV13850 的设备树节点句柄] ◄─── (内核比对) ───► [主控端 Notifier 记录的等待句柄]- 匹配成功(Bound) :
内核确认 OV13850 就是主控端等待的组件,触发bound回调。 - 加入链表 :
内核在此时调用v4l2_device_register_subdev(v4l2_dev, sd),正式将ov13850_subdev的指针插入到v4l2_device->subdevs链表中。
- 此时,
v4l2_device与v4l2_subdev之间的级联关系确立。
- 匹配成功(Bound) :
-
级联关系的"最终完整"阶段
只有当主控端总驱动声明的所有子设备(例如 MIPI D-PHY、摄像头传感器、镜头马达等)全部匹配成功并被收编后,才会触发
complete回调:- 完整性检查 :主控端确认链路上的所有
v4l2_subdev都已就位。 - 创建用户接口 :主控端在此阶段正式调用
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_device 和 video_device 确实都可以有多个实例。
为了理清在多设备并存的情况下,底层的 v4l2_subdev(如 ov13850)是如何精准绑定到某一个具体的 v4l2_device,我们需要看设备树的描述 与内核的异步匹配机制是如何在数据结构层面落地的。
- 为什么会有多个
v4l2_device和video_device?
- 多个
v4l2_device:如果板子上有多个独立的图像输入硬件控制器(例如,RK3568 芯片上有两个独立的视频捕获控制器 VICAP0 和 VICAP1),每个控制器驱动在初始化时都会注册一个独立的v4l2_device实例。 - 多个
video_device:一个v4l2_device内部也可以注册多个/dev/videoX。例如,主控支持一路输入同时输出"原图分辨率数据流"和"缩放后的预览数据流",这时同一个v4l2_device下就会有两个video_device结构体,分别对应/dev/video0和/dev/video1。
-
v4l2_subdev是如何精准绑定到某个具体v4l2_device的?底层子设备(
v4l2_subdev)绝对不会盲目绑定。它的绑定过程完全是由设备树(DTS)中硬件拓扑连接关系唯一决定的。内核通过以下三个步骤完成精确绑定:
步骤一:设备树(DTS)声明了唯一的物理连接路径
在设备树中,每个外设和控制器的绑定关系通过
port和endpoint的引用(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连接。vicap0驱动发现自己的输入端连接了mipi_dphy0。- 驱动继续往上游追溯,发现
mipi_dphy0连接的是挂在 I2C1 上的ov13850节点。 - 主控驱动 A 将这个特定的设备树节点指针(
struct device_node *)放进自己私有的v4l2_async_notifier(异步通知器)等待名单里。
关键点 :这个等待名单是绑定在主控驱动 A 的
v4l2_device_0上的。只有属于这个名单的子设备,才能加入这个v4l2_device。步骤三:内核匹配设备树指针完成精准绑定
当你的
ov13850驱动执行v4l2_async_register_subdev_sensor_common(sd)时:-
内核会将
ov13850对应的v4l2_subdev放入全局链表,并携带它自身的设备树节点指针(即sd->fwnode,指向camera-sensor@10)。 -
内核的 V4L2 核心层开始比对:
- 询问主控
v4l2_device_0的通知器:"你名单里的设备树节点,和这个注册上来的ov13850节点一致吗?" →\rightarrow→ 一致。 - 询问主控
v4l2_device_1的通知器:"你名单里有它吗?" →\rightarrow→ 没有。
- 询问主控
-
匹配成功后,内核直接调用:
cv4l2_device_register_subdev(v4l2_dev_0, sd);该函数内部会将
sd->v4l2_dev指针直接指向v4l2_dev_0。
- 摄像头 A(
2.6 设备类型分配
设备树(DTS)节点本身在被内核解析前,没有任何 V4L2 角色(如 v4l2_subdev 或 v4l2_device)的默认属性。设备树只负责描述硬件的物理参数(如 I2C 地址、引脚、兼容性字符串)。
这些 V4L2 角色,完全是由匹配到该节点的 C 语言驱动代码 在初始化(probe)阶段决定并设置的。
具体在何处设置、如何设置,解析如下:
-
v4l2_subdev(子设备)是在哪里设置的?以
ov13850为例,它成为v4l2_subdev是在 摄像头驱动的 C 源码 中决定的。-
对应的驱动文件 :
drivers/media/i2c/ov13850.c -
核心设置代码 :在
ov13850_probe函数中:cstruct 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。
-
-
v4l2_device(总管理设备)是在哪里设置的?主控芯片的视频采集控制器(如瑞芯微的 VICAP 或 ISP)在内核中被定义为
v4l2_device。-
对应的驱动文件 :例如
drivers/media/platform/rockchip/cif/capture.c或rkisp.c -
核心设置代码 :在主控视频控制器的
probe函数中:cstruct 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。
-
-
video_device(用户数据接口)是在哪里设置的?video_device对应用户空间看到的/dev/videoX节点,专门用于应用层传输图像数据。它同样是由主控视频控制器驱动设置并注册的。-
对应的驱动文件 :同上(主控视频控制器驱动,如
rkisp.c或cif/capture.c) -
核心设置代码 :通常在主控驱动匹配完所有子设备后的
complete回调中:cstruct 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) |