
做过几年高通平台 camera 驱动最常被新来的同事问到一个问题应用层调了connectDevice之后代码到底是怎么一步步走到内核的这个问题看起来简单但真要讲清楚得从 HAL、CHI、CamX、V4L2、msm_camera 一路拆到 CSIPHY 和 CCI。我早期也被这条链路绕晕过后来啃完代码又跟了几次线上问题才算把“高通 camera open”从 HAL 到 kernel 的主线串起来。这篇文章就按我的理解记一份完整流程既有模块划分、调用链也有我自己踩过的坑。1. 拆开“打开摄像头”这层壳高通相机架构分层与模块清单1.1 从应用层到内核中间隔着几个世界Android 相机应用的openCamera走到硬件要经过好几层状态机。最上层是应用进程的 CameraManager它通过 Binder 与系统的 CameraService 通信CameraService 再通过 HIDL/AIDL 调用到供应商实现的 Camera HAL。高通的 Camera HAL 在 2019 年之后的新平台基本全面切换到 CamXCamera Extension Framework旧平台常见的 mm-camera 已经逐步退役。CamX 下面还有一层 CHI (Camera HAL Interface) override用来做厂商定制再往下就是 Linux kernel 里的 camera 驱动主要是 msm_camera 相关驱动。这条链路上的每一层职责非常清晰CameraService 管权限、策略、生命周期CamX 管 pipeline 状态机、request 调度、设备元数据CHI override 提供厂商逻辑比如 feature 开关、算法适配kernel 的 msm_camera 驱动管硬件寄存器、电源时序、I2C/CSI 收发。所以“打开摄像头”这句话在不同层的人嘴里指的是完全不同的动作。搞驱动的兄弟听到 open脑子里是 subdev open、gpio 上电、i2c 读 sensor id搞 HAL 的同事听到 open脑子里是 CamxSession 创建、Usecase 加载、Pipeline 拓扑生成。两边如果不站在同一个边界上对齐排查问题的时候很容易各说各话。1.2 各层的关键代码模块与源码位置高通平台相机相关代码的位置不同项目、不同内核版本会有些差异但大体可以按下面的清单去翻层核心模块典型路径HAL 入口CamX 的 HAL3Modulevendor/qcom/proprietary/camx/src/core/CAMX 框架CamxSession / Pipeline / Nodevendor/qcom/proprietary/camx/src/core/CHI overrideChiModule / Feature 定制vendor/qcom/proprietary/chi-cdk/Kernel V4L2msm_camera_v2kernel/techpack/camera/drivers/cam_*/ 或 drivers/media/platform/msm/camera_v2/Sensor 驱动msm_sensor上述路径下的 cam_sensor/msm_sensor*.c新平台的内核里常见的是cam_sensor、cam_csiphy、cam_cci、cam_isp这一套字母驱动的命名方式。它们虽然和老的msm_camera驱动目录不一样但核心职责是一样的给上层提供统一的 V4L2 subdev 接口内部再做电源、I2C 和 CSI lane 的控制。1.3 open 这件事到底是谁在负责你需要有一个基本认知Android 应用层的openCamera并不是让 sensor 立刻上电出图。它只是创建了 HAL 层的 session真正让硬件启动的过程分布在configureStreams和processCaptureRequest里。不过从代码链路上看open 阶段会做大量铺垫枚举 sensor 能力、创建 pipeline、校验 stream 参数有些平台为了读取 EEPROM 或者做 sensor 探测也会在内核里把 CCI 先拉起来。所以这篇文章里的“open 代码流程”我按更广义的方式展开从 HAL 模块 open到 session/pipeline 初始化再到内核 subdev open 和 sensor power up最后落到第一次 sensor id 读取。这条链路完整走通相机才算真正“打开”了。2. HAL 侧主线CamxSession 创建、Pipeline 拓扑与 SensorNode 初始化的先后顺序2.1 CamX 的上电入口HAL3Module 与 CamxEntryCamX 是作为 Android Camera HAL3 的供应商实现加载的。进程启动时HAL3Module::Open会被调用随后创建CamxEntry初始化日志、内存管理、HWLHardware Layer等基础模块。我刚开始读这段代码时有个误区以为 HAL 里也会像 SDK 一样直接open(/dev/video0)。实际上 CamX 并不会在 HAL 模块加载时马上打开内核节点它会先通过media controller API枚举当前平台的 camera 子设备拓扑比如有哪些 sensor entity、CSIPHY entity、IFE entity以及它们的链路关系。这一步类似于在纸上先画一张拓扑图后续干活的时候按图索骥。枚举完成后CamX 会为每个逻辑 camera ID如后置主摄、前置、广角建立对应的静态 capabilities。这些 capabilities 一部分来自 CHI override 的 XML 配置一部分来自内核 sensor 驱动的能力上报。open 一个摄像头本质上就是把这些静态信息组装成 HAL3 Device 实例。2.2 Session 和 Pipeline相机世界的“项目”与“工位”当上层真的发起 open 时CamX 会走一套典型流程CamX::Session::Create创建 session 对象Session::Open加载该 camera ID 对应的 usecase场景定义usecase 中定义了需要哪几条 pipeline比如 preview pipeline、snapshot pipelinePipeline::Create根据拓扑描述创建节点数组Node 列表每个 Node 再各自创建内部资源和线程。你可以把 Session 理解成一个“项目总负责人”Pipeline 是项目里的一条条生产线Node 是生产线上各个工位。一次打开后置摄像头可能要同时创建 preview 和 snapshot 两条 pipeline两条 pipeline 共享同一个 sensor所以 HAL 层必须由 Session 统一协调避免两个 pipeline 同时去操作 sensor 而打架。管线里的 Node 类型有很多SensorNode、FlashNode、CSIPhyNode、IFENode、IPENode、StatsProcessingNode 等。open 阶段最重要的就是 SensorNode 的绑定和初始化。2.3 SensorNode 初始化从 Create 到 Capability 读取SensorNode 是 CamX 里和 sensor 硬件直接对应的节点。它的创建路径大致是这样的Pipeline::CreateNode - Node::Create - SensorNode 构造函数 - Node::Initialize - SensorNode::Initialize初始化过程中SensorNode 会调用GetSensorCapabilities把内核驱动的能力上报解析成 CamX 内部通用的 SensorCapabilities 结构。这里面包括 sensor 名字、CSI lane 数、I2C 地址、分辨率列表、曝光增益范围等。这些能力来自哪里内核驱动在注册 subdev 时会通过 v4l2 control 或者私有 ioctl 向上层返回msm_camera_sensor_info和msm_sensor_output_info。我简单画一个 HAL 侧调用序列的文字版HAL3Device::open - Session::Open - CreateUsecase - CreatePipeline - Node::Create - SensorNode::GetSensorCapabilities - V4L2_GetSensorInfo (private ioctl)这里有一个非常关键的工程意识SensorNode 的 open 不一定等于内核 subdev 的 open syscall。CamX 为了性能通常会缓存已经打开的内核 fd避免每个 request 都走一遍 open/close。所以你在 HAL 层看到的 open 更多是状态机在走真正的 open 动作可能发生在SensorNode::OnOpen、SensorNode::ExecuteProcessRequest第一次下发的时候。我在代码里经常搜的一个入口是CamxSession::Open这里能看到整个 usecase 加载逻辑。另一个常用入口是SensorNode::Open如果发现 HAL 层 open 卡住十有八九问题在这两个函数附近的阻塞等待中。3. 内核边界V4L2 子设备模型与 msm_camera 驱动的工作流3.1 内核节点与 subdev 的组织方式内核侧的 msm_camera 驱动是基于 Linux V4L2 框架的。一个典型的相机硬件拓扑会有这些子设备sensor subdev代表图像传感器负责 I2C 寄存器读写、供电、初始化序列csiphy subdev负责 CSI2 PHY lane 配置cci subdevI2C 控制器驱动提供可靠的 SCL/SDA 时序eeprom subdev负责读取校准数据actuator subdev控制 VCM 马达对焦flash subdev控制闪光灯。这些 subdev 会在系统启动时依次 probe然后注册到 media controller 上。用户态看到的节点通常是/dev/video0、/dev/v4l-subdev0、/dev/media0。其中/dev/video0作为主视频节点/dev/v4l-subdev0等作为硬件功能节点。CamX 在 HAL 层枚举设备本质上就是通过 media controller API 把这些 subdev 的 entity 和 link 关系读出来。要注意的是高通的 sensor 驱动通常不是主动暴露出很多独立的操作节点它会把大部分逻辑收敛到一个 video 节点的 private ioctl 上。原因是相机系统的控制流非常复杂如果完全照搬标准 V4L2 的S_FMT、S_PARMHAL 会绕很多弯路。高通这套设计在实际 bringup 时效率更高代价是代码阅读门槛高一些。3.2 从 subdev open 到 msm_sensor_open 的调用链内核驱动的 subdev open 调用链大概是这样的v4l2_subdev_open (core) - v4l2_device_call_all(subdev, open) - msm_sensor_subdev_open - 初始化 sensor_ctrl_t 内部状态 - 获取 regulator/gpio/clock 的 dts 配置 - 获取 CCI 控制器的上游引用msm_sensor_subdev_open本身不会马上把 sensor 的电给上去。它更像一个“战前准备”函数把设备树里的电源配置解析出来存到驱动私有结构体里同时确认 CCI 设备是否 ready。sensor 真正上电发生在msm_sensor_power_up这个函数通常会由s_power回调触发也可能由私有 ioctl 直接触发。我经常用一条命令观察内核 open 动作echo file drivers/media/platform/msm/camera_v2/msm_sensor/*.c p /sys/kernel/debug/dynamic_debug/control这样打开 sensor 时内核会把msm_sensor_platform_probe、msm_sensor_subdev_open、msm_sensor_power_up等关键函数执行路径打到 dmesg 里。动态 debug 的日志往往能直接告诉你当前停在哪个函数特别是时序问题导致卡死的时候比盲猜 HAL 日志高效得多。3.3 CSIPHY 和 CCI 在 open 阶段帮了什么忙open 阶段 CSIPHY 不一定会做什么复杂动作主要是确认 CSI lane 状态以及把 PHY 上可能残留的配置清掉。CCII 则一定会参与一次重要工作读 sensor id。CCI 驱动是内核里负责 I2C 收发的一层它通过内部的cci_read/cci_write函数完成 I2C 时序。传感器是挂在 CSI 接口旁边的 CCI 总线上的也就是摄像头模组的 I2C 引脚接到主控的 CCI 控制器上。内核驱动得先用 CCI 发送读寄存器命令传感器才会把 ID 寄存器返回值放到数据线上。这个过程中CCI 内部的 FIFO 和 IRQ 状态必须正确否则就会 I2C NACK 或者超时。一个常见的坑是同一个 CCI 控制器下同时挂了前后摄两个 sensor驱动初始化时如果两个 module 抢占同一个 CCI 设备而没有加锁就会出现读写总线混乱表现为 sensor id 时而能读出来、时而读不出来。高通驱动里一般会有sensor_ctrl_t-cci_client这类结构来标识 I2C 主控制器编号排查时要重点看它是否被错误复用。4. 数据与控制流的汇合点从 ioctl 到 power sequence 与 sensor id 读取4.1 用户态到底怎么把命令交给内核HAL 层与内核驱动之间最主要的交互通道就是 ioctl。CamX 虽然有封装好的 V4L2 API但底层最终会走两类 ioctl标准 V4L2 ioctl比如VIDIOC_SUBSCRIBE_EVENT、VIDIOC_STREAMON高通私有 ioctl在v4l2_subdev基础上扩展的VIDIOC_MSM_*系列命令。常见的私有 ioctl 大致包括ioctl 名称作用VIDIOC_MSM_SENSOR_GET_INFO获取 sensor 基本信息VIDIOC_MSM_SENSOR_SET_POWER请求上电/掉电VIDIOC_MSM_SENSOR_READ_I2C读取 sensor 寄存器VIDIOC_MSM_SENSOR_WRITE_I2C写入 sensor 寄存器VIDIOC_MSM_CSIPHY_ACQUIRE占用 CSIPHY 资源在内核 subdev ioctl 回调里会根据 cmd 的_IOC_TYPE(cmd)和_IOC_NR(cmd)分发到msm_sensor_private_ioctl。这个函数是 sensor open 阶段最核心的入口之一。你常会在日志里看到类似MSM_SENSOR_GET_SENSOR_INFO的字符串就是这里打出来的。4.2 power sequenceopen 阶段最容易被坑的硬骨头虽然最终的 sensor id 读取只需要让 sensor 供电稳定、MCLK 有时钟但一颗 sensor 从断电状态到能响应 i2c中间要满足严格的上电时序。高通平台上这个时序逻辑主要放在设备树和 msm_sensor 驱动里。设备树里常见的字段包括svdd-supply、vdd-supply、vaf-supply等供电配置gpios数组包含 reset、pwdn、mclk 等 GPIOqcom,cam-power-settings描述 step by step 的上电动作序列。我记得第一次调试一颗新 sensor 时sensor id 一直读出来是 0xFF。示波器看供电、MCLK、Reset 波形好像都有后来对比 datasheet 才发现 reset 引脚的极性定义反了。内核驱动按 dts 里配置的 deassert 电平去拉高结果恰好把 sensor 保持在了 reset 状态自然不响应 i2c。从那以后我 open 阶段遇到读不到 id 的第一反应就是先把 power sequence 的每一步 GPIO 极性和延时拉出来核对而不是急着查 i2c 波形。4.3 sensor id 读取的一次完整旅程完整走一遍 sensor id 读取能帮你把 HAL 到 kernel 的所有关键点串起来。流程大致是这样的CamX 的 SensorNode 发起能力查询向 CHI 询问 sensor 的 id register 配置SensorNode 通过 V4L2 private ioctl 向内核 sensor subdev 下发读寄存器命令kernel 的msm_sensor_cci_i2c_read构造一条 CCI messageCCI 驱动把 message 写入硬件 FIFO并等待读完成中断中断处理函数把读到的数据放回 message buffer内核再将返回值拷贝给用户态CamX 拿到数据和配置的期望 id 对比。这个链路里每一步都可能出问题。我在实际定位中总结过一个经验如果读出来的 id 是 0xFF优先查上电时序如果读出来是 0x00优先查 i2c 地址是否配错或者 sensor 是否被拉到 reset如果读出来的值和预期不一致但很稳定查驱动与 datasheet 的寄存器地址映射。5. 亲自跟一遍代码一次 open 失败的典型排查链路与工程经验5.1 我用过的日志开关和定位工具要跟 open 流程用好日志能省一半时间。CamX 侧有几个常用的日志控制设置 property 打开 CamX debug log修改/vendor/etc/camera/camxoverridesettings.txt中的 log 级别通过persist.vendor.camera.debug.logs打开更多 HWL 层日志。内核侧主要靠 dynamic debug。我一般会这样开echo file drivers/media/platform/msm/camera_v2/* p /sys/kernel/debug/dynamic_debug/control dmesg -w如果手上有硬件调试工具示波器是标配。别觉得这是硬件工程师的事驱动要确认 power sequence 有没有完全按 dts 配置走逻辑分析仪抓一下 MCLK 和 Reset 的相对延时比看代码猜靠谱得多。5.2 案例后置摄像头 open 后黑屏日志指到 CCI 超时之前遇到一个 case切换前后摄几次之后后摄开始随机打不开。抓 logcat 发现 CamX 的 Session open 报错HAL 层停在SensorNode::GetSensorCapabilities再抓 dmesg看到 cci_read 对应寄存器返回-EIO。第一反应是怀疑 CCI 总线被占死。因为前后摄挂在同一路 CCI但设备树里给后摄配置的 cci_client 指向了 controller 0前摄指向 controller 1正常应该互不影响。继续追日志发现报错前有一次cci_write超时随后 CCI 驱动的 IRQ handler 没有把 FIFO 清理干净导致后续命令全部超时。问题根因出在某次掉电时序太快sensor 没有完全释放总线而 CCI 驱动在连续错误后没有做总线恢复。这个 case 后续是通过两条手段共同解决的一是在驱动里对 CCI 读操作增加失败重试和 FIFO flush二是调整掉电时序保证 sensor 完全掉电后再操作上一条总线上的其他设备。它给我的教训是open 阶段的问题不见得只在 open 阶段看得到很多是反复开关之后暴露的竞态问题。5.3 新平台 bringup 第一颗 sensor 时的 checklist如果把“open 代码流程”落到实际工作最重要的一次应用就是在 bringup 第一颗 sensor 时。我给自己定过一个 checklist分享出来供参考先确认内核里 sensor probe 成功subdev 注册到 media controller通过 media-ctl 或类似工具查看 entity 链路是否完整确认 sensor id 寄存器地址和期望值如果 dts 里配了 eeprom先跳过 eeprom确认 CCI controller、I2C 地址、以及是否被其他 sensor 占用确认 GPIO 编号、电源 regulator 名称和极性用示波器抓一遍 power sequence在 HAL 侧先开一个极简 pipeline只配置 preview不要一开始就开复杂的 HDR/多帧 feature逐步打开 CamX 日志从 Session open 到第一次 SensorNode request 顺序排查。这套流程看起来繁琐但它能把“通 code 流程”变成“可复现的工程手段”。我见过太多同事遇到 open 问题直接开始乱改 dts结果越改越乱。代码流程的正确意义不是背下调用链而是当现象出现时你知道该在哪一层设断点、查日志、量波形。最后聊一点个人感受高通这套 camera open 流程无论 HAL 还是 kernel都已经被封装得相当复杂了但核心边界始终清晰。把 V4L2 subdev 当成 HAL 和硬件之间的海关把 CCI/CSIPHY 当作通关的车道把 power sequence 当作每个传感器自己的护照排查问题的时候就不容易迷路。我个人每次开一颗新 sensor都会先从 SensorNode 到 msm_sensor 走一遍打印定位到具体是哪一段没通再针对性地看时序和寄存器配置这个习惯到现在帮我省了很多和硬件、算法同事来回确认的时间。