核心内容摘要
《斗罗大陆》:唐三与比比东的宿命纠缠,何止“戳”那么简单!
上一篇文章已经讲解过摄像头数据流动环节的
分——sensor驱动接下来进行剩余部分的讲解本篇文章进行DPHY驱动和CSI驱动加载过程的解析Sensor (OV
➔ DPHY (物理层) ➔ CSI Host (控制器) ➔ VICAP(CIF) ➔ ISP驱动源码解析DPHY(物理层)驱动
设备树节点arch/arm64/boot/dts/rockchip/rk3588s.dtsiarch/arm64/boot/dts/rockchip/rk3588s-orangepi-5-camera
dtsi这个 csi2_dphy0 节点属于普通节点非I2C、SPI节点等因此内核在启动时会根据其生成一个platform_device结构体。
csi2_dphy0 { status disabled; ports { #address-cells 1; #size-cells 0; port0 { reg 0; #address-cells 1; #size-cells 0; mipi_in_ucam0: endpoint0 { reg 0; remote-endpoint ov13850_out2; >static const struct of_device_id rockchip_csi2_dphy_match_id[] { { .compatible rockchip,rk3568-csi2-dphy, // 设备树与此项匹配 .data rk3568_dphy_drv_data, }, { .compatible rockchip,rk3588-csi2-dcphy, .data rk3588_dcphy_drv_data, }, { .compatible rockchip,rv1106-csi2-dphy, .data rv1106_dphy_drv_data, }, {} }; struct platform_driver rockchip_csi2_dphy_driver { .probe rockchip_csi2_dphy_probe, // driver 和 driver 匹配时调用 .remove rockchip_csi2_dphy_remove, .driver { .name rockchip-csi2-dphy, .pm rockchip_csi2_dphy_pm_ops, .of_match_table rockchip_csi2_dphy_match_id, // 匹配表 }, }; static int __init rockchip_csi2_dphy_init(void) { return platform_driver_register(rockchip_csi2_dphy_driver); // 注册 platform_driver }程序在入口函数中注册了一个platform_driver结构体当platform_driver结构体的 .of_match_table 与设备树节点的 compatible匹配时probe函数会被调用。
2 probe函数详解详细的注释已经添加在函数中了/** * rockchip_csi2_dphy_probe - Rockchip CSI2 DPHY 驱动初始化函数 * pdev: 平台设备指针 * * 功能初始化 MIPI CSI-2 物理层DPHY接口用于连接摄像头传感器 * 返回值0表示成功负数表示错误码 */ static int rockchip_csi2_dphy_probe(struct platform_device *pdev) { struct device *dev pdev-dev; // 获取设备指针 const struct of_device_id *of_id; // 设备树匹配ID struct csi2_dphy *csi2dphy; // DPHY设备结构体 struct v4l2_subdev *sd; // V4L2子设备 const struct dphy_drv_data *drv_data; // 驱动私有数据不同芯片的配置 int ret; // 返回值 /*
分配内存并保存设备指针 */ // 为 DPHY 设备结构体分配内存驱动卸载时会自动释放 csi2dphy devm_kzalloc(dev, sizeof(*csi2dphy), GFP_KERNEL); if (!csi2dphy) return -ENOMEM; // 内存不足错误 csi2dphy-dev dev; // 保存设备指针方便后续使用 /*
匹配设备树并获取驱动配置数据 */ // 从设备树中查找匹配的设备根据 compatible 属性 of_id of_match_device(rockchip_csi2_dphy_match_id, dev); if (!of_id) return -EINVAL; // 设备树中没有匹配的节点 // 获取该设备的私有配置数据如寄存器地址、时钟名称等 drv_data of_id-data; csi2dphy-drv_data drv_data; /*
获取 PHY 索引编号 */ // 从设备树别名中获取 PHY 编号本节点为 csi2-dphy0因此取到0 csi2dphy-phy_index of_alias_get_id(dev-of_node, drv_data-dev_name); // 如果索引无效小于0或超出最大值默认设为0 if (csi2dphy-phy_index 0 || csi2dphy-phy_index PHY_MAX) csi2dphy-phy_index 0; /*
根据厂商类型连接硬件资源 */ // 判断 DPHY 的厂商类型采用不同的初始化方法获取硬件资源本节点是非Samsung的dphy因此走else分支 if (csi2dphy-drv_data-vendor PHY_VENDOR_SAMSUNG) { /* Samsung DCPHY双模PHY支持CSI和DSI */ ret rockchip_csi2_dphy_attach_samsung_phy(csi2dphy); // 设置 Samsung 特定的 DPHY 参数 csi2dphy-dphy_param rk3588_dcphy_param; } else { /* Rockchip 标准 MIPI DPHY */ ret rockchip_csi2_dphy_attach_hw(csi2dphy); } // 如果硬件资源获取失败如寄存器映射、时钟获取失败 if (ret) { dev_err(dev, csi2 dphy hw cant be attached, register dphy%d failed!\n, csi2dphy-phy_index); return -ENODEV; // 设备不存在错误 } /*
初始化 V4L2 子设备 */ sd csi2dphy-sd; // 获取子设备指针 // 初始化互斥锁防止多线程并发访问冲突 mutex_init(csi2dphy-mutex); // 初始化 V4L2 子设备注册操作函数集 v4l2_subdev_init(sd, csi2_dphy_subdev_ops); // 标记该设备有独立的设备节点可在 /dev/ 下访问 sd-flags | V4L2_SUBDEV_FL_HAS_DEVNODE; // 生成设备名称如 rockchip-csi2-dphy0 snprintf(sd-name, sizeof(sd-name), rockchip-csi2-dphy%d, csi2dphy-phy_index); sd-dev dev; // 关联设备指针 /*
保存平台设备私有数据 */ // 将 V4L2 实体指针存到平台设备的私有数据中 // 后续可通过 platform_get_drvdata() 获取 platform_set_drvdata(pdev, sd-entity); /*
初始化 Media Controller */ // Media Controller 用于管理视频数据流摄像头→PHY→ISP→内存 ret rockchip_csi2dphy_media_init(csi2dphy); if (ret
goto detach_hw; // 失败则跳转到资源清理 /*
启用运行时电源管理 */ // 允许设备在不使用时自动进入省电模式 pm_runtime_enable(pdev-dev); /*
打印成功信息 */ dev_info(dev, csi2 dphy%d probe successfully!\n, csi2dphy-phy_index); return 0; // 初始化成功 /* 错误处理清理已分配的资源 */ detach_hw: // 销毁互斥锁 mutex_destroy(csi2dphy-mutex); // 根据厂商类型调用对应的硬件资源释放函数 if (csi2dphy-drv_data-vendor PHY_VENDOR_SAMSUNG) rockchip_csi2_dphy_detach_samsung_phy(csi2dphy); // 释放Samsung PHY资源 else rockchip_csi2_dphy_detach_hw(csi2dphy); // 释放标准DPHY资源 return 0; // 注意这里应该返回ret错误码但代码写的是0可能是bug }
3 重点解析如果看过前面的sensor驱动程序解析就会很熟悉这里的三大步了即分配、设置、注册。
这里就不详细解释了快速过一下。
分配subdevcsi2dphy devm_kzalloc(dev, sizeof(*csi2dphy), GFP_KERNEL);分配csi2dphy结构体顺带分配sundev结构体。
设置subdevsd csi2dphy-sd; mutex_init(csi2dphy-mutex); v4l2_subdev_init(sd, csi2_dphy_subdev_ops); sd-flags | V4L2_SUBDEV_FL_HAS_DEVNODE; snprintf(sd-name, sizeof(sd-name), rockchip-csi2-dphy%d, csi2dphy-phy_index); sd-dev dev;这里重点设置了subdev的操作函数集 csi2_dphy_subdev_ops。
注册subdevret rockchip_csi2dphy_media_init(csi2dphy);以下是函数原型/** * rockchip_csi2dphy_media_init - 初始化 DPHY 的 Media Controller 接口 * dphy: DPHY 设备结构体指针 * * 功能 *
配置 media pads数据端口 *
注册异步通知器等待摄像头传感器连接 *
注册为 V4L2 子设备 * * 返回值0表示成功负数表示错误码 */ static int rockchip_csi2dphy_media_init(struct csi2_dphy *dphy) { int ret; /*
配置 Media Pads数据端口 */ /* 配置 SOURCE pad数据输出端*/ dphy-pads[CSI2_DPHY_RX_PAD_SOURCE].flags MEDIA_PAD_FL_SOURCE | // 标记为数据源输出数据 MEDIA_PAD_FL_MUST_CONNECT; // 必须连接到下一级设备如ISP /* 配置 SINK pad数据接收端*/ dphy-pads[CSI2_DPHY_RX_PAD_SINK].flags MEDIA_PAD_FL_SINK | // 标记为数据接收端接收摄像头数据 MEDIA_PAD_FL_MUST_CONNECT; // 必须连接到上一级设备摄像头传感器 /* 设置设备功能类型视频接口桥接器连接摄像头和ISP */ dphy-sd.entity.function MEDIA_ENT_F_VID_IF_BRIDGE; /* 初始化 media entity 的 pads注册2个端口SINK和SOURCE */ ret media_entity_pads_init(dphy-sd.entity, CSI2_DPHY_RX_PADS_NUM, // pads数量通常是2 dphy-pads); // pads数组 if (ret
return ret; // 初始化失败 /*
初始化异步通知器 */ /* 初始化异步通知器用于等待摄像头传感器设备就绪 */ v4l2_async_notifier_init(dphy-notifier); /* * 解析设备树中的 endpoint查找连接的摄像头传感器 * - 从设备树的 port 0 中解析 endpoint * - 为每个 endpoint 分配一个 sensor_async_subdev 结构体 * - 调用 rockchip_csi2_dphy_fwnode_parse 解析具体配置 */ ret v4l2_async_notifier_parse_fwnode_endpoints_by_port( dphy-dev, // 设备指针 dphy-notifier, // 通知器 sizeof(struct sensor_async_subdev), // 每个子设备的结构体大小 0, // 解析 port 0摄像头输入端口 rockchip_csi2_dphy_fwnode_parse); // 解析回调函数 if (ret
return ret; // 解析失败可能设备树没配置摄像头 /*
注册异步通知器 */ /* 将通知器关联到子设备 */ dphy-sd.subdev_notifier dphy-notifier; /* 设置通知器的操作函数当摄像头设备就绪时会调用这些函数 */ dphy-notifier.ops rockchip_csi2_dphy_async_ops; /* 注册异步子设备通知器 */ ret v4l2_async_subdev_notifier_register(dphy-sd, dphy-notifier); if (ret) { dev_err(dphy-dev, failed to register async notifier : %d\n, ret); /* 清理通知器资源 */ v4l2_async_notifier_cleanup(dphy-notifier); return ret; } /*
注册为 V4L2 异步子设备 */ /* * 将 DPHY 注册为 V4L2 子设备 * - 等待上级设备如 ISP连接 * - 等待下级设备摄像头传感器连接 * - 所有设备连接后整个视频管道才能工作 */ return v4l2_async_register_subdev(dphy-sd); }这个函数的流程基本与上一篇文章的sensor异步注册subdev过程一致因此不做过多讲解简单看下注释即可。
细节小结看到这里有人可能会发现之前的sensor注册过程和dphy注册过程有一点差别具体如下
1 sensorv4l2_async_notifier_init(notifier); ret v4l2_async_notifier_parse_fwnode_sensor_common(sd-dev, notifier); ret v4l2_async_subdev_notifier_register(sd, notifier); ret v4l2_async_register_subdev(sd);
2 dphyv4l2_async_notifier_init(dphy-notifier); ret v4l2_async_notifier_parse_fwnode_endpoints_by_port(dphy-dev, dphy-notifier, sizeof(struct sensor_async_subdev), 0, rockchip_csi2_dphy_fwnode_parse); dphy-notifier.ops rockchip_csi2_dphy_async_ops; // 主要区别 ret v4l2_async_subdev_notifier_register(dphy-sd, dphy-notifier); v4l2_async_register_subdev(dphy-sd);这两个驱动虽然都使用异步通知器但用途完全不同所以是否需要设置 ops 也不同。
3 关键区别谁在等待谁
rockchip_csi2dphy_media_initDPHY驱动dphy-notifier.ops rockchip_csi2_dphy_async_ops; // 设置了 ops作用DPHY 等待摄像头传感器连接数据流向摄像头 → DPHY → ISP DPHY 的角色 - 我是中间设备需要等待上游的摄像头 - 摄像头连接成功时我需要执行一些操作通过 ops 回调ops 的具体作用static const struct v4l2_async_notifier_operations rockchip_csi2_dphy_async_ops { /* 当摄像头设备绑定成功时调用 */ .bound rockchip_csi2_dphy_notifier_bound, /* 当所有等待的设备都就绪时调用 */ .complete rockchip_csi2_dphy_notifier_complete, };为什么需要 ops摄像头连接后DPHY 需要建立数据链路media link需要配置数据通道数data-lanes需要通知下游设备ISP准备接收数据
v4l2_async_register_subdev_sensor_commonsensor驱动// 注意这里没有设置 notifier-ops v4l2_async_notifier_init(notifier);作用摄像头传感器等待镜头/闪光灯等外围设备数据流向镜头 → 传感器 → DPHY 传感器的角色 - 我是最上游设备只需要等待镜头、闪光灯等配件 - 这些配件连接与否不影响数据流只是功能增强 - 不需要复杂的回调处理为什么不需要 ops镜头lens、闪光灯flash只是可选配件它们连接后传感器只需要记录引用不需要额外操作V4L2 框架会自动处理基本的绑定逻辑
4 设备树对应关系
DPHY 的设备树有 opscsi2_dphy0 { ports { port0 { // ← 这个 port 会被 parse_fwnode_endpoints_by_port 解析 mipi_in_ucam1: endpoint1 { reg 1; remote-endpoint ov13855_out2; >ov13855: ov1385536 { compatible ovti,ov13855; /* 可选的镜头配件 */ lens-focus dw9714_p1; // ← parse_fwnode_sensor_common 解析这个 port { ov13855_out2: endpoint { remote-endpoint mipi_in_ucam1; >
5 代码执行流程对比场景1DPHY 等待摄像头需要 ops
DPHY 驱动加载 ↓
rockchip_csi2dphy_media_init() ↓
解析设备树发现需要等待 ov13855 ↓
注册通知器设置 ops ↓
OV13855 驱动加载 ↓
触发 DPHY 的 ops.bound 回调 ↓ 【在回调中执行】 - 获取摄像头的数据通道配置 - 创建 media link: ov13855:0 → dphy:0 - 配置 DPHY 的接收参数 ↓
所有设备就绪触发 ops.complete ↓ 【在回调中执行】 - 注册整个 media controller 图 - 通知用户空间可以使用摄像头ops.bound 实际代码示例static int rockchip_csi2_dphy_notifier_bound( struct v4l2_async_notifier *notifier, struct v4l2_subdev *sd, struct v4l2_async_subdev *asd) { struct csi2_dphy *dphy container_of(notifier, struct csi2_dphy, notifier); struct sensor_async_subdev *s_asd container_of(asd, ...); /* 保存传感器引用 */ dphy-sensor_sd sd; /* 获取数据通道配置 */ dphy-lane_num s_asd-lanes; /* 创建 media link */ return media_create_pad_link(sd-entity, 0, dphy-sd.entity, CSI2_DPHY_RX_PAD_SINK, MEDIA_LNK_FL_ENABLED); }场景2传感器等待镜头不需要 ops
OV13855 驱动加载 ↓
v4l2_async_register_subdev_sensor_common() ↓
解析设备树发现 lens-focus dw9714_p1 ↓
注册通知器不设置 ops使用默认行为 ↓
DW9714 镜头驱动加载 ↓
V4L2 框架自动绑定默认行为 ↓ 【框架自动执行】 - 设置 sd-lens dw9714_subdev ↓
传感器可以直接使用 sd-lens 控制镜头对焦为什么不需要 ops因为 V4L2 框架已经提供了默认的绑定逻辑// V4L2 框架内部的默认绑定代码简化版 static int v4l2_async_match_notify(struct v4l2_async_notifier *notifier, struct v4l2_subdev *sd, struct v4l2_async_subdev *asd) { // 如果是镜头设备 if (asd-match.fwnode.fwnode lens_fwnode) { notifier-sd-lens sd; // 自动保存镜头引用 } // 如果是闪光灯设备 if (asd-match.fwnode.fwnode flash_fwnode) { notifier-sd-flash sd; // 自动保存闪光灯引用 } // 不需要用户提供额外的 ops return 0; }
6 什么时候需要设置 ops需要设置 ops 的情况需要建立 media link如 DPHY ↔ ISP需要配置硬件参数如 DPHY 的数据通道数需要通知下游设备如告诉 ISP 准备接收数据需要复杂的初始化流程如 ISP 的多级管道配置不需要设置 ops 的情况只是简单的设备引用如传感器 ↔ 镜头框架已提供默认行为如 lens、flash 的自动绑定不涉及数据流配置镜头不参与数据传输驱动源码解析CSI(控制器)驱动
设备树节点同理内核在启动时会根据其生成一个platform_device结构体。
mipi2_csi2 { status disabled; ports { #address-cells 1; #size-cells 0; port0 { reg 0; #address-cells 1; #size-cells 0; mipi2_csi2_input: endpoint1 { reg 1; remote-endpoint csidphy0_out; }; }; port1 { reg 1; #address-cells 1; #size-cells 0; mipi2_csi2_output: endpoint0 { reg 0; remote-endpoint cif_mipi_in2; }; }; }; }; mipi2_csi2: mipi2-csi2fdd30000 { compatible rockchip,rk3588-mipi-csi2; reg 0x0 0xfdd30000 0x0 0x10000; reg-names csihost_regs; interrupts GIC_SPI 147 IRQ_TYPE_LEVEL_HIGH, GIC_SPI 148 IRQ_TYPE_LEVEL_HIGH; interrupt-names csi-intr1, csi-intr2; clocks cru PCLK_CSI_HOST_2; clock-names pclk_csi2host; resets cru SRST_P_CSI_HOST_2, cru SRST_CSIHOST2_VICAP; reset-names srst_csihost_p, srst_csihost_vicap; status disabled; };
驱动代码解析drivers/media/platform/rockchip/cif/mipi-csi
cCSI2的驱动注册、匹配流程和之前的dphy驱动注册、匹配流程完全一致这里不过多赘述。
下面主要看prob函数中是否还是那三大步分配、设置、注册验证我们所了解的”套路“。
1 分配subdevcsi2 devm_kzalloc(pdev-dev, sizeof(*csi
, GFP_KERNEL);分配csi2结构体时顺带分配subdev
2 设置subdevv4l2_subdev_init(csi2-sd, csi2_subdev_ops); v4l2_set_subdevdata(csi2-sd, pdev-dev); csi2-sd.entity.ops csi2_entity_ops; csi2-sd.dev pdev-dev; csi2-sd.owner THIS_MODULE; csi2-sd.flags | V4L2_SUBDEV_FL_HAS_DEVNODE | V4L2_SUBDEV_FL_HAS_EVENTS; ret strscpy(csi2-sd.name, DEVICE_NAME, sizeof(csi2-sd.name)); ret csi2_media_init(csi2-sd);设置subdev中的一系列成员。
static int csi2_media_init(struct v4l2_subdev *sd) { struct csi2_dev *csi2 sd_to_dev(sd); int i 0, num_pads 0; num_pads csi2-match_data-num_pads; for (i 0; i num_pads; i) { csi2-pad[i].flags (i CSI2_SINK_PAD) ? MEDIA_PAD_FL_SINK : MEDIA_PAD_FL_SOURCE; } csi2-pad[RK_CSI2X_PAD_SOURCE0].flags MEDIA_PAD_FL_SOURCE | MEDIA_PAD_FL_MUST_CONNECT; csi2-pad[RK_CSI2_PAD_SINK].flags MEDIA_PAD_FL_SINK | MEDIA_PAD_FL_MUST_CONNECT; // ....... return media_entity_pads_init(sd-entity, num_pads, csi2-pad); }初始化entity的pad端口entity与subdev是一一对应的。