早些年做高通平台Camera调试,最耗时的往往不是解问题本身,而是在一堆堆不明所以的日志里找方向。Log打少了,现场复现不充分,问题就越查越糊涂;Log开多了,又密到让人看不进去,反而把关键点淹没掉。更麻烦的是,用户态和内核态的日志如果不在一起,一个问题要来回横跳两个终端,时间戳还对不上,等整明白流程,半天已经没了。
这篇内容主要围绕高通Camx架构下的调试基本功展开:UMD(用户态驱动层)日志怎么开、KMD(内核态驱动层)日志怎么开、Camx的图像Dump怎么做,最后分享一个我用过的离线日志合成脚本思路,能把用户态和内核态的log按时间顺序合并到一起。做Camera驱动、系统集成或者图像效果调试的朋友,应该都能用得上。
1. 先搞清楚要打哪层日志:Camx架构的UMD与KMD
1.1 Camx在高通Camera栈里的位置
高通平台从SM8250(骁龙865)这一代开始,就把老的mm-camera架构逐步往Camx(Camera eXtension)上迁移。到现在,Camx已经是高通主流平台的标准Camera框架,上接Android Camera HAL层,下连内核的Camera驱动。日常调试时,我们通常把整个链路拆成三层来看:App与Camera Framework、HAL与Camx Core(UMD层)、内核Camera驱动(KMD层)。
Camx Core用户态这部分,实际会加载libcamxhwl、libcamxswl这类库,还会配合Chi-CDK(Camera Hardware Interface)去做node的定制编排。KMD层则是Camera Subsystem的内核驱动部分,包括cam_sync、cam_req_mgr、cam_isp、cam_sensor、cam_cpas这些模块,负责更底层的中断、寄存器操作和sensor的I2C配置。
单看某一层日志,往往只能看到问题的一半。比如预览黑屏,问题可能出在sensor上下电、也可能出在ISP的streamon失败、还可能出在HAL侧的pipeline没有真正start。所以我一般会建议:调试开始前,先决定要开哪几层日志,而不是一口气全开。
1.2 UMD与KMD分工,为什么两侧日志都要开
Camx里有个非常核心的设计理念,叫“Pipeline-based”,底层通过Request机制驱动sensor、ISP、IPE等节点。用户态Camx Core负责构建pipeline、分发request、处理HAL回调;内核态则负责硬件调度、中断处理、buffer管理。
以一条出图链路为例:HAL发起processCaptureRequest,Camx Core会给pipeline的各node填好配置,通过IOCTL下发到KMD;KMD的cam_req_mgr收到后登记request,再逐级调度给ISP硬件。如果用户态日志显示request已经发出去了,但内核日志里没看到对应的request arrival,那基本可以确认问题出在KMD入口之前。反过来,如果内核日志已经出图完成,但HAL侧迟迟收不到回调,那大概率是UMD的event分发或者buffer状态机卡住了。
所以调试时我习惯把UMD Log和KMD Log一起开,哪怕先不分析内容,也要保证时间戳能对齐。这里需要提一下,高通在车规平台上会把Camx和安全相关的补丁合在一起,内核版本也会跟着CAF Kernel走,不同平台Log开关的位置和格式会有差异,但核心思路不会变。
1.3 日志链路整体策略
开日志之前先想清楚两个问题:问题大概在哪个模块?可复现性怎么样?
如果问题可稳定复现,优先开目标模块的详细日志,周边模块保持默认级别。如果问题像幽灵一样偶发,那建议直接开全链路日志,并且在复现后立刻抓取。全开日志会对帧率有一点影响,特别是ISP统计相关的日志,打多了会延长出图耗时,复现出来的现象可能和用户崩溃时的现象不完全一样,这个要有心理准备。
2. UMD Log开启实操:从环境变量到CamxOverrides
2.1 UMD日志的打印路径
Camx UMD层打印日志,底层走的是系统Log机制。Android平台上,Camx相关日志会打到logcat里,TAG一般是CamX、ChiNode、CHICONTEXT这些。用adb logcat直接抓也能看到,但因为Camx日志量实在太大了,系统通常会把大部分日志打到独立的文件节点,再通过属性开关控制。
常用的获取方式:
adb shell "logcat -d -v threadtime | grep -E 'CamX|ChiNode|Camx'" > camx_umd.log如果平台上有独立的Camx log文件,路径一般在/vendor/logs或者/data/vendor/camera,这类文件是Camx侧的转储日志,比logcat里的内容更完整,字段也更规整。
2.2 camxoverridesettings.txt 关键配置项
Camx提供了一套运行时配置机制,集中在camxoverridesettings.txt文件里,平时就放在vendor/etc/camera目录下。这个文件本质上是键值对,可以配置很多调试开关和内部参数。
需要先确认相机进程有没有读这个文件,有些平台会用generateoverride脚本把配置合到/vendor/etc/camera/camxoverridesettings.txt,如果改动没生效,大概率就是文件路径不对或者权限不对。
下面是我常用的几个Log相关的配置:
| 配置项 | 作用 | 推荐值 |
|---|---|---|
| camxLoggingEnable | 总开关,让Camx内部输出日志 | TRUE |
| camxLoggingMode | 日志模式,1表示输出到logcat,2表示输出到文件,3表示都输出 | 3 |
| camxLoggingLevel | 全局日志级别:0为error,1为warning,2为info,3为debug | 2 |
| camxLoggingMask | 按模块bitmask控制,可按需组合 | 0xFFFFFFFF |
| CamxLogDumpIsp | ISP相关dump节点开关 | TRUE |
实际配置时不要上来就全开,这会非常吵,而且影响性能。建议按模块拆。调试sensor对焦时,就开sensor和AF相关的配置;调试3A统计时,就开stats和AFDump相关的配置。
2.3 配置完成后如何验证
改完配置文件后,重启camearaProvider进程。
adb shell pkill -f camera.provider adb shell pkill -f cameraserver然后重新打开相机App,观察logcat里CamX的日志是否明显增多。如果增加量不明显,先用一个简单的Exposure补偿命令确认配置是否被加载:
adb shell "getprop | grep -i camx"如果这个属性没有输出对应调试属性,那可能是Camx native层没读取overrides文件,需要把配置放到init加载完的路径下,确保属性权限正确。我在某些平台上踩过坑,改完配置总不生效,折腾半天发现是文件放到了/vendor/etc/camera/下,但平台实际读取的是/odm/etc/camera/。所以拿到一个新平台,第一件事就是确认camxoverridesettings.txt默认路径。
3. KMD Log开启实操:内核侧到底怎么打开
3.1 KMD日志节点与动态调试
KMD层的高通Camera驱动里,各个模块都有自己的打印,默认情况下有很多是关闭的,需要打开动态调试开关。
内核日志查看基础命令:
adb shell dmesg > kmd.log adb shell "cat /proc/kmsg" > kmd.log模块化的打印,推荐用内核的dynamic_debug机制,可以在运行时打开某个文件的pr_debug,不需要重新编译内核。以cam_isp模块为例:
adb shell "echo 'file cam_isp.c +p' > /sys/kernel/debug/dynamic_debug/control"如果debugfs没有挂载,需要先挂载:
adb shell mount -t debugfs none /sys/kernel/debug打开之后,cam_isp.c里所有pr_debug的日志就会输出到dmesg里。内核对Camera模块驱动的文件名通常是cam_xxx.c,对应模块:cam_req_mgr.c、cam_isp.c、cam_sensor.c、cam_sync.c、cam_cpas.c。
3.2 使能trace event打点
比dynamic_debug更强大的,是高通Camera驱动里的trace event。这套机制在内核的tracefs里挂了一组Camera相关的事件节点,采集成perfetto或者trace文件后可以做很精细的时间线分析。
开启trace event:
adb shell "echo 0 > /sys/kernel/debug/tracing/tracing_on" adb shell "echo > /sys/kernel/debug/tracing/trace" adb shell "echo 1 > /sys/kernel/debug/tracing/events/camera/enable" adb shell "echo 1 > /sys/kernel/debug/tracing/tracing_on"复现问题后停止抓取:
adb shell "echo 0 > /sys/kernel/debug/tracing/tracing_on" adb shell "cat /sys/kernel/debug/tracing/trace" > trace_camera.txt这套trace几乎能钩到每次request从UMD下来的完整流转路径,包括哪个时刻进了cam_req_mgr、哪个时刻ISP开始处理、哪个时刻产生sof/ef。排查帧率波动、出图时序问题时作用非常突出。
3.3 高级技巧:panic与超时场景的last_kmsg
系统卡死或者camera进程崩溃时,往往来不及手动抓dmesg。这种情况要看last_kmsg或者pstore。部分平台会把上次启动时的内核日志保存在/sys/fs/pstore/console-ramoops,重启后还能读出来:
adb shell cat /sys/fs/pstore/console-ramoops > last_kmsg_pstore.txt还有一种常见做法是打开高通平台的t32抓取内核log,不过这项操作对大多数调试场景来说开销过高,实际生产环境不建议直接上。需要先确认平台是否支持pstore,如果支持,能省下很多反复复现的时间。
4. Dump图像:让平台把数据吐出来
4.1 图像dump的应用场景与数据链路
Log再全,也只是间接反映状态。遇到图像效果类问题、ISP的统计值异常问题、sensor输出异常问题时,直接用dump图说话会更有说服力。
Camx的dumplog分成几个维度:
- Sensor raw dump:直接dump sensor输出的raw图,检查sensor出图是否正常
- ISP统计dump:dump AF、AEC、AWB等统计信息,用来分析3A收敛过程
- YUV/RGB中间图dump:在ISP处理链路的不同node上dump出图,定位问题发生在哪个节点
raw dump的数据量很大,一片raw图动辄几十MB,调试前要确认sensor分辨率,同时注意dump时间不要过长,免得存储空间被写满。
4.2 ChiDump与各个节点配置
Camx的Dump开关通常也在camxoverridesettings.txt里配置。比如,开启raw dump可以配置:
| 配置项 | 作用 | 推荐值 |
|---|---|---|
| ChiDump | 总开关,控制用户态dump节点数据 | TRUE |
| ChiDumpMode | dump模式,按7位bit控制哪些node | 对应节点位的值 |
| ChiDumpNodeMask | 需要dump的node的mask | 按node填入 |
| ChiDumpPath | dump文件的输出目录 | /data/vendor/camera |
| AfDump | AF统计dump开关 | TRUE |
| AecDump | AEC统计dump开关 | TRUE |
这里的ChiDumpMode和ChiDumpNodeMask需要对照Chi节点ID去配置。最直观的办法是先把Mode设成全F,把NodeMask也设成全F,dump一轮看看有哪些目录和文件产生,再根据文件名反推节点ID,缩小范围。
需要提醒的是,Camx的dump路径默认是/data/vendor/camera,如果dumpserver进程没有权限写这个目录,dump会静默失败。遇到dump不出来,第一步检查这个目录是否存在、以及目录权限。
4.3 dump出来怎么看
dump出来的文件一般有两种格式:纯二进制raw文件和带头部的dump文件。raw文件可以用Python的numpy加载,也可以直接用高通的QPST工具离线解析。我习惯的做法是先用Python脚本解析raw文件,看文件名里的信息:
一个常见的dump文件名格式大概是:
IMG_20250101_120000_[PIPELINE_ID]_[NODE_ID]_[FRAME_ID].raw文件名携带的信息包括:时间、pipeline id、node id、frame id,这正好能和log里的时间戳对应上。解析raw文件时如果尺寸不对,可以先查看文件大小,通过像素格式和分辨率反推是否正确。例如一个1920x1080的NV12图,每帧大小应该约等于192010803/2。
对于3A统计dump,出来的文件一般是txt或bin,里面记录的是图像各个区域的亮度均值、对焦评价值等。分析这类文件时,可以按frame id逐条绘制曲线,这样能很直观看到AEC收敛过程有没有来回震荡。
5. 离线Log合成脚本:让用户态与内核态同屏
5.1 为什么要离线合成
Log抓是抓到了,但分析仍然费劲。UMD的logcat是threadtime格式,每一行都带进程号、线程号、时间戳;内核的dmesg则是另一个时间体系,两个文件的时间戳参照不一样,直接对比基本没法用。我遇到过不止一次,用户态log显示某个request已经返回了,内核侧同一时间的log却显示硬件还没有启动,查了一圈发现两个时间轴差了十几秒。
把两侧日志合到一起,本质上做一件事:把两边的日志时间统一到一个坐标系里。时间对齐完成后,所有日志都按顺序排列,可以直接从日志流上还原一帧从HAL下发到ISP出图的完整链路。
5.2 脚本设计思路与代码
我写过一个Python脚本,思路不复杂:
- 读取UMD日志文件,解析每行开头的日期时间(logcat默认“MM-DD HH:mm:ss.mmm”,threadtime格式里还带进程号)。
- 读取KMD日志文件,解析dmesg时间(一般是“秒.毫秒”或者带时间戳),需要结合开机时间换算。
- 人工选择一个同步锚点,比如系统开机时刻,或UMD和KMD共同打印的一条进程启动日志。
- 统一成绝对时间戳后排序输出。
锚点的选择最关键。如果平台能在启动时同步记录开机时刻,那直接用systime换算,dmesg里的时间加开机时刻offset即可。如果拿不到准确开机时间,就找一条两侧都存在的日志,例如“camx_initialize”或“CamxCreate”日志,把它们视作同一时刻,再反推偏移量。
下面是脚本的核心示例,供参考:
#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Offline Log Merger for Camx UMD/KMD logs 用法: python3 log_merger.py --umd umd.log --kmd kmd.log --boot-time "2025-01-01 12:00:00.000" """ import argparse import re from datetime import datetime, timedelta UMD_TIME_RE = re.compile(r"^(\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3})") KMD_TIME_RE = re.compile(r"^\[\s*(\d+\.\d+)\]") def parse_umd_time(line, year): m = UMD_TIME_RE.match(line) if not m: return None, line dt = datetime.strptime(f"{year}-{m.group(1)}", "%Y-%m-%d %H:%M:%S.%f") return dt, line[m.end():].strip() def parse_kmd_time(line, boot_time): m = KMD_TIME_RE.match(line) if not m: return None, line seconds = float(m.group(1)) dt = boot_time + timedelta(seconds=seconds) return dt, line[m.end():].strip() def main(): ap = argparse.ArgumentParser() ap.add_argument("--umd", required=True) ap.add_argument("--kmd", required=True) ap.add_argument("--boot-time", required=True) ap.add_argument("--year", default=2025) args = ap.parse_args() boot_time = datetime.strptime(args.boot_time, "%Y-%m-%d %H:%M:%S.%f") entries = [] with open(args.umd, "r", errors="ignore") as f: for line in f: dt, content = parse_umd_time(line, args.year) if dt: entries.append((dt, "UMD", content)) with open(args.kmd, "r", errors="ignore") as f: for line in f: dt, content = parse_kmd_time(line, boot_time) if dt: entries.append((dt, "KMD", content)) entries.sort(key=lambda x: x[0]) fmt = "%Y-%m-%d %H:%M:%S.%f" with open("merged_log.txt", "w") as out: for dt, tag, content in entries: out.write(f"{dt.strftime(fmt)[:-3]} [{tag}] {content}\n") print(f"合并完成,共 {len(entries)} 条日志,输出到 merged_log.txt") if __name__ == "__main__": main()脚本本身不复杂,但胜在实用。实际使用时,UMD日志的年份如果跨年,要去日志里确认,或者从文件修改时间推断;KMD日志如果平台没有提供开机时刻,可以在脚本里加一个“手动对齐时间戳”参数,用两侧都有的一条日志来锚定。
5.3 实际使用效果与局限
用这个脚本处理完日志之后,整个日志流里UMD和KMD是交错的。比如一条3840x2160的抓拍请求,日志顺序会是这样:
10:00:01.123 [UMD] Camx::RequestProcessor: New request 42 received 10:00:01.124 [UMD] Camx::HWL: Submitting request 42 to KMD, pipeline=0 10:00:01.125 [KMD] cam_req_mgr: apply_request req_id=42 10:00:01.130 [KMD] cam_isp: hw acquire req_id=42, slot=3 10:00:01.142 [KMD] cam_isp: notify_sof req_id=42 10:00:01.150 [UMD] Camx::StatsProcessor: SOF received req_id=42这样一眼就能看出request在哪个环节停留了多长时间,UMD有没有及时收到KMD的sof事件。
局限也很明显:如果两侧时间戳对不齐,合并后顺序就会错乱,所以第一步还是要核对锚点。另外,脚本只是文本处理,不会区分进程线程,如果同时打开多个camera场景,建议先在UMD侧标记pipeline id,再进脚本合并。
6. 常见问题与排查技巧实录
6.1 常见问题速查表
| 现象 | 可能原因 | 处理建议 |
|---|---|---|
| UMD Log只有少量打印 | overrides文件路径不对或属性未加载 | 确认平台读取路径,重启camera provider |
| KMD动态调试无输出 | debug分区未挂载或权限不足 | 先mount debugfs,检查节点访问权限 |
| 图像dump没有文件生成 | dump目录不存在或dumpserver无写权限 | 手动创建目录并chmod 777 |
| dump文件生成了但无法解析 | dump格式不是默认格式 | 对照文件名和文件大小反推像素格式 |
| 日志时间戳对不上 | 锚点选错 | 用启动时刻或特定日志对齐 |
| 打开大量日志后卡顿 | 日志量过大拖慢帧率 | 分级开启,只保留目标模块detail日志 |
| 系统重启后log丢失 | 日志没有落盘或落盘分区清理 | 提前设置pstore,或设置日志循环缓冲区 |
6.2 我的几点避坑心得
第一个心得很实际:改完camxoverridesettings后,一定要确认改的文件被正确加载。不要把时间浪费在猜配置上,用一个特殊字符串比如“DUMP_TEST_ON”写进去,再搜log里有没有出现,有就是加载了。
第二个心得,抓KMD日志时尽量同时抓一份time stamp的锚点。可以在开camera前执行一次“date”,把这个输出也存下来,后面换算dmesg时间就方便很多。如果不做,等日志抓完想对时间轴时就晚了。
第三个心得,图像dump尽量从raw开始。有时候效果问题,绕来绕去看中间yuv会觉得无从下手;但如果直接看raw没问题,起码能把sensor摘出去;如果raw本身有问题,那就直接查sensor端出图和I2C配置,问题范围一下子就缩小了。
第四个心得,高通平台不同子系统对日志处理的细节虽然有差异,但整体思路是通用的:找开关、开日志、抓现场、对时间轴。把这一套标准化之后,无论是给产线复现问题,还是远程让现场工程师抓log,沟通成本都会小很多。
最后再分享一个细节,楼道里经常看到同事调试时开着一堆终端窗口刷日志,其实真正有效的调试时间往往只集中在复现的那两分钟里。把Log开关做成一套标准操作文档,抓log流程做成脚本,每次复现前只需要一条命令启动抓取,复现后一条命令停止抓取并把日志打包出来。这套流程一旦跑顺,调试效率能提升一大截,这也是我不厌其烦写这篇文章的原因。