做高通平台camera bringup的人第一关大概率都卡在sensor驱动上。这活儿在别人眼里特别“玄学”——同样是上电、读ID、配timing三个步骤有人一次点亮有人能把MIPI调崩、把I2C调超时、把GPIO调出冲突最后对着黑屏怀疑人生。早几年我在msm8953和sdm660上跟sensor驱动缠斗了很久后来切到SM8250的CamX架构又把这些经验翻新了一遍。这篇文章就把我从老平台到新平台积累的sensor驱动知识完整梳理一遍包括驱动架构差异、代码套路、配置方式、调试方法以及几段实打实的踩坑经历。不管你是刚接手camera bringup的驱动新人还是已经在V4L2里摸爬滚打了一阵子但对CamX还不熟这都应该能帮你省下一两周的摸索时间。1. 高通camera架构的演变写sensor驱动前必须先看懂的背景1.1 老MSM架构下的驱动生态V4L2 msm_camera_v2在msm8996、msm8953、sdm660、sdm845这一代平台上高通的camera方案还是基于Linux内核的V4L2框架。整个camera驱动链路是这样的HAL层向下通过V4L2的video节点和subdev节点发ioctl内核侧对应着drivers/media/platform/msm/camera_v2这个目录。这个目录里写得明明白白sensor子目录专门承载sensor驱动cci目录是CCI控制器的驱动csiphy是MIPI物理层驱动isp、vfe这些管图像处理。这里的sensor驱动本质上是一个V4L2 subdev。以我当年调过的sensor驱动为例核心文件msm_sensor.c是个通用框架注册好subdev ops之后厂商只需要在这个框架基础上做一层自己的驱动实现电源控制、i2c读写、模式切换等回调函数。驱动会从设备树里解析GPIO、供电、CCI通道等配置再根据hal层下发的命令去执行power up、sensor init、stream on/off。这个阶段sensor驱动的代码量其实不大真正让人头疼的是设备树和上电时序。设备树里把摄像头节点、cci控制器、csiphy编号、eeprom、actuator的依赖关系全部串起来任何一个节点的属性写错sensor根本不会被probe成功。而这个probe成功与否不会像普通设备那样报个“没有这个设备”给你看很多时候是camera服务启动后一直黑屏或者demand probe时kernel log里打一条不痛不痒的错误。1.2 CamX时代的分层哲学从内核算力转向用户态控制到了SM8150、SM8250这一波平台高通把camera框架整体重写这就是CamXCamera eXtension和CHICamera Hardware Interface。一句话概括这个变化sensor驱动的“灵魂”从内核态搬到了用户态内核只保留最基本的CCI读写和电源控制能力。CamX的分层大致是最上层是Android Camera HAL 3往下是CamX核心框架它负责pipeline调度、request分发、节点管理再往下就是sensor、eeprom、actuator、flash这些具体组件。用户态侧的代码里有一个关键目录叫chi-cdk里面oem/qcom/sensor目录下每一个sensor都有独立的模块用来实现sensor的上电时序、i2c寄存器序列、分辨率切换这些逻辑。这里有个特别容易让人迷惑的地方即便在CamX时代内核里依然存在sensor相关的驱动但它的职责被砍得很窄主要是CCI控制器的i2c读改写、GPIO的下发、上电下电的原语操作。真正那颗sensor的初始化序列、曝光增益配置、mode切换参数全部在用户态通过CSensorModule这个C类实现配上一个JSON或者头文件来描述这颗sensor的所有能力。对新入行的人来说这个转型带来的最大影响不是技术变难了而是“调试入口变了”。以前调sensor驱动拉个kernel log看dmesg现在要同时看logcat、CamX log、内核log三层日志。以前改一个分辨率参数要重新编译内核现在更多是改JSON后替换vendor镜像。这也是我写这篇文章最想强调的一点你在网上搜到的很多资料还是老平台的套路但高通平台已经全面转向CamX你得搞清楚自己手里的平台是哪一代否则照着老教程去新平台上找msm_sensor.c是找不到的。2. 老平台内核态sensor驱动那些年我们这样写驱动2.1 一份sensor驱动的主要文件和设备树绑定关系老平台上的sensor驱动虽然听起来是“写代码”但实际工作中很大一部分是在核对设备树和驱动属性是否对得上。先说目录结构drivers/media/platform/msm/camera_v2/sensor/下面有msm_sensor.c作为通用驱动框架它定义了一套msm_sensor_subdev_ops提供s_power、s_ctrl、s_config一类的回调。厂商的sensor驱动要做的就是实现自己的power setting数组、slave address、sensor ID等信息然后注册到subdev系统里。设备树里对应的是一长串camera节点。以msm8953为例每个sensor对应一个qcom,camera0这样的子节点它挂在cci节点之下。节点里通常会写cell-indexsensor实例编号qcom,sensor-position前后摄位置0是后摄1是前摄qcom,cci-master走CCI总线0还是1这直接决定i2c挂在哪条总线上cam_vdig-supply、cam_vio-supply等sensor需要的各路电源qcom,gpio-no-clock-req之类的GPIO配置MCLK、RESET、PWDN等引脚定义qcom,eeprom-src、qcom,actuator-src关联的eeprom和马达节点我在第一次调一个sensor时最常犯的错误就是把cci-master配错。物理上sensor明明接在CCI1上设备树里却写的0结果i2c读ID永远超时。这个错误kernel不会主动报给你你得学会自己看log、比对原理图的连接关系。msm_sensor.c的整体流程相当清晰probe的时候解析设备树把GPIO和电源配置读进一个msm_sensor_power_setting结构体数组然后注册subdev。之后hal层通过v4l2 ioctl调用msm_sensor_config根据不同的command去执行相应的操作比如CFG_POWER_UP、CFG_POWER_DOWN、CFG_SET_STOP、CFG_SET_MODE。2.2 probe、sensor ID和i2c读写链条老平台sensor驱动的第一个关键环节是从i2c总线里读出sensor ID并判断是否匹配。这个过程一般被叫做“demand probe”或“sensor probe”它的原理很简单驱动拿到设备树里配置的sensor slave address通过CCI控制器向sensor寄存器发起读操作然后把这个值与驱动预设的ID比对。我举个例子假设一颗sensor的slave address是0x20ID寄存器在0x0000和0x0001两个字节期望值分别是0x00和0x58。那么在msm_sensor_get_id里会通过msm_sensor_i2c_read读这两个寄存器比对成功后probe流程继续失败则直接返回错误。高通在probe时还支持“多颗sensor共用同一个设备树节点”的机制就是通过不同的ID来区分如果ID不匹配自动跳过当前驱动尝试下一颗。这里有个非常实用的排查思路sensor ID读不上来绝大多数情况不是sensor驱动写错而是硬件或者时序问题。首先是GPIO和电源没起来sensor根本没上电读出来的寄存器值全是0xFF其次是i2c的地址或者总线通道错了再次是MCLK没有正常输出sensor内部的i2c接口需要clock才能工作。后面调试章节我会专门展开这里先记住一个判断方法——读出来的值如果全是0xFF基本是sensor没上电读出来全是0x00可能是sensor处于reset状态读到某个固定值但和ID表对不上多半是i2c地址或者寄存器地址长度设置错了。2.3 power sequenceshutdown时长的耐心账sensor驱动的上电时序是最磨人的部分。每一颗sensor的datasheet里都会给一张上电时序图上面详细标明了AVDD、DVDD、DOVDD、MCLK、RESET这些信号拉起的先后顺序以及每个步骤之间的最小延时要求。高通的msm_sensor_power_setting数组正好对应这张图每一项包含seq_type、seq_val、config_val和delay。拿我调过的一颗sensor举例它的上电顺序是先拉高VANA模拟电源等待10ms然后拉高VDIG数字电源等待10ms再拉高VIO接口电源等待5ms接着输出MCLK等待20ms最后RESET从低拉高等待20ms。把这个过程翻译成代码就是下面这个数组static struct msm_sensor_power_setting ov5675_power_setting[] { { .seq_type SENSOR_GPIO, .seq_val CAM_GPIO_VANA, .config_val 1, .delay 10, }, { .seq_type SENSOR_GPIO, .seq_val CAM_GPIO_VDIG, .config_val 1, .delay 10, }, { .seq_type SENSOR_GPIO, .seq_val CAM_GPIO_VIO, .config_val 1, .delay 5, }, { .seq_type SENSOR_CLK, .seq_val 0, .config_val 19200000, .delay 20, }, { .seq_type SENSOR_GPIO, .seq_val CAM_GPIO_RESET, .config_val 1, .delay 20, }, };如果你拿到的sensor datasheet里明确写了“RESET拉低后至少要等待X毫秒才能拉高”那这个delay一定不能省。曾经我把RESET的delay从20ms改成了2ms结果就是sensor ID偶尔能读到、偶尔读不到严重的时候30次probe只成功两三次。这种“偶发无效”的问题最坑人因为它不固定非常难排查。后来用逻辑分析仪抓时序才发现sensor内部的电源管理模块还没完成初始化就被我强行拉起了reset它干脆就没进入正常工作状态。到了stream on阶段驱动的动作也比较机械把sensor从software standby切换到streaming模式一般就是往某个寄存器写一个值然后等待若干ms让sensor输出第一帧。stream off则是反过来。这个动作在旧平台由内核驱动执行新平台则整个搬到了用户态sensormodule里。3. 新平台sensor驱动从写程序到写配置3.1 一颗新sensor如何被“翻译”成sensormodule到了CamX时代写sensor驱动的感觉完全变了。如果你打开SM8250或者SM7250平台的代码会发现chi-cdk/oem/qcom/sensor/目录下那颗sensor的文件夹里摆着两个核心文件一个是以sensor命名的.cpp模块文件一个是对应的.json配置文件。.cpp文件继承CSensorModule这个类它做的事情是定义这颗sensor的私有数据包括sensor的名字、slave address、上电时序表、start/stop寄存器序列、不同分辨率的mode表、以及曝光、增益转换的算法。.json文件看起来更像一份“传感器的说明书”里面描述了sensor的物理能力、mode分辨率、数据格式、MIPI lane数、timing参数等。我知道很多人第一次看到这个架构会发懵之前写的明明是C语言驱动现在怎么变成C对象 JSON了其实背后的逻辑很直接——高通希望把sensor驱动从“内核态代码”变成“可配置的数据”这样OEM在适配一颗新sensor时不需要重新编译内核只需要在用户态增加一个模块然后重新build vendor镜像。而且JSON格式天然适合描述sensor这种“表格型能力”的东西分辨率、timing、寄存器序列都可以很直观地展示出来。我自己的感受是CamX把sensor驱动的门槛从“嵌入式驱动工程师”下沉到了“懂camera参数配置的工程师”。你不会写内核代码也可以做sensor bringup只要能看懂datasheet、会填JSON就能让摄像头出图。当然深入调曝光、去噪这类问题时数据结构里的那些字段背后的像素时钟、行时间计算还是需要扎实的硬件功底。3.2 从拿到datasheet到点亮sensor的完整步骤在新平台上点亮一颗新sensor我的固定流程大致分为六步每一步都会踩出不同的坑。第一步把datasheet吃透。重点整理四类信息sensor的I2C地址、ID寄存器和期望值供电要求AVDD/DVDD/DOVDD的电压值MCLK频率绝大多数高通平台用19.2MHzMIPI lane数量和数据速率上限。第二步在内核设备树里把硬件连接关系配好。虽然在CamX里sensor的逻辑上移到了用户态但CCI通道、CSIPHY通道、电源映射这些硬件关系还是由内核来描述的。你需要确认sensor接在CCI0还是CCI1对应的CSIPHY是0还是1然后检查供电的regulator节点是否可用。第三步在chi-cdk的sensor目录下创建sensor模块的文件夹把.cpp文件和.json文件放进去。.cpp文件可以从同平台的参考sensor拷贝改造重点改sensorName、slaveAddress和上电时序数组.json文件需要导入这颗sensor的关键参数特别是resolution mode表。第四步把sensor模块注册进编译系统。这一步要检查目标平台下的sensor目录里的构建列表把新加的sensor文件夹路径加进去。如果漏了这一步你编译出来的vendor镜像里根本没有这颗sensor的模块camera服务启动时会直接跳过它。第五步编译烧录后先不要急着出图先读sensor ID验证i2c链路。CamX的log中会打印sensor module加载和probe的结果如果读到ID成功log里会有明确信息如果失败会打一条sensor probe failed。这一步通过才去进行后面的stream on。第六步出图并校准。第一次出图大概率不是花屏就是黑屏真正把颜色、帧率、曝光调到一个可用状态还要看sensor默认参数、ISP的配置、以及你是否配了正确的data rate。3.3 为什么CamX把复杂度“上移”了很多从老平台转过来的人会有一个公开的抱怨CamX把sensor驱动搞复杂了。以前改个寄存器直接在内核驱动数组里改一行现在要找到对应sensor的JSON/config文件还要搞明白C对象怎么编译进去。从一个开发者写代码的角度看这确实是变“重”了。但如果你从系统整体去看CamX的设计其实更合理。第一用户态出问题不会直接导致内核panicsensor驱动崩了最多camera服务重启系统不会挂这在实际调试中太重要了。第二用户态模块可以按需加载、动态替换一颗sensor驱动坏了不用重新刷内核只push一个vendor镜像就行。第三CamX把pipeline调度和sensor控制解耦上层可以用更统一的方式管理多摄像头并发、切换、HDR多帧合成这些复杂场景。我自己的经验是在CamX上做sensor驱动适配前期的学习曲线比老平台陡不少但是一旦你理解了sensor模块在pipeline里的位置以及它和CSLCamera Services Layer之间的调用关系后面调多摄、调HDR、调SATSpatial Alignment Transform反而比老平台顺手得多。4. 实战复盘一次完整的sensor bringup问题排查4.1 背景一颗新后摄的就是点不亮想借一次实际的bringup复盘把上面这些知识串起来。当时是在一款SM7250平台的项目上要新加一颗50M像素后摄平台本身没有这颗sensor的参考驱动我参考同序列的另外一颗sensor搭建了模块。第一轮亮机后启动camera结果当然是黑屏。这个“黑屏”并不意外但它的troubleshooting路径非常典型。我当时的排查思路是先确认camera服务有没有正常拉起、CamX有没有加载到sensor模块再确认sensor ID有没有读到最后确认stream on有没有报错。打开logcatCamX的log里明确打了sensor probe failed错误发生在读取sensor ID的时候。从log来看i2c通信已经发送了读命令但sensor没有返回正确的ID值返回来的是一串0xFF。4.2 第一步验证i2c链路和上电状态读出来全是0xFF根据我的经验sensor压根没有上电。所以我把目光转向了上电时序。CamX的sensor上电时序由.cpp文件里的power sequence数组控制打开log确认CamX确实执行了power up流程GPIO和regulator的下发动作也都做了。但是仔细看log我发现复位RESET动作发生在MCLK输出之前。这颗sensor的datasheet里明确要求MCLK稳定输出一段时间后才能把RESET拉高。我把数组调整了一下让MCLK先输出之后再拉RESET。改完后重新编译、烧录、再试sensor ID还是读不到。这个结果说明问题没有出在RESET顺序上或者说不止出在RESET顺序上。4.3 第二步上示波器眼见为实软件log能告诉你的东西始终有限。真正解决这个问题的是一台示波器。我把示波器探头接到sensor的MCLK引脚和RESET引脚上触发camera启动抓出来的波形让我很意外MCLK确实有19.2MHz的信号但RESET引脚的波形始终没有拉高一直停留在低电平。这说明CamX上层认为它已经拉高RESET了但实际上GPIO电平没有变化。这个“表里不一”的情况在老平台也常见十有八九是GPIO的复用配置不对——这颗sensor用的RESET引脚可能被其他模块配置成了别的功能甚至被某个外设占用了。我去查了这颗sensor所用的RESET GPIO在设备树里的引脚复用定义果然它被之前某个dtsi配置成了别的用途。把冲突的配置注释掉重新编译烧录再抓波形RESET引脚正常拉高了sensor ID也一次性读出来了。4.4 第三步出图后的帧率与曝光问题sensor ID读到后紧接着就是出图和调帧率。第一帧出来是花的这个问题也算预料之中——花屏往往是MIPI lane数或者data rate配置和sensor实际输出不匹配。我对照datasheet核对了sensor在50M分辨率下的MIPI输出配置发现json里配置的是4 lane但sensor实际跑的是2 lane输出。sensor输出的数据量和PHY层的接收配置对不上画面自然就是撕裂的。把lane数改成4之后画面终于稳定了。帧率又不对。目标帧率是30fps实际跑出来只有25fps。这个问题的根源通常要回到sensor的timing参数上。sensor输出帧率由像素时钟、行长度和帧长度共同决定。我去sensor的mode表里检查了line_length_pck和frame_length_lines两个关键值发现frame_length_lines明显比理想值大这意味着sensor每个frame之间插入了过多的blanking行。高通CamX里sensor mode表的一个经验法则是如果实际帧率比目标低优先检查VTSVertical Total Size帧总行数。VTS配大了帧率一定往下掉。我把frame_length_lines调整到满足30fps的数值附近帧率恢复正常。至此这颗新sensor才算真正“点亮”了。5. sensor驱动调试必备工具与经验技巧5.1 硬件侧工具示波器和逻辑分析仪是底线做sensor驱动调试示波器我觉得是必需品至少也要有一台逻辑分析仪。很多驱动问题是“软件逻辑看起来完全正确”但硬件没反应这种问题只能靠物理波形来定位。MCLK要用示波器量确认频率是不是19.2MHzRESET和PWDN的时序要通过波形对比datasheet里的时序图确认拉高拉低的先后顺序和延时是否合规I2C信号最好用逻辑分析仪抓包确认slave address、寄存器地址和读写方向都正确。刚入行的时候我总觉得一条一条读sensor寄存器也能完成调试为什么非要上示波器后来遇到一个“I2C读写偶尔成功偶尔失败”的问题读寄存器读了一下午也没发现规律一上示波器就看到了I2C的时钟线高电平被电源干扰压得不够高导致从设备识别不了SDA信号。这种问题是软件排查无法定位的。5.2 软件侧常用命令i2c读写和log抓取在调试sensor驱动时我们经常要在内核态和用户态之间交叉验证sensor的状态。老平台可以直接在shell里用i2cget和i2cset去读sensor寄存器。如果sensor挂在I2C总线3上地址是0x20读0x0000寄存器命令类似i2cget -f -y 3 0x20 0x0000但要注意高通平台的sensor通常挂在CCI总线上这个总线在Linux下可能不直接暴露为标准的i2c设备节点。这种情况下我一般借助CamX已经跑起来的系统通过底层的CSL ioctl读寄存器或者直接在代码里加debug log打印读取值。新平台CamX也可以在/vendor/etc/camera/camxoverridesettings.txt这个override文件里打开更详细的sensor调试log里面能看到sensor probe、mode switch、stream on/off每个阶段的详细状态。关于log再分享一个经验新平台上sensor驱动出问题不要只盯着logcat看还要同时抓内核log。因为内核侧的CCI驱动、GPIO驱动、电源管理模块如果出问题只会打dmesglogcat里完全看不到。我的习惯是两边log一起抓然后按时间戳对齐排查大多数问题的位置都能快速圈定。5.3 常见问题速查一张表帮你定位方向根据我的实际经验给一个高频问题定位方向速查表方便排查时对照现象优先检查方向读ID全是0xFFsensor没上电查MCLK/电源/RESET时序读ID全是0x00sensor一直处于reset状态检查PWDN/RESET引脚读ID是固定错误值i2c地址或寄存器地址长度配置错误i2c超时无应答CCI通道错误检查cci-master配置黑屏无信号MIPI lane数不对sensor没进入streaming画面撕裂/花屏MIPI data rate和lane数量不匹配帧率偏低检查VTS/frame_length_lines配置是否过大画面偏色sensor输出格式和ISP配置不一致这个表不是万能药但能让你在拿到一个sensor相关bug时第一时间知道往哪个方向去查而不会像无头苍蝇一样乱试。5.4 平台差异带来的隐藏坑最后聊一个我切换平台时踩过的坑给已经习惯老平台的读者提个醒。老平台sensor驱动的很多初始化动作是在内核态完成的比如sensor的寄存器序列可以直接写在driver里probe阶段就执行而CamX里这些序列的时机变得非常关键sensor上电之后不一定马上就执行初始化序列它有可能延迟到stream on之前的某个sensorNode线程里才执行。这个差异导致了一个很隐蔽的问题在老平台只要probe成功sensor寄存器就已经按预期配好了在新平台即使sensor probe成功了在第一次stream on之前sensor可能仍然处于默认甚至未初始化的状态。如果你在这个中间阶段去读sensor的某个寄存器验证配置可能会读到和预期完全不符的值这时候别怀疑sensor坏了先确认初始化序列到底执行了没有。还有一个和曝光相关的坑CamX里sensor的曝光、gain计算不是简单往寄存器里写值就行它走了一套CSensorModule::CalculateExposure之类的算法路径需要在sensor模块里实现GetExposureInfo、FillExposureSettings这些接口。如果曝光值一直不对不要只找寄存器配置先看看这几个接口里是否有除法取整的问题特别是长曝光时行数超过16位可能溢出。调试camera sensor驱动某种程度上像是在和一个看不见的硬件“谈判”你得知道它的脾气给它提供恰好的电源、恰好的时钟、恰好的复位时序它才愿意吐数据给你。而每一颗sensor的脾气都藏在它的datasheet里驱动代码只是把你对这份datasheet的理解翻译给平台听而已。把这句话想明白了无论以后平台怎么换代你都能快速上手。