1. 项目缘起与整体设计思路
1.1 为什么选这个平台做Camera移植
手里这块基于msm8953的开发板,属于高通骁龙625系列,八核A53架构,14nm制程,在当年是中端主力,现在拿来做嵌入式视觉项目依然够用。我接到的需求是把一颗MIPI接口的CMOS传感器点亮,跑通从设备树配置到用户空间出图的完整链路。这颗sensor不是原厂参考设计里默认的那颗,所以DTS要改、驱动要适配、时钟和电源域要重新对,最后还得用v4l2-ctl抓一帧确认数据通路没问题。
选这个平台的原因很实际:msm8953的Camera子系统(CAMSS)架构成熟,资料相对完整,社区里踩过坑的人多,遇到问题容易找到线索。而且它的ISP管线支持RAW到YUV的处理,对于不想在外挂ISP上多花钱的方案来说,直接走平台内置ISP是最省事的路径。
整个移植工作的核心目标就一个:让/dev/video0节点出现,并且通过v4l2-ctl能抓到一帧正常的图像。听起来简单,但中间涉及DTS的CSI、CCI、 regulator、clock、GPIO多个节点的联动,任何一个对不上,sensor就probe失败或者出图花屏。
1.2 整体方案拆解与选型考量
移植路线我分成了四层:硬件层确认供电和时钟、DTS层描述设备连接关系、驱动层适配sensor寄存器序列、用户层验证数据通路。这四层是递进关系,前面不对后面白搭。
硬件层要先确认三件事:sensor的供电轨(通常有AVDD、DVDD、DOVDD三路)、MCLK时钟频率(常见24MHz或19.2MHz)、以及I2C地址(7位地址,常见0x10、0x36、0x6c等)。这些信息从sensor的datasheet里拿,拿不到就找模组厂要初始化序列。
DTS层是移植的重头戏。msm8953的Camera DTS结构比较绕,CSI(MIPI接收控制器)、CCI(I2C控制器)、CSIPHY(物理层)、CSID(解码层)是分开描述的,sensor节点要挂到CCI下面,同时通过"remote-endpoint"和CSIPHY建立连接。这个连接关系如果画错了,media controller里就看不到完整的pipeline。
驱动层我选择基于平台已有的sensor驱动框架改,而不是从零写。msm8953的kernel里通常有ov5648、ov8865这类参考驱动,把寄存器序列、分辨率配置、时钟配置替换成目标sensor的就行。这样能省掉大量V4L2子设备注册的样板代码。
用户层验证用v4l2-ctl,这是最直接的工具。先media-ctl -p看pipeline拓扑,确认sensor、csiphy、csid、isp这几个entity都连上了,再用v4l2-ctl --stream-mmap抓帧存成raw文件,最后用工具转成能看到图片。
注意:msm8953的Camera子系统在不同kernel版本上差异较大,3.18和4.9的DTS绑定方式就不一样。动手前先确认你手上的BSP是哪个版本,别拿错参考文档。
2. DTS配置的核心细节与实操要点
2.1 CCI与Sensor节点的挂载关系
CCI在msm8953里就是I2C控制器,但它在DTS里的描述方式和普通I2C不太一样。你要在cci节点下面建一个i2c的subnode,然后把sensor挂进去。关键属性是reg(I2C从地址)和compatible(匹配驱动)。
&cci { status = "ok"; i2c@1 { reg = <1>; #address-cells = <1>; #size-cells = <0>; camera_sensor: camera-sensor@36 { compatible = "vendor,sensor-name"; reg = <0x36>; status = "ok"; /* 供电配置 */ vdd-supply = <&pm8953_l10>; vddio-supply = <&pm8953_l6>; vdda-supply = <&pm8953_l3>; /* 时钟配置 */ clocks = <&clock_gcc clk_mclk0_clk_src>; clock-names = "cam_src_clk"; clock-frequency = <24000000>; /* GPIO配置 */ reset-gpios = <&tlmm 26 0>; pwdn-gpios = <&tlmm 36 0>; /* CSI连接 */ port { sensor_out: endpoint { remote-endpoint = <&csiphy0_ep>; >&csiphy0 { status = "ok"; csi-vdd-supply = <&pm8953_l2>; port { csiphy0_ep: endpoint { remote-endpoint = <&sensor_out>; >&cci { assigned-clocks = <&clock_gcc clk_mclk0_clk_src>; assigned-clock-rates = <24000000>; };电源域这边,msm8953的Camera供电通常走PM8953的LDO。L10给AVDD(2.8V),L6给DOVDD(1.8V),L3给DVDD(1.2V)。具体哪路LDO给哪个电压,得看原理图。我遇到过LDO电压设错导致sensor I2C能通但不出图的情况,查了半天才发现是DVDD只有1.0V,sensor内部PLL没锁上。
实操心得:DTS改完之后,先别急着编译整个kernel,用
dtc工具单独编译dtb,然后反编译回来检查节点关系是否正确。命令是dtc -I dtb -O dts -o check.dts arch/arm64/boot/dts/qcom/msm8953.dtb,这样能快速发现语法错误。
3. 驱动适配与寄存器序列调试
3.1 基于参考驱动的移植策略
msm8953的kernel里通常自带几个sensor驱动,比如ov5648、ov8865、imx214。我选了一个寄存器结构最接近的作为模板,把里面的sensor ID、分辨率表、寄存器序列全部替换掉。
驱动文件一般放在drivers/media/platform/msm/camera_v2/sensor/下面。核心结构体是msm_sensor_ctrl_t,里面包含了sensor_lib_params、sensor_output、sensor_reg_tbl这些关键字段。
移植时重点改这几个地方:
sensor_lib_params里的full_size、prev_size、video_size等分辨率定义sensor_output里的width、height、line_length_pclk、frame_length_linessensor_reg_tbl里的初始化寄存器序列msm_sensor_power_setting里的上电时序
上电时序特别关键。一般顺序是:先给AVDD,延时1ms,再给DOVDD,延时1ms,再给DVDD,然后拉高reset,延时1ms,拉低reset,最后给MCLK。这个顺序如果错了,sensor可能直接锁死,I2C都读不到ID。
3.2 寄存器序列的调试方法
寄存器序列通常从sensor厂拿,但拿到的往往是针对特定模组的,换一个镜头或者换一个供电方案就可能需要微调。调试的时候我习惯把序列分成几段:PLL配置段、模拟配置段、时序配置段、输出格式段。
PLL配置决定了sensor内部的工作时钟,如果PLL没锁上,I2C能读到ID但出不了图。验证方法是读PLL相关的状态寄存器,看lock位是否置起。
模拟配置段包括增益、曝光、黑电平这些,一般用默认值就行,出图后再调。
时序配置段是最容易出问题的,包括HTS、VTS、HBlank、VBlank这些。如果HTS设小了,一行数据没传完就被截断,表现为图像右边有绿边或者错位。VTS设小了,帧同步会乱,表现为图像滚动或者撕裂。
输出格式段决定sensor输出的是RAW8、RAW10还是RAW12,以及是MIPI还是DVP。msm8953的CSID支持RAW8/10/12,但ISP处理RAW10最顺,所以一般配成RAW10。
/* 典型的RAW10输出配置 */ static struct msm_sensor_output_info_t sensor_output_info = { .x_output = 1920, .y_output = 1080, .line_length_pclk = 2200, .frame_length_lines = 1125, .vt_pixel_clk = 148500000, .op_pixel_clk = 148500000, .binning_factor = 1, .max_fps = 30, };line_length_pclk和frame_length_lines这两个值要算对。以1920x1080@30fps为例,MIPI时钟如果是594Mbps每lane,四lane总带宽是2376Mbps,RAW10每像素10bit,所以理论最大像素率是237.6M像素每秒。1920x1080是207万像素,30fps需要62.2M像素每秒,带宽是够的。HTS和VTS的乘积要大于等于总像素数除以帧率。
3.3 驱动probe失败的排查思路
sensor驱动probe失败是最常见的问题,表现是/dev/video0不出现,dmesg里有probe failed或者i2c read failed。
排查顺序我一般这样走:
- 先确认I2C通不通。用
i2cdetect -y 1扫一下,看sensor地址能不能被识别。如果扫不到,查供电和上拉电阻。 - I2C通了但probe失败,查sensor ID。驱动里一般会读sensor的ID寄存器,和预期值比对。读到的ID不对,可能是I2C地址错了,或者寄存器地址位宽不对(8位还是16位)。
- ID对了但probe还是失败,查时钟。用示波器量MCLK,看频率对不对,有没有波形。没有波形就查时钟树配置。
- 时钟对了但probe失败,查reset和pwdn GPIO。这两个GPIO的极性很重要,有的sensor是高电平复位,有的是低电平。搞反了sensor一直处于复位状态。
注意:msm8953的CCI控制器在probe阶段可能会因为时钟没准备好而超时,dmesg里会打印
cci timeout。这时候不要急着改驱动,先确认CCI的时钟源有没有使能。
4. v4l2-ctl出图验证与问题排查
4.1 media controller拓扑检查
出图之前先用media-ctl看pipeline拓扑,确认所有entity都连上了。
media-ctl -p -d /dev/media0正常输出里应该能看到sensor、csiphy、csid、ispif、isp这几个entity,并且它们之间的link是ENABLED状态。如果某个link是IMMUTABLE或者DISABLED,说明DTS里的endpoint没配对。
我遇到过csiphy和csid之间的link没使能的情况,原因是csid的endpoint名字写错了。DTS里csid的endpoint必须叫csid0_ep,写成csid_ep就匹配不上。
拓扑确认之后,用media-ctl设置格式:
media-ctl -d /dev/media0 --set-v4l2 '"camera-sensor":0[fmt:SRGGB10_1X10/1920x1080]' media-ctl -d /dev/media0 --set-v4l2 '"msm_csiphy0":0[fmt:SRGGB10_1X10/1920x1080]' media-ctl -d /dev/media0 --set-v4l2 '"msm_csid0":0[fmt:SRGGB10_1X10/1920x1080]'格式要一层层往下设,从sensor开始,到csiphy,到csid,最后到ispif。如果中间某一层设不进去,说明那一层的驱动没准备好。
4.2 v4l2-ctl抓帧命令与参数
抓帧用v4l2-ctl的stream-mmap模式:
v4l2-ctl -d /dev/video0 --set-fmt-video=width=1920,height=1080,pixelformat=RG10 --stream-mmap --stream-count=1 --stream-to=frame.rawpixelformat要和你sensor输出的格式对应。RAW10对应RG10(Bayer RGGB),RAW8对应RGGB。写错了会报VIDIOC_S_FMT failed。
抓到的raw文件是纯Bayer数据,不能直接看,要用工具转。我一般用raw2rgb或者自己写个python脚本:
import numpy as np width, height = 1920, 1080 with open('frame.raw', 'rb') as f: data = f.read() # RAW10打包格式,每5字节存4个像素 pixels = np.zeros((height, width), dtype=np.uint16) idx = 0 for i in range(0, len(data), 5): b0, b1, b2, b3, b4 = data[i:i+5] pixels.flat[idx] = ((b0 << 2) | (b4 & 0x03)) pixels.flat[idx+1] = ((b1 << 2) | ((b4 >> 2) & 0x03)) pixels.flat[idx+2] = ((b2 << 2) | ((b4 >> 4) & 0x03)) pixels.flat[idx+3] = ((b3 << 2) | ((b4 >> 6) & 0x03)) idx += 4 # 归一化到8bit pixels = (pixels >> 2).astype(np.uint8) # 简单的Bayer转RGB rgb = np.stack([pixels, pixels, pixels], axis=-1) from PIL import Image Image.fromarray(rgb).save('frame.png')这个脚本把RAW10解包成16bit,再右移2位变8bit,最后存成灰度图。虽然颜色不对(因为没做Bayer插值),但能看出图像结构对不对。
4.3 常见出图问题速查表
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 全黑 | sensor没出数据 | 查MCLK、reset、I2C是否正常 |
| 全绿 | 数据线序错 | 检查data-lanes配置,尝试调整lane顺序 |
| 横条纹 | MIPI信号质量差 | 查CSIPHY供电,降低MIPI速率试试 |
| 图像错位 | HTS/VTS配置错 | 重新计算line_length_pclk和frame_length_lines |
| 滚动 | 帧同步丢失 | 查VTS是否小于实际帧长 |
| 花屏 | 数据格式不匹配 | 确认sensor输出格式和CSID配置一致 |
| 只有一半图像 | lane数不对 | 检查data-lanes属性,确认实际用了几个lane |
实操心得:出图有问题时,先把分辨率降到最低(比如640x480),把MIPI速率也降到最低,这样能排除带宽和信号完整性问题。低分辨率能出图,再逐步往上加,定位问题更快。
5. 移植过程中的经验沉淀与扩展思考
5.1 踩过的坑与避坑指南
第一个坑是DTS的endpoint名字。msm8953的DTS里,CSIPHY的endpoint名字在不同BSP里不一样,有的叫csiphy0_ep,有的叫csiphy_ep。名字对不上,media controller里就看不到link。我的做法是先把BSP里自带的参考DTS翻出来,照着它的命名规则写,别自己发明。
第二个坑是时钟频率。sensor datasheet上写的MCLK是24MHz,但平台默认给的是19.2MHz。我一开始没改时钟树,直接改sensor的PLL配置,结果PLL锁不上。后来把assigned-clock-rates改成24000000,问题解决。所以遇到PLL锁不上,先确认MCLK频率对不对。
第三个坑是电源上电顺序。我按照datasheet的顺序写,但实际模组上有一颗LDO的使能脚接在reset GPIO上,导致reset拉高的时候LDO才输出。这种情况下要把reset GPIO的配置改成先拉高再配置LDO,或者干脆把LDO的使能脚改到别的GPIO上。
第四个坑是v4l2-ctl的pixelformat。RAW10在V4L2里的fourcc是RG10,但有些kernel版本里是BA10或者RGGB。写错了会报错,但错误信息很模糊,只说S_FMT failed,不告诉你哪个格式不对。我的做法是把所有可能的fourcc都试一遍,用v4l2-ctl --list-formats看驱动支持哪些。
5.2 性能调优与稳定性验证
出图只是第一步,后面还要调帧率、调曝光、调白平衡。帧率上不去通常是VTS设太大了,把VTS减小能提高帧率,但太小了会丢帧。我一般把VTS设成实际帧长的1.1倍,留一点余量。
曝光和白平衡这些3A算法,msm8953的ISP里有硬件支持,但需要用户空间配合。如果只是验证数据通路,可以先用固定曝光和固定增益,出图正常了再调3A。
稳定性验证我一般跑连续抓帧,抓1000帧看有没有丢帧或者花屏。命令是:
v4l2-ctl -d /dev/video0 --stream-mmap --stream-count=1000 --stream-to=/dev/null如果中间报错,dmesg里会有CSID的error log,根据log定位是MIPI错误还是ISP错误。
5.3 后续可扩展的方向
这套移植流程跑通之后,可以往几个方向扩展。一是加第二颗sensor,做双摄同步,这需要配置CSIPHY的lane分配和CSID的虚拟通道。二是接ISP的3A算法,把图像质量调上去。三是把v4l2的接口封装成GStreamer插件,方便上层应用调用。
双摄同步的难点在于两颗sensor要共享MCLK,并且帧同步信号要连在一起。msm8953支持MCLK0和MCLK1两路输出,但帧同步需要额外的GPIO。这个我在另一个项目里做过,后面可以单独写一篇。
ISP 3A的调优是个大工程,涉及AE、AWB、AF三个模块的参数整定。msm8953的ISP支持硬件统计,用户空间只需要读统计值然后算控制量。这部分高通有参考代码,但文档很少,基本靠读代码和试参数。
GStreamer封装相对简单,写一个v4l2src的element就行,但要注意buffer的零拷贝,不然CPU占用会很高。msm8953的ISP支持直接输出到ION内存,GStreamer这边用dmabuf导入就能实现零拷贝。
最后再分享一个小技巧:调试Camera的时候,dmesg的log级别调到7,能看到CSID和ISP的详细寄存器读写记录。命令是echo 7 > /proc/sys/kernel/printk,这样排查问题会快很多。不过log量很大,建议用串口抓,别用adb,不然会丢log。