[驱动进阶——MIPI摄像头驱动(四)]rk3588+OV13855摄像头驱动加载过程详细解析第三部分——CIF驱动+SDITF驱动

上一篇文章已经讲解过摄像头数据流动环节的第二部分——DPHY驱动+CSI驱动,接下来进行剩余部分的讲解,本篇文章进行CIF驱动+SDITF驱动加载过程的解析:

Sensor (OV13855) ➔ DPHY (物理层) ➔ CSI Host (控制器) ➔ VICAP(CIF) ➔ ISP

SDITF是依附于CIF硬件控制器的。


驱动源码解析:cif_lvds2驱动

这个设备驱动有些特殊,和前面已经讲过的设备驱动稍有不同,它处于/dev/media0所管理的层次中的最后一层。

一、设备树节点

内核会根据此节点生成一个platform_device。

&rkcif_mipi_lvds2 { status = "disabled"; port { cif_mipi_in2: endpoint { remote-endpoint = <&mipi2_csi2_output>; }; }; }; ​ rkcif_mipi_lvds2: rkcif-mipi-lvds2 { compatible = "rockchip,rkcif-mipi-lvds"; rockchip,hw = <&rkcif>; iommus = <&rkcif_mmu>; status = "disabled"; };

二、驱动代码解析

drivers/media/platform/rockchip/cif/dev.c

2.1 probe函数详解

详细注释已经放在源码中,后续重点关注两个函数(rkcif_plat_init、rkcif_get_reserved_mem)

