接手显示驱动调试之后,我最大的感触不是“代码难写”,而是“现象太难复现”。你改了某个DTS里的时钟,屏幕直接黑掉;你把pixel clock调低了5%,满屏雪花;你觉得scanout buffer地址没问题,实际显示出来整屏都是上一个画面的残影。显示驱动不像网络协议栈那样有清晰的报文可抓,很多问题的现场就是一块不亮的屏和一段串口log。所以做显示驱动调试,工具链比写代码更像是第一生产力。
这篇是系列的第5篇,围绕“显示驱动”和“调试工具”这两个关键词,把我实际用下来、且能快速定位问题的工具做一次系统梳理。不是一上来就罗列工具清单,而是按信号链和故障现象来讲:每一层负责什么、该用哪类工具取证、工具能证明什么、证明不了什么,以及我踩过的那些看起来很蠢的坑。适合刚接触DRM/KMS、嵌入式显示驱动的同学,也适合经常被黑屏花屏问题折磨却不知道从哪下手的工程师。
1. 接手显示驱动调试时,先按信号链分清楚“锅”在哪一层
1.1 显示驱动不是“一个模块”,而是一条从显存到屏幕的链路
很多新人拿到一个显示bug,第一反应是“我去看一下驱动代码”。但显示驱动在Linux内核里从来不是单个文件,而是一条跨了多个子系统的链路:应用合成内容、DRM/KMS管理CRTC和plane、display controller从显存取数据、encoder把像素转成TMDS/DP/MIPI信号、panel或显示器完成最终的物理显示。任何一个环节出错,最终呈现给用户的都是黑屏、花屏或者闪烁,现象雷同但根因完全不同。
我习惯把它类比成快递链路:包裹本身是framebuffer里的像素数据,快递车是display controller,送货路线是encoder和物理链路,收件人是屏幕。包裹坏了(内容错)、车开错路(时序不对)、路被堵了(带宽不够),最终用户看到的结果都可能是“没收到货”。调试工具的作用,就是在链路上每一站设置检查点,逐步确认哪一环节断了。
所以拿到问题后,第一件事不是翻代码,而是把故障现象拆成几个候选层次,再决定上哪类工具。这个习惯能省掉大量盲目尝试的时间。
1.2 根据现象建立“候选故障清单”,再决定先上哪个工具
整理一下我经常用的症状到工具的映射关系,基本是一个“现象—怀疑节点—首选用工具—辅助确认手段”的四步表:
| 现象 | 优先怀疑的链路节点 | 首选用工具 | 辅助确认手段 |
|---|---|---|---|
| 完全黑屏且背光不亮 | 电源、GPIO、背光配置 | dmesg、regulator debugfs | 示波器测enable和PWM波形 |
| 有背光但无图像 | CRTC/plane配置、scanout地址 | modetest、drm_info | framebuffer内存dump |
| 闪屏、横向噪点 | pixel clock、PLL、信号完整性 | clk_summary、示波器 | 逻辑分析仪测同步信号 |
| 花屏、撕裂 | 内存带宽、stride、vblank | dmesg、igt测试工具 | Perfetto、DRM trace |
| 颜色/灰阶异常 | format、色彩管理、LUT | drm_info、modetest格式列表 | EDID抓包、寄存器LUT校验 |
| 运行一段时间后挂死 | 链路训练、中断、电源稳压 | trace、crash工具、dmesg | 长稳脚本+协议分析仪 |
这张表不是万能药,但它能让你少走弯路。比如背光不亮的黑屏,你上来就抓EDID、改mode,大概率浪费时间;反过来,有背光但完全没图像,你一直查GPIO也不对。显示调试的核心思维是“每次只验证一层”,而工具就是你在那一层取证的手电筒。
2. 内核态三板斧:dmesg、DRM debugfs、modetest
2.1 dmesg 不止看报错,还要看 probe 顺序和失败点
很多人只在出Oops、panic的时候才想起dmesg,其实显示驱动调试里,dmesg更多是用来看驱动的“生命周期”。probe是否成功、是否EPROBE_DEFER、panel节点有没有注册、DP/eDP的link training结果、HDMI的HDCP握手状态,这些都会以内核日志的形式出现。尤其是有多个display相关驱动叠加时,probe顺序错会导致很诡异的现象,比如“第一次开机黑屏,sleep再唤醒就正常了”。
我一般会这么做:
dmesg -T | grep -E "drm|display|panel|dwc|hdmi|dp-aux|backlight" | tail -200把dmesg -T的每个时间点对应到操作步骤上,比如“按下电源键后300ms出现[drm] Enabling DRAM training”,这个时间点本身就是判断链路状态的重要依据。如果log里一直出现EPROBE_DEFER,说明某个regulator、clock或pinctrl还没准备好,导致display子系统的某个节点延迟绑定。这类问题在改动DTS之后特别常见,不是你代码写错,是设备依赖顺序没满足。
我踩过比较典型的一次:改了panel的compatible之后,dmesg里panel-simple节点一直没probe,但系统也没报错,只是HDMI输出始终黑屏。最后靠grep dmesg才发现新compatible没有对应的驱动匹配表,驱动压根没加载。这种问题不看dmesg纯看代码,真的很难定位。
2.2 debugfs 节点比你想的更有用
内核编译时开启了CONFIG_DEBUG_FS之后,DRM子系统会把很多当前状态导出到/sys/kernel/debug/dri/目录。默认路径可能是/sys/kernel/debug/dri/0/,里面有几个关键节点非常值得看:
state:整个DRM设备的atomic state dump,包含每一条CRTC、plane、connector的连接状态、mode、format、fence序号。clk_summary:不是DRM专属,但显示驱动离不开时钟,这个节点能直接看到pixel clock、disp clock、PLL当前跑在多少MHz,parent是否对。regmap:如果display controller驱动用的是regmap接口,调试寄存器有无读写权限会很有用。gpio:看panel enable、reset、背光使能脚的电平状态。
查看示例:
cat /sys/kernel/debug/dri/0/state cat /sys/kernel/debug/clk/clk_summary | grep -E "pll|hdmi|lcdc|dsi" cat /sys/kernel/debug/gpio | grep -E "panel|enable|reset|bl"这里有个实用技巧:state节点里的crtc->vblank_enabled、plane->fb、connector->link_status字段,能直接告诉你内核态是否认为当前链路正常。如果内核态全部正常但屏幕还是黑,那问题就大概率要下沉到寄存器配置或物理层,而不是继续在DRM框架层找bug。
2.3 modetest 与 igt-gpu-tools:把 mode 状态“打印”到用户态
modetest是libdrm自带的测试工具,我一直把它当成“DRM世界里的ls命令”。它能列出当前设备的connector、encoder、CRTC、plane和mode列表,特别适合回答“驱动到底认为自己能输出什么”这个问题。
常用姿势:
modetest -M rockchip -p # 查看当前连接状态和属性 modetest -M rockchip -c # 查看connector支持的mode列表 modetest -M rockchip -s 32:1920x1080@60 # 强制设置某个connector的mode我在调试HDMI输出时会先跑modetest -M xxx -p,确认connector状态是connected还是disconnected。如果内核认为connected、mode也正确,但屏幕不亮,就基本排除了“没有识别显示器”的因素,问题转向encoder/PHY或pixel clock。
不过modetest有个坑:-s这条命令会直接接管显示器输出,如果当前weston或X11还占着DRM master,运行时会报Busy,甚至可能造成显示卡死。建议调试时先停掉桌面合成器,或者用--drop-master配合,避免和现有显示服务打架。另外,modetest的-s在部分平台上是阻塞式的,跑完记得恢复之前的mode,否则屏幕可能一直停在测试画面。
igt-gpu-tools里也有一套更系统的显示测试,比如igt@kms_vblank、igt@kms_flip,可以验证vblank中断和帧翻转是否稳定。遇到闪烁、撕裂这类跟vblank强相关的问题,我会跑一下这两个case,比手动改代码观测效率高得多。
3. 寄存器与内存层面的“显微镜”:devmem、register dump 与 framebuffer 检查
3.1 devmem 读寄存器之前,先确认总线地址和访问方式
当软件层面的DRM状态看起来一切正常,但显示就是不对时,大概率要往下沉到寄存器级。最直接的工具是devmem,它允许从用户态读写物理地址。嵌入式平台上的busybox一般都自带devmem,用法很朴素:
busybox devmem 0x2b000000 32 # 读物理地址0x2b000000处的4字节 busybox devmem 0x2b000000 32 0x1f # 向该地址写值0x1f但devmem不是随便乱用的。首先你得拿到display controller的基地址,通常来自芯片datasheet或DTS里的reg = <0x0 0x2b000000 0x0 0x1000>。其次要清楚当前寄存器是否经过clock gating,有些模块没使能时钟时,读寄存器返回的往往是总线错误或全0,会严重误导排查方向。
我一般不建议一上来就devmem“扫描式”读写寄存器,而是先确认一个具体寄存器该是什么值。比如排查HDMI PHY时,先找到PHY控制寄存器,读配置位和状态位,对照datasheet确认该位代表link training成功还是失败。这里的核心规律是:devmem给你的是“物理层现场”,但解读现场的能力不在工具里,而在你对datasheet的熟悉程度上。
还有个容易忽略的点:devmem读某个被remap成不可访问区域的地址,可能会直接导致内核Oops甚至整机挂死。尤其是地址写错时,总线hang住比软件crash更难恢复。嵌入式开发板还好,拔电重启就行,如果你是在x86平台上直接写BAR地址,很可能把PCIe设备锁死,必须reset系统。
3.2 framebuffer dump 判断内容层是否正常
显示问题的常见纠缠是“到底是GPU没画对,还是display controller没取对”。把framebuffer里的内容dumped出来和屏幕上实际显示对比,是隔离这两层非常有效的方法。
如果是传统fbdev设备,可以直接从/dev/fb0读取:
dd if=/dev/fb0 of=/tmp/fb.raw bs=1024 count=4096如果是DRM/KMS设备,没有标准设备节点时,一个变通办法是从drm_info或modetest -p里拿到plane当前挂的dumb buffer尺寸、stride和format,然后从对应的物理地址用devmem分块读出来。注意DRM一次scanout可能是双缓冲,直接读当前active buffer不一定能拿到正在显示的那一帧,必要时要连读前后两个buffer地址做对比。
dump出来的raw文件可以直接用ImageMagick或Python脚本按已知的width/height/format转成图片,比如RGB888格式:
convert -size 1920x1080 -depth 8 rgb:/tmp/fb.raw /tmp/fb.png如果dump出来的内容和预期图像一致,说明GPU/合成器没问题,问题在取数地址、stride或时序;如果dump出来都是乱码或半截图像,那优先查CPU侧/GPU侧写入framebuffer的路径。这个区分能帮你把排查范围缩小一半。
3.3 从 panic/oops 和 ramdump 里还原显示现场
显示驱动最常见的panic场景是:访问空指针的clock/regulator/reset、并发访问同一条DP链路、或者关闭电源时序时寄存器还挂着。遇到panic,第一件事是把完整call trace存下来,别急着重启。
我会优先看这几处:
PC指针和LR指针指向的函数,是否能对应到显示驱动某个操作函数。DRM state相关的全局变量或struct drm_device是否可打印,里面可能有当前mode、active CRTC、plane设置。- 寄存器的最后写入值,很多时候panic发生在写寄存器之后立即操作返回值,值不对就会panic。
ramdump分析在显示调试里相对小众,但遇到“只有死机现场、没有日志”的长期挂死,它是唯一能还原寄存器状态的途径。分析时最重要的不是看栈,而是把LCD/HDMI控制器寄存器整体导出,和一份已知正常的寄存器快照做diff。只要确定了差异寄存器,往往就能直接指向问题代码。这个思路看起来笨,实际比靠猜高效得多。
4. 协议层链路问题必须上硬件:示波器、逻辑分析仪与 AUX 抓包
4.1 为什么软件工具到链路层就失效
内核日志看到的link training成功、drm状态显示connected,这些都不等于物理信号真的合格。DP的电压摆幅、HDMI的TMDS clock稳定性、MIPI DSI的数据lane eye diagram,任何一个指标不达标,屏幕都会出现闪屏、雪花或时好时坏。这些只能靠示波器和逻辑分析仪去量。
拿eDP举例,source端报告link rate 5.4Gbps、lane count 4,但实际铜线通道损耗过大,面板端训练失败。内核可能反复重试后自动降级到2.7Gbps,现象是“有时候能亮,有时候不能亮,亮度刷新率随机变”。你从软件日志里只会看到一堆link training failed,但真正原因在物理层损耗,必须上硬件测量才能定位。
因此我的经验是:一旦怀疑链路层,就不要再反复改驱动参数碰运气。先看波形,再改代码。工具成本可能会高,但它的时间成本远低于靠猜。
4.2 HDMI/DP AUX 通道抓包的核心思路
HDMI链路里,source和sink之间的DDC通道本质是I2C,速率不高,逻辑分析仪很容易抓到EDID读写过程。你拖一根线到HDMI connector的SCL/SDA上,逻辑分析仪配置成I2C协议解码,就能看到source读取EDID的地址、请求字节数和返回内容。EDID读取失败、校验和错误、某段block超时,这类问题在协议层抓包下无所遁形。
DP/eDP的AUX通道稍微特殊,它是一条单线、双向、Manchester编码的通道,速率在1Mbps左右。用逻辑分析仪抓AUX时,采样率建议至少10MS/s以上,否则Manchester码元容易丢。抓回来后可以先按原始波形看包间隔和ACK位,再配合协议解码器解析DPCD读写命令。你会发现很多“无法进入正常显示”的问题,其实是source反复尝试读取某个DPCD寄存器失败,导致link training永远走不完。
这里有个实操建议:好一点的逻辑分析仪支持在解码结果里直接标出AUX transaction的请求地址和回复类型,比如DPCD Read, Address 0x00100, length 0x04,这样一眼就能看出source在做哪一步握手。不要贪图“全量抓包”,而是要配合代码里的link training步骤,按地址过滤,效率更高。
4.3 MIPI DSI 信号测量的几个关键节点与常见误判
MIPI DSI和eDP/HDMI不一样,它是source和panel之间的近距离高速串行接口。测量DSI时,重点看clock lane和data lane的高/低速率切换时序,以及HS状态下每个lane的差分电压。
我测量DSI的常规顺序:
- 在靠近connector的地方挂差分探头,量clock lane P/N。
- 观察HS burst是否存在,burst频率是否与期望的lane rate一致。
- 分别量四个data lane的差分电压,确认没有某一路明显偏低。
- 用示波器的眼图模式看每lane的眼高、眼宽,确认margin是否足够。
一个常见误判是:在控制器端测量时信号很好,但面板端就花屏。这是因为PCB走线过长或阻抗不连续,导致信号在接收端反射。所以测量点尽量选在面板连接器处,而不是SoC引脚附近,否则你会得出“信号好得很”的结论,实际显示器端已经满屏噪点。
另外,测量DSI时探头的接地方式很关键。用长接地线的普通探头测差分信号,接地电感会带来严重振铃,看到的花纹会让你误以为驱动能力不足。正确做法是用弹簧接地探针或者差分探头,测量点离焊点越近越好。这个细节一开始容易被忽略,等你在噪声波形上白耗半天后才会长记性。
5. 用户态与合成层工具:SurfaceFlinger、drm_info、Perfetto 的组合使用
5.1 drm_info 与内核 DRM 对象的完整视图
drm_info是查DRM当前状态非常好用的用户态工具,它比modetest -p更结构化,能显示connector的物理状态、支持的pixel format、plane的capabilities、property的值,以及当前绑定的CRTC。
调试格式问题时,重点看plane支持的format列表。比如一个YUV420的video plane同时连接在主屏上,如果你把NV12 buffer直接送进Primary plane,display controller可能拒绝显示或者显示成一片绿色。drm_info能清楚告诉你每个plane允许的format簇,这比反复改应用层代码验证要快得多。
drm_info --all输出里会看到类似:
Plane 0 (type: Primary) formats: XR24 AR24 RG24 NV12 crtc: 32确认format没问题后,再查connector上当前的scaling mode、color space、hdr_output_metadata等属性。颜色不对或画面发灰,往往是这里的属性配合出了问题,而不是驱动色彩引擎写错。
5.2 Android 合成层:把责任从硬件驱动里摘出来
如果你调的是Android设备或电视盒子,显示链路里夹着一层SurfaceFlinger和HWC。很多时候屏幕花屏、闪帧,根因根本不在DRM驱动,而在合成策略、layer损坏区域或者buffer fence没有等到。这时候必须用Android侧的调试接口去“摘责任”。
两个最常用的命令:
adb shell dumpsys SurfaceFlinger | grep -A 30 "Display 0" adb shell dumpsys display | grep -E "mBaseDisplayInfo|DisplayDeviceInfo"dumpsys SurfaceFlinger会列出每一层的layer名字、来源buffer尺寸、transform、damage region以及合成方式。如果某个layer的type是Client,说明它走了GPU合成而非硬件overlay,对应的掉帧问题要往SurfaceFlinger侧看;如果type是Device,说明走的是HWC硬件合成,此时出问题再往DRM驱动里查。
dumpsys display则能看到系统认定的显示模式、刷新率、color mode和HDR能力,判断“系统期望”和“驱动实际支持”之间是否对齐。我遇到过一个问题:系统UI显示56Hz,而驱动只支持60Hz/48Hz,结果播放视频时出现周期性卡顿,看dumpsys瞬间就明白了。
5.3 Perfetto 抓取显示管线 trace 的配置示例
Perfetto已经成了Android显示性能问题的标配抓取工具。它能把SurfaceFlinger合成、vblank、sf fence、DrmDriver worker这些事件放在统一时间轴上,特别适合回答“一帧到底是在哪里等了很久”这个问题。
简单抓取命令:
adb shell perfetto -o /data/misc/perfetto-traces/display_trace.perfetto-trace -t 10s -b 32mb sched gfx drm如果你的内核里开了DRM的tracepoint,还可以手动指定ftrace事件来抓vblank和atomic commit的时间戳。抓完拉到本地:
adb pull /data/misc/perfetto-traces/display_trace.perfetto-trace ./display_trace打开Perfetto UI后,先看SurfaceFlinger和DrmDriver两个track交错的区间。如果DrmDriver::Commit线程每次都在vblank后一帧才开始工作,说明atomic commit被延迟了,往线程优先级或锁竞争方向查。如果SurfaceFlinger等待acquire fence的时间很长,问题很可能在GPU/CPU合成速度,而不是显示驱动本身的时序。
Perfetto的显示能力很强,但不要每帧都全量抓。一次抓10秒,配合复现操作,抓到问题帧后立刻停。先小后大、先粗后细,不要一上来就抓5分钟几十MB的trace,分析时反而找不到关键点。
6. 从工具到结论:几个实际症状的排查链路与踩坑经验
6.1 黑屏:先查电源/使能,再查 mode,后查 scanout
黑屏是我遇到最多、也最容易误导人的问题。经验是“先问电源和使能,再问modalias和mode,最后才问scanout”。
有一个案例是这样的:嵌入式平台的HDMI输出,内核dmesg显示connector已连接,modetest也能列出1080p模式,但接上显示器就是无信号。用显示器和示波器对比后才发现,HDMI_5V或HPD引脚电平正常,但TMDS clock根本没有输出。回头看寄存器配置,发现HDMI PHY的PLL用了错误的外部参考时钟,DTS里配的24MHz和实际晶振27MHz不一致,导致所有mode的pixel clock全部算错。
这个案例里,如果我先花时间查framebuffer地址,就完全走偏了。正确的排查链路是:dmesg确认真实设备状态,debugfs确认clock tree,最后再用示波器验证PHY输出是否有信号。每一步都在回答“这一层有没有活着”这个问题。
6.2 闪屏和撕裂:不要忽略 vblank 和 fence 的“时间差”
闪屏很多时候不是因为像素内容错乱,而是因为vblank时机乱了。DRM驱动在atomic commit时会把新buffer的fence提交上去,等到显示器下一次vblank来临时切换scanout。如果驱动实现的vblank计数不准,或者compositor在旧帧还没有完整显示时就开始翻页,用户就会看到撕裂和闪烁。
工具选择上,我会先跑igt@kms_vblank这类case,看vblank interval是否稳定、是否有漏中断。接着用Perfetto抓一段正常播放视频的trace,观察DrmDriver提交commit的时刻和vblank间隔的重叠关系。如果发现commit经常跨越两个vblank周期,说明不是驱动代码逻辑错,而是有锁竞争或线程调度延迟导致提交晚了一帧。
还有一个容易忽略的点:Linux内核的CONFIG_DRM_DEBUG_MODESET和CONFIG_DRM_USE_DYNAMIC_DEBUG打开后,能看到更多atomic check/commit的细节。遇到闪屏,别急着改硬件参数,先把这两个选项打开抓一次内核日志,往往能看到driver内部对mode的调整动作。
6.3 花屏:检查 stride、地址对齐和内存带宽
花屏的根因比黑屏更容易定位,但也更容易误诊。一次比较典型的花屏是:画面显示正常色块的左边有大块错位像素,像图像被“切”成了两段。后来排查发现是应用层计算buffer的stride时,没有为NV12格式做按行对齐,导致显示控制器按128字节对齐去取数时,每行起始地址偏移了一个固定错误。
遇到花屏时,我会优先做两件事:第一,用drm_info确认当前buffer的width、height、stride和format;第二,把framebuffer dump出来和预期画面比。如果dump出来的画面里错位位置和屏幕完全一致,说明内容写入端或stride计算有问题;如果dump画面正常但屏幕错位,那就是display controller取数参数或地址对齐配置有误。
硬件对地址对齐的约束,芯片手册里通常会写,比如“scanout buffer基地址需按32字节对齐”。这类约束经常是导致“偶尔花屏”的隐蔽原因。代码review时肉眼很难发现,但一旦你把dumped image和寄存器配置放在一起看,问题往往几秒钟就暴露了。
最后说一个个人的习惯:每次处理显示问题,我都会在一张纸上写“现象—候选层—用什么工具—工具给出的证据—下一步动作”。这个习惯看起来简单,但真的能逼自己不去乱猜。显示驱动的坑,绝大多数不是工具不够先进,而是没有按层去缩小嫌疑范围。把每一层用工具验证一遍,结论自然浮出来。