/** * rkcif_plat_probe - Rockchip CIF 平台驱动初始化函数 * @pdev: 平台设备指针 * * 功能:初始化 CIF(Camera Interface)设备,负责接收摄像头数据并写入内存 * CIF 在视频管道中的位置:摄像头 → DPHY → CIF → 内存 → ISP * * 返回值:0表示成功,负数表示错误码 */ static int rkcif_plat_probe(struct platform_device *pdev) { const struct of_device_id *match; // 设备树匹配信息 struct device_node *node = pdev->dev.of_node; // 设备树节点 struct device *dev = &pdev->dev; // 设备指针 struct rkcif_device *cif_dev; // CIF 设备结构体 const struct rkcif_match_data *data; // 驱动私有数据 int ret; ​ /* ========== 1. 打印驱动版本信息 ========== */ /* 将版本号格式化为字符串(如 "v01.02.03") */ sprintf(rkcif_version, "v%02x.%02x.%02x", RKCIF_DRIVER_VERSION >> 16, // 主版本号 (RKCIF_DRIVER_VERSION & 0xff00) >> 8, // 次版本号 RKCIF_DRIVER_VERSION & 0x00ff); // 修订号 ​ /* 打印版本信息,方便调试 */ dev_info(dev, "rkcif driver version: %s\n", rkcif_version); ​ /* ========== 2. 匹配设备树并获取驱动配置 ========== */ /* 从设备树中查找匹配的节点(根据 compatible 属性) */ match = of_match_node(rkcif_plat_of_match, node); if (IS_ERR(match)) return PTR_ERR(match); // 设备树中没有匹配的节点 /* 获取该设备的私有配置数据(如接口类型、通道数等) */ data = match->data; ​ /* ========== 3. 分配内存并初始化设备结构体 ========== */ /* 为 CIF 设备结构体分配内存(驱动卸载时自动释放) */ cif_dev = devm_kzalloc(dev, sizeof(*cif_dev), GFP_KERNEL); if (!cif_dev) return -ENOMEM; // 内存不足 ​ /* 将 cif_dev 保存到平台设备的私有数据中 */ dev_set_drvdata(dev, cif_dev); /* 保存设备指针 */ cif_dev->dev = dev; ​ /* ========== 4. 创建 sysfs 属性组 ========== */ /* * 在 sysfs 中创建属性文件(通常在 /sys/devices/.../下) * 用户空间可以通过这些文件查询状态或调整参数 * 例如: * /sys/.../rkcif/status - 查看 CIF 状态 * /sys/.../rkcif/frame_count - 查看接收帧数 */ if (sysfs_create_group(&pdev->dev.kobj, &dev_attr_grp)) return -ENODEV; // sysfs 创建失败 ​ /* ========== 5. 连接硬件资源 ========== */ /* * 获取并映射硬件资源: * - 映射寄存器地址(ioremap) * - 获取时钟(如 ACLK_CIF、HCLK_CIF) * - 获取复位控制器 * - 获取中断号 */ rkcif_attach_hw(cif_dev); ​ /* ========== 6. 解析设备树配置 ========== */ /* * 从设备树读取配置参数: * - 接口类型(MIPI/DVP/BT656 等) * - 数据通道数量 * - DMA 缓冲区配置 * - 中断配置 * - 工作模式等 */ rkcif_parse_dts(cif_dev); ​ /* ========== 7. 初始化 CIF 平台相关功能(重点关注!!!) ========== */ /* * 功能:初始化 CIF 设备的核心功能,包括: * - 初始化锁和管道 * - 根据芯片型号初始化不同数量的数据流通道 * - 注册 V4L2 和 Media 设备 * - 注册平台子设备(异步等待上游 DPHY) *>1. rkcif_plat_init
/** * rkcif_plat_init - CIF 平台初始化函数 * @cif_dev: CIF 设备结构体 * @node: 设备树节点 * @inf_id: 接口 ID(MIPI/DVP 等) * * 功能:初始化 CIF 设备的核心功能,包括: * - 初始化锁和管道 * - 根据芯片型号初始化不同数量的数据流通道 * - 注册 V4L2 和 Media 设备 * - 注册平台子设备(异步等待上游 DPHY) * * 返回值:0表示成功,负数表示错误码 */ int rkcif_plat_init(struct rkcif_device *cif_dev, struct device_node *node, int inf_id) { struct device *dev = cif_dev->dev; struct v4l2_device *v4l2_dev; int ret; ​ /* ========== 1. 初始化基本配置 ========== */ cif_dev->hdr.hdr_mode = NO_HDR; // HDR 模式:默认关闭 cif_dev->inf_id = inf_id; // 接口 ID(MIPI/DVP) ​ /* ========== 2. 初始化同步机制 ========== */ /* 互斥锁:保护共享资源 */ mutex_init(&cif_dev->stream_lock); // 数据流操作锁 mutex_init(&cif_dev->scale_lock); // 缩放操作锁 mutex_init(&cif_dev->tools_lock); // 工具通道锁 /* 自旋锁:保护中断上下文的数据 */ spin_lock_init(&cif_dev->hdr_lock); // HDR 配置锁 spin_lock_init(&cif_dev->buffree_lock); // 缓冲区释放锁 spin_lock_init(&cif_dev->reset_watchdog_timer.timer_lock); // 看门狗定时器锁 spin_lock_init(&cif_dev->reset_watchdog_timer.csi2_err_lock); // CSI2 错误锁 ​ /* ========== 3. 初始化引用计数和状态标志 ========== */ atomic_set(&cif_dev->pipe.power_cnt, 0); // 管道电源引用计数 atomic_set(&cif_dev->pipe.stream_cnt, 0); // 管道流引用计数 atomic_set(&cif_dev->power_cnt, 0); // 设备电源引用计数 cif_dev->is_start_hdr = false; // HDR 启动标志 cif_dev->id_use_cnt = 0; // ID 使用计数 cif_dev->sync_type = NO_SYNC_MODE; // 同步类型 cif_dev->sditf_cnt = 0; // SDITF 计数 cif_dev->is_notifier_isp = false; // ISP 通知标志 cif_dev->sensor_linetime = 0; // 传感器行时间 cif_dev->early_line = 0; // 提前行数 cif_dev->is_thunderboot = false; // 快速启动标志 cif_dev->rdbk_debug = 0; // Readback 调试 ​ /* ========== 4. 初始化管道操作函数 ========== */ cif_dev->pipe.open = rkcif_pipeline_open; // 打开管道 cif_dev->pipe.close = rkcif_pipeline_close; // 关闭管道 cif_dev->pipe.set_stream = rkcif_pipeline_set_stream; // 设置流状态 /* 中断处理函数(根据芯片型号选择) */ cif_dev->isr_hdl = rkcif_irq_handler; // 默认中断处理 if (cif_dev->chip_id == CHIP_RV1126_CIF_LITE) cif_dev->isr_hdl = rkcif_irq_lite_handler; // Lite 版本的中断处理 ​ /* ========== 5. 根据芯片型号初始化数据流通道 ========== */ /* 老芯片(RK3399/RK3326 等):根据接口类型决定通道数 */ if (cif_dev->chip_id < CHIP_RV1126_CIF) { if (cif_dev->inf_id == RKCIF_MIPI_LVDS) { /* MIPI 接口:4 个虚拟通道 */ rkcif_stream_init(cif_dev, RKCIF_STREAM_MIPI_ID0); rkcif_stream_init(cif_dev, RKCIF_STREAM_MIPI_ID1); rkcif_stream_init(cif_dev, RKCIF_STREAM_MIPI_ID2); rkcif_stream_init(cif_dev, RKCIF_STREAM_MIPI_ID3); } else { /* DVP/BT656 接口:1 个通道 */ rkcif_stream_init(cif_dev, RKCIF_STREAM_CIF); } } else { /* 新芯片(RV1126/RK3568/RK3588):统一支持 4 通道 */ // <-- 对于本案例,走这条分支 rkcif_stream_init(cif_dev, RKCIF_STREAM_MIPI_ID0); rkcif_stream_init(cif_dev, RKCIF_STREAM_MIPI_ID1); rkcif_stream_init(cif_dev, RKCIF_STREAM_MIPI_ID2); rkcif_stream_init(cif_dev, RKCIF_STREAM_MIPI_ID3); } ​ /* ========== 6. 初始化额外的缩放和工具通道(仅部分芯片支持)========== */ if (cif_dev->chip_id == CHIP_RK3588_CIF || cif_dev->chip_id == CHIP_RV1106_CIF) { /* 缩放通道(Scale Channel):用于硬件缩放 */ rkcif_init_scale_vdev(cif_dev, RKCIF_SCALE_CH0); rkcif_init_scale_vdev(cif_dev, RKCIF_SCALE_CH1); rkcif_init_scale_vdev(cif_dev, RKCIF_SCALE_CH2); rkcif_init_scale_vdev(cif_dev, RKCIF_SCALE_CH3); ​ /* 工具通道(Tools Channel):用于调试/统计 */ rkcif_init_tools_vdev(cif_dev, RKCIF_TOOLS_CH0); rkcif_init_tools_vdev(cif_dev, RKCIF_TOOLS_CH1); rkcif_init_tools_vdev(cif_dev, RKCIF_TOOLS_CH2); } ​ /* ========== 7. 根据编译配置设置工作模式 ========== */ #if defined(CONFIG_ROCKCHIP_CIF_WORKMODE_PINGPONG) cif_dev->workmode = RKCIF_WORKMODE_PINGPONG; // 乒乓模式(双缓冲) #elif defined(CONFIG_ROCKCHIP_CIF_WORKMODE_ONEFRAME) cif_dev->workmode = RKCIF_WORKMODE_ONEFRAME; // 单帧模式 #else cif_dev->workmode = RKCIF_WORKMODE_PINGPONG; // 默认乒乓模式 #endif ​ /* ========== 8. 根据编译配置决定是否使用 Dummy Buffer ========== */ #if defined(CONFIG_ROCKCHIP_CIF_USE_DUMMY_BUF) cif_dev->is_use_dummybuf = true; // 使用虚拟缓冲区(避免溢出) #else cif_dev->is_use_dummybuf = false; #endif /* RV1106 特例:强制禁用 Dummy Buffer */ if (cif_dev->chip_id == CHIP_RV1106_CIF) cif_dev->is_use_dummybuf = false; ​ /* ========== 9. 初始化 Media Device ========== */ /* 设置 media device 名称(如 "rkcif-mipi-lvds") */ strlcpy(cif_dev->media_dev.model, dev_name(dev), sizeof(cif_dev->media_dev.model)); /* 获取 CSI Host 索引(从设备树别名) */ cif_dev->csi_host_idx = of_alias_get_id(node, "rkcif_mipi_lvds"); if (cif_dev->csi_host_idx < 0 || cif_dev->csi_host_idx > 5) cif_dev->csi_host_idx = 0; // 默认索引 0 cif_dev->media_dev.dev = dev; ​ /* ========== 10. 初始化 V4L2 Device ========== */ v4l2_dev = &cif_dev->v4l2_dev; v4l2_dev->mdev = &cif_dev->media_dev; // 关联 media device strlcpy(v4l2_dev->name, dev_name(dev), sizeof(v4l2_dev->name)); ​ /* 注册 V4L2 设备 */ ret = v4l2_device_register(cif_dev->dev, &cif_dev->v4l2_dev); if (ret < 0) return ret; ​ /* ========== 11. 注册 Media Device ========== */ media_device_init(&cif_dev->media_dev); ret = media_device_register(&cif_dev->media_dev); if (ret < 0) { v4l2_err(v4l2_dev, "Failed to register media device: %d\n", ret); goto err_unreg_v4l2_dev; } ​ /* ========== 12. 注册平台子设备(异步等待 DPHY)========== */ /* * ★ 关键函数:注册异步通知器 * - 解析设备树 endpoint * - 查找上游设备(DPHY) * - 注册 bound/complete 回调 */ ret = rkcif_register_platform_subdevs(cif_dev); if (ret < 0) goto err_unreg_media_dev; ​ /* ========== 13. 注册亮度统计设备(仅部分芯片)========== */ if (cif_dev->chip_id == CHIP_RV1126_CIF || cif_dev->chip_id == CHIP_RV1126_CIF_LITE || cif_dev->chip_id == CHIP_RK3568_CIF) rkcif_register_luma_vdev(&cif_dev->luma_vdev, v4l2_dev, cif_dev); ​ /* ========== 14. 将设备加入全局列表 ========== */ mutex_lock(&rkcif_dev_mutex); list_add_tail(&cif_dev->list, &rkcif_device_list); // 加入全局设备链表 mutex_unlock(&rkcif_dev_mutex); ​ return 0; // 初始化成功 ​ /* ========== 错误处理 ========== */ err_unreg_media_dev: media_device_unregister(&cif_dev->media_dev); err_unreg_v4l2_dev: v4l2_device_unregister(&cif_dev->v4l2_dev); return ret; }

可以看到大部分步骤(1~8)都是初始化和基础配置之类的代码,简单看看即可,重点看后面的代码。

第9步,初始化一个media_device,media_device管理着所有entity(实体);

第10步,初始化一个v4l2_device,并将其与media_device关联起来,最后注册此v4l2_device,v4l2_device管理着多个subdev(这里的内容在前面总结v4l2子系统时,已经讲解过,不清楚的可以看看前面的文章);

第11步,注册media_device,注册后,可生成创建/dev/mediaX(在本例中,代表/dev/media0)等设备节点;

第12步,我去除了错误检查等冗余代码,剩下的是比较重要的函数:

/** * rkcif_register_platform_subdevs - 注册 CIF 平台的所有子设备 * @cif_dev: CIF 设备结构体 * * 功能:这是 CIF 驱动初始化的核心函数,负责: * 1. 注册视频流设备(/dev/videoX) * 2. 注册缩放通道设备(硬件缩放功能) * 3. 注册工具通道设备(调试/统计功能) * 4. 启动异步通知机制(等待上游设备连接) * * 这个函数会被 rkcif_plat_init() 调用 * * 返回值:0表示成功,负数表示错误码 */ static int rkcif_register_platform_subdevs(struct rkcif_device *cif_dev) { int stream_num = 0, ret; ​ /* ========== 1. 注册视频流设备(核心功能)========== */ /* * 设置视频流数量为最大 MIPI 通道数 * RKCIF_MAX_STREAM_MIPI 通常是 4(对应 MIPI 的 4 个虚拟通道) */ stream_num = RKCIF_MAX_STREAM_MIPI; /* * 注册视频流设备: * - 创建 4 个 video 设备节点(/dev/video0 ~ /dev/video3) * - 每个节点对应一个 MIPI 虚拟通道 * - true: 表示支持多输入模式 * */ ret = rkcif_register_stream_vdevs(cif_dev, stream_num, true); ​ /* ========== 2. 注册缩放通道设备(可选功能)========== */ /* * 注册硬件缩放通道: * - RKCIF_MAX_SCALE_CH 通常是 4 * - 仅 RK3588/RV1106 等芯片支持 * - 功能:在硬件层面对图像进行缩放 * - 生成额外的 video 设备节点(如 /dev/video4 ~ /dev/video7) * */ ret = rkcif_register_scale_vdevs(cif_dev, RKCIF_MAX_SCALE_CH, true); ​ /* ========== 3. 注册工具通道设备(调试功能)========== */ /* * 注册工具/调试通道: * - RKCIF_MAX_TOOLS_CH 通常是 3 * - 用于图像质量分析、统计信息等 * - 仅部分高端芯片支持 * * 功能示例: * - 亮度直方图统计 * - 自动曝光/白平衡参考数据 * - 帧率统计 */ ret = rkcif_register_tools_vdevs(cif_dev, RKCIF_MAX_TOOLS_CH, true); ​ /* ========== 4. 初始化异步通知机制 ========== */ /* * 初始化完成量(completion): * - 用于同步通知 ISP 驱动 * - 当 CIF 准备好后,会通过这个机制通知 ISP * - ISP 驱动会等待(wait_for_completion)这个信号 */ init_completion(&cif_dev->cmpl_ntf); /* * 创建内核线程用于通知 ISP: * - 线程名:notifier isp * - 线程函数:notifier_isp_thread * - 参数:cif_dev * * 这个线程会等待 CIF 初始化完成,然后通知 ISP 驱动 */ kthread_run(notifier_isp_thread, cif_dev, "notifier isp"); ​ /* ========== 5. 注册异步子设备通知器(核心机制)========== */ /* * 启动 V4L2 异步通知机制: * - 解析设备树,查找上游设备(DPHY/Sensor) * - 注册 notifier,等待上游设备注册 * - 当 DPHY 注册时,触发 bound 回调 * - 当所有依赖都就绪时,触发 complete 回调 * * 这是我们之前详细讨论过的异步绑定机制! */ ret = cif_subdev_notifier(cif_dev); ​ /* ========== 6. 返回成功 ========== */ return 0; /* 总是返回 0(即使前面有错误也不检查)*/ }

rkcif_register_stream_vdevs、rkcif_register_scale_vdevs、rkcif_register_tools_vdevs基本是相同的套路,和前面讲过的内容大差不差,唯一需要注意的是这里注册的设备节点是/dev/video(区别于前面的/dev/subdev),以rkcif_register_stream_vdevs为例看一下代码。

/** * rkcif_register_stream_vdevs - 批量注册视频流设备节点 * @dev: CIF 设备结构体 * @stream_num: 需要注册的视频流数量(通常是 1-4 个) * @is_multi_input: 是否为多输入模式 * * 功能: * - 为每个数据流(stream)注册一个独立的 video 设备 * - 每个 stream 对应一个 /dev/videoX 节点 * - 例如:4 个 MIPI 虚拟通道会生成 /dev/video0 ~ /dev/video3 * * 返回值:0表示成功,负数表示错误码 */ int rkcif_register_stream_vdevs(struct rkcif_device *dev, int stream_num, bool is_multi_input) { struct rkcif_stream *stream; int i, j, ret; ​ /* 遍历所有需要注册的视频流 */ for (i = 0; i < stream_num; i++) { /* 获取当前流结构体 */ stream = &dev->stream[i]; /* 关联到父设备 */ stream->cifdev = dev; /* 注册单个视频流设备 */ ret = rkcif_register_stream_vdev(stream, is_multi_input); if (ret < 0) goto err; /* 注册失败,跳转到错误处理 */ } ​ return 0; /* 所有流注册成功 */ ​ err: /* 错误处理:注销已成功注册的流设备(代码未显示) */ return ret; } ​ /** * rkcif_register_stream_vdev - 注册单个视频流设备 * @stream: 视频流结构体 * @is_multi_input: 是否为多输入模式 * * 功能: * 1. 初始化 video_device 结构体 * 2. 设置设备操作函数(ioctl/fops) * 3. 初始化 vb2 缓冲区队列 * 4. 注册为 V4L2 video 设备(生成 /dev/videoX) * 5. 初始化 media entity(用于 media controller) * * 返回值:0表示成功,负数表示错误码 */ static int rkcif_register_stream_vdev(struct rkcif_stream *stream, bool is_multi_input) { struct rkcif_device *dev = stream->cifdev; struct v4l2_device *v4l2_dev = &dev->v4l2_dev; struct video_device *vdev = &stream->vnode.vdev; struct rkcif_vdev_node *node; int ret = 0; char *vdev_name; ​ /* ========== 1. 初始化 video_device 基本信息 ========== */ /* 设置设备名称(如 "rkcif_mipi_id0") */ strlcpy(vdev->name, vdev_name, sizeof(vdev->name)); /* 获取 vdev_node 结构体 */ node = vdev_to_node(vdev); /* 初始化互斥锁(保护设备访问) */ mutex_init(&node->vlock); ​ /* ========== 2. 配置 video_device 回调和属性 ========== */ /* 设置 ioctl 操作函数集(处理用户空间的 ioctl 调用) */ vdev->ioctl_ops = &rkcif_v4l2_ioctl_ops; /* 设置释放函数(设备卸载时调用) */ vdev->release = video_device_release_empty; /* 设置文件操作函数集(open/close/mmap/poll 等) */ vdev->fops = &rkcif_fops; /* 设备次设备号(-1 表示自动分配) */ vdev->minor = -1; /* 关联到父 V4L2 设备 */ vdev->v4l2_dev = v4l2_dev; /* 设置互斥锁(在 ioctl 时自动加锁) */ vdev->lock = &node->vlock; /* 设置设备能力标志 */ vdev->device_caps = V4L2_CAP_VIDEO_CAPTURE_MPLANE | /* 多平面视频捕获 */ V4L2_CAP_STREAMING; /* 支持流式传输 */ /* 将 stream 保存到 video_device 的私有数据中 */ video_set_drvdata(vdev, stream); /* 设置数据流方向(RX = 接收/捕获) */ vdev->vfl_dir = VFL_DIR_RX; ​ /* ========== 3. 初始化 media pad(用于 media controller)========== */ /* 设置 pad 类型为 SINK(数据输入端) */ node->pad.flags = MEDIA_PAD_FL_SINK; ​ /* ========== 4. 初始化 videobuf2 队列 ========== */ /* * 初始化缓冲区队列: * - 管理用户空间的视频缓冲区 * - 支持 MMAP/USERPTR/DMABUF 等内存类型 * - 处理 QBUF/DQBUF 操作 */ rkcif_init_vb2_queue(&node->buf_queue, stream, V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE); /* 关联缓冲区队列到 video_device */ vdev->queue = &node->buf_queue; ​ /* ========== 5. 注册 video 设备 ========== */ /* * 注册为 V4L2 video 设备: * - 分配设备号(/dev/videoX) * - 创建字符设备文件 * - 注册到 V4L2 核心框架 * * VFL_TYPE_VIDEO: 标准视频捕获设备 * -1: 自动分配设备号 */ ret = video_register_device(vdev, VFL_TYPE_VIDEO, -1); if (ret < 0) { v4l2_err(v4l2_dev, "video_register_device failed with error %d\n", ret); return ret; } ​ /* ========== 6. 初始化 media entity ========== */ /* * 初始化 media entity 的 pads: * - 用于 media controller 框架 * - 描述设备的输入/输出端口 * - 支持 media-ctl 工具查询拓扑 * * 参数说明: * - &vdev->entity: video_device 对应的 media entity * - 1: 这个 entity 有 1 个 pad * - &node->pad: pad 结构体数组 */ ret = media_entity_pads_init(&vdev->entity, 1, &node->pad); if (ret < 0) goto unreg; /* 初始化失败,注销 video 设备 */ ​ /* ========== 7. 注册成功 ========== */ return 0; ​ unreg: /* 错误处理:注销已注册的 video 设备 */ video_unregister_device(vdev); return ret; }

通过上述过程,内核最终会注册4个设备节点(/dev/video0~3);同理,rkcif_register_scale_vdevs会注册4个节点(/dev/video4~7);rkcif_register_tools_vdevs会注册3个节点(/dev/video8~10)

接着看看线程函数(notifier_isp_thread),这个函数有点特殊,他会异步注册后面要讲解的 cif_lvds2_sditf 驱动中的subdev,也就是:两个驱动配合工作,cif_lvds2_sditf 驱动负责分配、设置一个subdev,cif_lvds2驱动负责注册这个subdev

static int notifier_isp_thread(void *data) { // ...... subdev_asyn_register_itf(dev); // ...... return 0; } ​ static int subdev_asyn_register_itf(struct rkcif_device *dev) { // ...... sditf = dev->sditf[0]; // 取出cif_lvds2_sditf 驱动中分配的 subdev if (sditf && (!sditf->is_combine_mode) && (!dev->is_notifier_isp)) { ret = v4l2_async_register_subdev_sensor_common(&sditf->sd); dev->is_notifier_isp = true; } ​ return ret; } ​ int v4l2_async_register_subdev_sensor_common(struct v4l2_subdev *sd) { // ...... ​ v4l2_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); // 最终调用到此函数,异步注册subdev // ...... ​ sd->subdev_notifier = notifier; ​ return 0; ​ // ...... }

现在回过头看之前的RK3588数据流向过程(两段式接力),就明白了下图中 media0 下管理的最底下11个设备节点从哪里生成的了。

2. rkcif_get_reserved_mem

通过这个函数,我们可以看出,数据经过 cif 存入内存中。

/** * rkcif_get_reserved_mem - 获取 Thunderboot 预留内存 * @cif_dev: CIF 设备结构体 * * 功能:从设备树中获取专门为快速启动(Thunderboot)预留的内存区域 * * Thunderboot 应用场景: * - 车载后视摄像头:上电后需要立即显示画面 * - 倒车影像:不能等待系统完全启动 * - 安防监控:需要尽快开始录像 * * 工作原理: * 1. Bootloader 阶段已经配置好摄像头并使用这块内存 * 2. 内核启动时,CIF 驱动直接接管这块内存 * 3. 避免重新分配内存和重新初始化硬件的延迟 * * 返回值:0表示成功或无预留内存,负数表示错误 */ static int rkcif_get_reserved_mem(struct rkcif_device *cif_dev) { struct device *dev = cif_dev->dev; // 设备指针 struct device_node *np; // 设备树节点指针 struct resource r; // 资源结构体(保存地址和大小) int ret; ​ /* ========== 1. 从设备树查找预留内存节点 ========== */ /* * 解析设备树属性 "memory-region-thunderboot" * * 设备树示例: * rkcif: rkcif@fdce0000 { * memory-region-thunderboot = <&cif_thunderboot>; * ↑ phandle 引用 * }; * * 这个函数会找到 cif_thunderboot 对应的设备树节点 * * 注意:使用 "memory-region-thunderboot" 而非标准的 "memory-region" * 原因:Rockchip 的 Thunderboot 是专有的快速启动方案,使用专用属性名 */ np = of_parse_phandle(dev->of_node, "memory-region-thunderboot", 0); /* 如果没有找到该属性,说明未配置 Thunderboot 模式 */ if (!np) { /* 打印提示信息(这不是错误,只是没有使用 Thunderboot)*/ dev_info(dev, "No memory-region-thunderboot specified\n"); /* 返回 0 表示成功,但没有预留内存 */ /* 驱动会继续工作,使用普通的动态内存分配 */ return 0; } ​ /* ========== 2. 解析预留内存的地址和大小 ========== */ /* * 将设备树节点转换为 resource 结构体 * * 设备树示例: * cif_thunderboot: cif-thunderboot@70000000 { * reg = <0x0 0x70000000 0x0 0x4000000>; * ↓ ↓ ↓ ↓ * 高32位 低32位 高32位 低32位 * (起始地址) (大小) * }; * * 解析后的结果: * r.start = 0x70000000 // 物理地址起始位置(1792MB) * r.end = 0x73FFFFFF // 物理地址结束位置 * r.flags = IORESOURCE_MEM // 资源类型:内存 */ ret = of_address_to_resource(np, 0, &r); /* 如果解析失败,说明设备树配置有误 */ if (ret) { dev_err(dev, "No memory address assigned to the region\n"); return ret; // 返回错误码 } ​ /* ========== 3. 保存预留内存信息到 CIF 设备结构体 ========== */ /* 保存物理地址起始位置 */ cif_dev->resmem_pa = r.start; /* 计算并保存内存大小(end - start + 1)*/ cif_dev->resmem_size = resource_size(&r); /* resource_size() 宏定义: * #define resource_size(res) ((res)->end - (res)->start + 1) * 例如:0x73FFFFFF - 0x70000000 + 1 = 0x4000000 (64MB) */ /* 标记为 Thunderboot 模式 */ cif_dev->is_thunderboot = true; ​ /* ========== 4. 打印调试信息 ========== */ /* * 打印预留内存的物理地址和大小 * 示例输出: * "Allocated reserved memory, paddr: 0x70000000, size 0x4000000" * * 注意:使用 (u32) 强制转换,因为: * - resmem_pa 和 resmem_size 可能是 64 位类型 * - %x 格式化字符串期望 32 位整数 * - 在 32 位系统上这样转换是安全的 */ dev_info(dev, "Allocated reserved memory, paddr: 0x%x, size 0x%x\n", (u32)cif_dev->resmem_pa, // 物理地址 (u32)cif_dev->resmem_size); // 大小 ​ /* ========== 5. 返回成功 ========== */ return ret; // ret = 0(成功) }

驱动源码解析:cif_lvds2_sditf驱动

一、设备树节点

内核会根据此节点生成一个platform_device。

&rkcif_mipi_lvds2_sditf { status = "disabled"; port { mipi2_lvds_sditf: endpoint { remote-endpoint = <&isp0_vir1>; }; }; }; rkcif_mipi_lvds2_sditf: rkcif-mipi-lvds2-sditf { compatible = "rockchip,rkcif-sditf"; rockchip,cif = <&rkcif_mipi_lvds2>; status = "disabled"; };

二、驱动代码解析(probe函数)

drivers/media/platform/rockchip/cif/subdev-itf.c

/** * rkcif_subdev_probe - CIF 子设备(SDITF)探测函数 * @pdev: 平台设备指针 * * 功能:初始化 SDITF(Serial Data Interface)子设备 * * SDITF 作用: * - 处理串行数据接口(如 BT656/BT1120) * - 支持多路输入合并(Combine Mode) * - 作为 V4L2 子设备提供额外的数据通道 * * 返回值:0表示成功,负数表示错误码 */ static int rkcif_subdev_probe(struct platform_device *pdev) { struct device *dev = &pdev->dev; struct v4l2_subdev *sd; /* V4L2 子设备结构体 */ struct sditf_priv *priv; /* SDITF 私有数据 */ struct device_node *node = dev->of_node; int ret; /* ========== 1. 分配并初始化私有数据结构体 ========== */ /* 分配 SDITF 私有数据结构体内存 */ priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); if (!priv) return -ENOMEM; /* 内存分配失败 */ /* 保存设备指针 */ priv->dev = dev; /* ========== 2. 初始化 V4L2 子设备 ========== */ /* 获取 v4l2_subdev 结构体指针 */ sd = &priv->sd; /* * 初始化子设备: * - 设置操作函数集(sditf_subdev_ops) * - 包含 pad 操作、video 操作等回调 */ v4l2_subdev_init(sd, &sditf_subdev_ops); /* * 设置子设备标志: * V4L2_SUBDEV_FL_HAS_DEVNODE: * - 创建独立的设备节点(/dev/v4l-subdevX) * - 允许用户空间直接访问子设备 * - 支持独立的 ioctl 控制 */ sd->flags |= V4L2_SUBDEV_FL_HAS_DEVNODE; /* 设置子设备名称(用于调试和日志)*/ snprintf(sd->name, sizeof(sd->name), "rockchip-cif-sditf"); /* 关联设备指针 */ sd->dev = dev; /* ========== 3. 保存子设备到平台设备私有数据 ========== */ /* * 将 media_entity 保存到平台设备: * - 后续可以通过 platform_get_drvdata() 获取 * - 用于 media controller 拓扑构建 */ platform_set_drvdata(pdev, &sd->entity); /* ========== 4. 关联到父 CIF 设备 ========== */ /* * 查找并关联到父 CIF 设备: * - 通过设备树的 rockchip,cif 属性查找 * - 建立父子设备关系 * - 共享硬件资源(寄存器、时钟等) * * 设备树示例: * rkcif_mipi_lvds2_sditf: rkcif-mipi-lvds2-sditf { * rockchip,cif = <&rkcif_mipi_lvds2>; ← 指向父设备 * }; */ ret = rkcif_sditf_attach_cifdev(priv); if (ret < 0) return ret; /* 关联失败,返回错误 */ /* ========== 5. 解析设备树配置(Combine Mode)========== */ /* * 读取 combine-index 属性: * - 用于多路输入合并模式 * - 例如:将 2 路 BT656 合并为 1 路输出 * * 设备树示例: * rkcif_sditf: cif-sditf { * rockchip,combine-index = <0>; * }; */ ret = of_property_read_u32(node, "rockchip,combine-index", &priv->combine_index); if (ret) { /* 没有配置 combine-index,使用普通模式 */ // <-- 本例子走此分支 priv->is_combine_mode = false; priv->combine_index = 0; } else { /* 配置了 combine-index,启用合并模式 */ priv->is_combine_mode = true; } /* ========== 6. 初始化 Media Controller ========== */ /* * 初始化子设备的 media 实体: * - 创建 pads(输入/输出端口) * - 注册到 media controller * - 支持 media-ctl 工具查询 */ ret = rkcif_subdev_media_init(priv); if (ret < 0) return ret; /* ========== 7. 启用电源管理 ========== */ /* * 启用 Runtime PM(运行时电源管理): * - 设备不使用时自动进入省电模式 * - 使用时自动唤醒 * - 框架会调用 runtime_suspend/resume 回调 */ pm_runtime_enable(&pdev->dev); /* ========== 8. 初始化成功 ========== */ return 0; }

都是老套路了,分配、设置subdev结构体,但是注册在哪里?

SDITF 子设备的注册不是在 probe 阶段完成,而是在父 CIF 设备(cif_lvds2_sditf 驱动)的异步绑定回调中完成。

也就是下图中红框部分的节点:

上图剩余节点在下篇文章中讲解。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/1215434.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2026 年 1 月铝材加工设备与材料厂家推荐榜单:压块机、铝棒、铝管、研磨铝棒管、挤压棒、铝合金,飞象精铝等源头实力厂家全景解析

2026 年 1 月铝材加工设备与材料厂家推荐榜单:压块机、铝棒、铝管、研磨铝棒管、挤压棒、铝合金,飞象精铝等源头实力厂家全景解析 随着高端制造业向精密化、智能化方向持续演进,铝材作为现代工业的“骨骼”与“肌肉…

基于机器学习的道路交通状态分析(代码+报告+数据)(设计源文件+万字报告+讲解)(支持资料、图片参考_相关定制)_文章底部可以扫码

基于机器学习的道路交通状态分析(代码报告数据)(设计源文件万字报告讲解)&#xff08;支持资料、图片参考_相关定制&#xff09;_文章底部可以扫码1、本项目利用关联规则算法挖掘分析影响交通状况的原因&#xff0c;再利用随机森林算法完成交通状况预测。 2、由于计算机科学的快…

我们将讨论如何在 React 中使用表单单元素与 Reac

React 表单与事件 本章节我们将讨论如何在 React 中使用表单。 HTML 表单元素与 React 中的其他 DOM 元素有所不同,因为表单元素生来就保留一些内部状态。 在 HTML 当中&#xff0c;像 <input>, <textarea>, 和 <select> 这类表单元素会维持自身状态&…

数据变化(原始数据—数据清洗—特征工程)

数据清洗步骤 用户行为数据缺失值处理 user_id、item_id是关联用户和商品的唯一标识&#xff0c;缺失后无法建立有效关联behavior_type是核心行为标签&#xff0c;缺失无法定义交互类型timestamp是时间序列分析的基础&#xff0c;缺失影响序列特征的准确性直接删除比填充更可靠…

Emacs 折腾日记(三十五)——归档

在前几篇文章中,我们经历了 GTD 流程中的收集想法、制定计划、以及执行和记录计划的过程,现在我们继续后续的流程,也就是最后的回顾和归档。 当日回顾 在我个人实践 GTD 的流程中,前一晚会做这些事情:回顾一下今天…

2026 年 1 月投饵船厂家推荐排行榜,无人投饵船,自动投饵船,遥控投饵船,智能投料船,水产养殖自动化精准投喂解决方案精选

2026年1月投饵船厂家推荐排行榜:水产养殖自动化精准投喂解决方案精选 随着全球水产养殖业向集约化、智能化方向加速转型,传统依赖人工作业的投喂模式正面临效率瓶颈、成本高企与精准度不足等多重挑战。在此背景下,以…

2026 年 1 月液相色谱厂家推荐排行榜,色谱柱/液相色谱仪/二维液相色谱/UPLC/制备液相色谱,精准分离分析技术源头实力解析

2026 年 1 月液相色谱厂家推荐排行榜:色谱柱/液相色谱仪/二维液相色谱/UPLC/制备液相色谱,精准分离分析技术源头实力解析 在生命科学、药物研发、食品安全、环境监测等众多前沿与基础研究领域,液相色谱技术作为不可…

xFUZZ: A Flexible Framework for Fine-Grained, Runtime-Adaptive Fuzzing Strategy Composition

概要:现有灰盒模糊测试工具要么策略固定、要么只能整工具切换,无法随目标程序和测试阶段 的变化而细粒度、运行时地调整策略;我们提出 xFUZZ,首次把输入调度、变异调度等核 心组件全部做成可热插拔的插件,并用滑动…

2026 年 1 月古建瓦厂家推荐排行榜,中式古建瓦,园林古建瓦,仿古瓦定制,古建瓦供应,匠心传承与建筑美学融合之选

2026年1月古建瓦厂家推荐排行榜:中式古建瓦、园林古建瓦、仿古瓦定制与供应的匠心之选 在当代建筑文化复兴与文旅产业蓬勃发展的浪潮下,古建瓦作为承载中式建筑灵魂的重要构件,其市场需求持续升温。无论是历史古迹的…

【VLM】Visual Merit or Linguistic Crutch? 看DeepSeek-OCR

note 论文证明了 DeepSeek-OCR 的强性能很大程度上来自语言模型的“语言先验”&#xff0c;而非真正的深度视觉理解&#xff0c;并指出这种依赖使得它在语义扰动或超长上下文下表现脆弱。 文章目录note一、Visual Merit or Linguistic Crutch?二、实验RQ1&#xff1a;句子级语…

Go进阶之反射

Go语言是静态类型语言.比如int float32 []byte32等等.每个变量都有一个静态类型.并且在编译的时候就已经确定了.type Myint int var i int var j Myint变量i和j不是相同类型.因为二者拥有不同的静态类型.尽管二者底层的类型都是int.但在没有类型转换的情况下是不可以相互赋值的…

2026 年 1 月仿古瓦厂家推荐排行榜,中式仿古瓦,小青瓦仿古瓦,定制仿古瓦,古建屋面瓦公司推荐,甄选匠心工艺与耐久品质!

2026年1月仿古瓦厂家推荐排行榜:甄选匠心工艺与耐久品质 随着文化自信的回归与文旅产业的蓬勃发展,中式仿古建筑及传统风貌街区建设迎来了新一轮热潮。作为承载建筑神韵与历史文脉的关键元素,仿古瓦,尤其是中式仿古…

.bat脚本新建文件夹【项目结构】

一、源码 当有原来的同名文件时保留原来的文件夹【仅限文件夹同名保留】 NewFile.bat echo off chcp 65001 > nul 2>&1 setlocal enabledelayedexpansion:: 获取脚本所在的文件夹路径 set "script_dir%~dp0" echo echo 目标创建路径&#xff1a;%script_…

2026 年 1 月工业醇类及溶剂厂家推荐榜单:乙醇/无水乙醇/二丙酮醇/异丙醇/乙二醇/正丁醇/工业酒精/甲醇/醇酸漆稀释剂/丙二醇甲醚等源头实力厂家精选

2026 年 1 月工业醇类及溶剂厂家推荐榜单:乙醇/无水乙醇/二丙酮醇/异丙醇/乙二醇/正丁醇/工业酒精/甲醇/醇酸漆稀释剂/丙二醇甲醚等源头实力厂家精选 在精细化工与制造业的庞大体系中,工业醇类及溶剂扮演着不可或缺的…

2025年教我学英语 - 穿、衣

2025年教我学英语 - 穿、衣1、穿、戴 - wear [weə(r)] 穿、着 - put on [ˈpʊt ɒn] 穿、戴(状态)-dress [dres] 穿、套 - wear in [ˈweə(r) ɪn] 试穿 - try on [ˈtraɪ ɒn]2、衣服 - clothes [kləʊz] 外套…

探索Matlab水下图像处理与GUI界面构建之旅

数字图像处理matlab水下图像处理&#xff0c;gui 界面运用&#xff0c;有讲解报告 在数字图像处理的广袤领域中&#xff0c;水下图像处理因其独特的挑战而备受关注。Matlab作为一款强大的工具&#xff0c;为我们解决水下图像处理难题提供了丰富的资源和便捷的途径。同时&#…

快过年了 , 我就简简单单写一个总结吧 ! | 马年快乐 !

1. 项目正式上线了 书接上回 , 经过一个月的不懈努力 , 我和另一个哥们终于把这个项目 (帮学校做校园宿舍报修系统小程序) 拿下了. 但是这边宿管长这边又出了点状况: 因为这个项目是为学校做公益,没有问宿管长要一分钱. 但是服务器的钱肯定是要宿管长联系学校那边给报销一下…