
搞显示驱动调试跟调普通的内核驱动还真不太一样。普通驱动调不通顶多是日志刷屏、功能缺失显示驱动要出问题产品拿在手里就是一块砖——黑屏、花屏、闪屏、绿屏、撕裂哪一个都够喝一壶。这些年我从消费类嵌入式一路做到带SoC的显示子系统前前后后经手过RGB LCD、MIPI DSI、eDP、HDMI、DP各种接口踩过的坑两只手数不过来。今天这篇接着说我的第5篇记录专门聊聊显示驱动调试里那些真正“救命”的调试工具。说“救命”可能有点夸张但确实我大部分疑难问题最后都是靠一套工具组合拳解决的而不是靠某个神秘神器。这里说的工具不单指某个软件还包括命令、内核接口、debugfs、协议分析手段甚至一些简单的脚本。目的是帮刚入行或者正在被屏搞崩溃的朋友建立一套属于自己的调试工具链知道黑屏时先敲什么命令花屏时去哪里拿数据时序不对时怎么验证链路训练失败时怎么定位是上游还是下游的问题。这篇内容以Linux下的DRM/KMS显示驱动为背景兼顾嵌入式Android平台大部分命令和思路是通用的。1. 先搞清楚“屏不亮”是哪个环节的问题分段定位法每次有人拿着设备来找我说“屏不亮”我几乎不会直接去看驱动代码。显示链路太长了从App合成、framebuffer、KMS提交到CRTC扫描输出、Encoder编码、PHY信号再到面板侧的TCON初始化和背光任何一个环节断了都是黑屏。而“黑屏”这个现象本身是最没有信息量的。1.1 显示链路分段与对应工具我习惯把整条链路由上到下切成四段内容生成段App/Weston/SurfaceFlinger合成出帧最终落到某个framebuffer或dma-buf里。如果这一段挂了系统可能还在跑但屏幕就是全黑。KMS/DRM核心段Plane、CRTC、Encoder、Connector这四件套的状态组合。模式是否设置成功、时钟是否锁定、扫描是否开始都是这里决定的。物理链路段DP/HDMI/MIPI DSI/eDP的信号、HPD、链路训练Link Training、时钟和电压摆幅。这一段的问题经常表现为“系统认为屏已经亮了但物理上没信号”。面板端供电、背光、初始化序列、寄存器配置。MCU屏或者带TCON的屏尤其容易出现“背光亮但无图像”的情况。对应的工具也就分成了几类modetest、drm_info看KMS状态dmesg和动态日志看内核链路edid-decode、i2c-tools、DPCD寄存器读取看链路物理层示波器和逻辑分析仪则是最后的硬件兜底手段。我见过不少工程师一上来就抱着逻辑分析仪去测MIPI信号其实先用软件工具确认驱动状态往往十分钟就能定位到具体环节根本不用动仪器。1.2 典型症状与第一个诊断动作下面这张表是我自己总结的平时排查问题先对号入座症状最可能的环节第一个诊断动作完全黑屏背光也不亮供电/背光/面板查GPIO、背光驱动、dmesg里的probe状态背光亮屏幕无图像内容生成或KMS扫描modetest看CRTC状态检查framebuffer花屏/雪花点帧数据或时序失配截帧比对检查pitch和色彩格式闪屏/周期性闪烁背光调节或HPD抖动查背光PWM频率看HPD中断是否频繁分辨率不对/画面偏移模式设置或EDID解析edid-decode解析确认timing参数色彩异常格式转换/YUV-RGB检查drm_format、color space属性撕裂缓冲切换时机检查VBlank同步、GAMMA/CTM优先级这里特别提醒一下背光亮但无图很多人先去查panel初始化序列其实是走偏了。这种症状里十有八九问题出在CRTC没有真正开始扫描输出或是framebuffer句柄为空。用modetest一眼就能看出来。我自己常用的排查顺序是先dmesg看有没有驱动报错再modetest看KMS对象状态然后截一帧数据看内容最后才去动面板和物理链路。按这个顺序走绝大多数问题能在一刻钟内锁定范围。2. 日志层工具dmesg与动态调试是基本盘显示驱动调试离开内核日志基本寸步难行。内核日志是驱动与外部世界沟通的唯一通道很多显示问题其实内核已经明确告诉你原因了只是没人仔细看。2.1 从dmesg里读出关键信息驱动probe阶段先确认设备有没有被正确识别。我通常会先grep几个关键词dmesg | grep -i -E drm|display|dpu|dsi|hdmi|dp|edp|panel重点看三类信息。第一类是有没有-EPROBE_DEFER这说明驱动依赖的某个资源还没准备好比如电源域、时钟、复位控制器系统会自动晚点再试。第二类是probe直接失败返回-ENODEV或-EINVAL这时候要看具体是哪个子设备没找到经常是I2C不通或者GPIO申请失败。第三类是模式设置相关的信息比如“failed to set mode”“CRTC mismatch”这会直接告诉你KMS状态机卡在哪。举一个典型例子我调一块eDP屏时dmesg里循环刷“edp_link_train link training failed”说明入物理链路训练阶段就失败了后面再查DPCD寄存器时目标非常明确。如果没有dmesg先行定位我可能会把面板初始化、背光、framebuffer全查一遍纯浪费时间。2.2 动态打印不用改代码重编内核很多显示驱动的日志默认是关掉的尤其是DRM子系统和具体厂商驱动里那些详细的寄存器操作日志。如果每次都要加printk然后重新编译内核迭代速度太慢了。Linux的动态调试机制能直接在运行时打开指定文件或函数的日志。前提是内核开启了CONFIG_DYNAMIC_DEBUG# 打开某个驱动的所有动态调试日志 echo file drivers/gpu/drm/msm/dpu/dpu_crtc.c p /sys/kernel/debug/dynamic_debug/control # 打开整个DRM子系统的日志 echo file drivers/gpu/drm/* p /sys/kernel/debug/dynamic_debug/control # 关闭 echo file drivers/gpu/drm/msm/dpu/dpu_crtc.c -p /sys/kernel/debug/dynamic_debug/control配合echo 0x1f /proc/sys/kernel/printk把printk级别放到最低就能看到所有调试信息。有一点必须提醒DRM子系统的日志量极大一旦全开串口输出基本会被淹没系统可能因为打印阻塞卡顿。我的习惯是先全开抓到一次完整启动日志然后立刻关闭再把日志存下来慢慢分析而不是开着全量日志去跑功能测试。提示打开动态日志前先用df -h确认存储空间swappiness高的设备上日志写满根分区会死得很难看。别问我怎么知道的。另外厂商SDK经常自带pstore/ramoops机制重启后能在/sys/fs/pstore里翻到上一次崩溃前后的kernel日志。看显示驱动panic先翻这个目录往往能拿到panic前最后的显示状态比如是哪条提交路径触发的异常我当时就是靠它定位过一个CRTC操作空指针的问题。3. DRM子系统官方诊断命令modetest与drm_info的组合拳如果说日志是被动的等待信息那么modetest和drm_info就是主动出击。这两个工具能从用户态直接查询KMS子系统的当前状态是显示驱动调试出场率最高的两个命令。3.1 modetest查询与设置模式modetest来自libdrm工具集在Ubuntu上装libdrm-tools嵌入式平台一般编进rootfs或者交叉编译一份丢到板子上。# 列出所有连接器、CRTC、encoder、plane modetest -M rockchip # 查看具体一个DRM设备的详细状态带属性 modetest -M rockchip -p -c -d最常用的玩法是先看当前连接器连接状态和可用分辨率modetest -M rockchip -c输出里会列出Connector、encoder、mode列表包括分辨率和刷新率。如果这里显示connected并且mode列表正常说明物理链路层大半是好的。想手动测试模式是否正常可以直接指定一条模式modetest -M rockchip -s 32:1920x108060这条命令的意思是在编号为32的CRTC通常是Connector对应的CRTC上直接设置1920x108060的模式。如果设完屏能亮起来说明驱动核心的扫描输出链路OK问题在应用层。如果设完黑屏再回来查CRTC状态看是不是时钟没锁住。有个坑要提醒在产品板上直接跑modetest -s有风险。它会把当前显示管线的状态直接改掉Weston或者SurfaceFlinger那边可能瞬间失联桌面直接黑掉。跑之前先确认已经停止上层显示服务比如用killall weston或者stop surfaceflinger。3.2 drm_info比modetest更细的属性视图modetest擅长看对象和模式drm_info来自drm_info这个小工具更适合看属性。同样是查询drm_info会把每个CRTC、Encoder、Connector、Plane上挂的属性全部以键值对的形式列出来包括当前值和可枚举的值列表还能看色彩管理相关的属性。比如查DP连接器的链路带宽、lane count、色彩深度这类信息用modetest只能看到粗粒度结果drm_info却能直接反映max_link_rate、max_lane_count、link_status等属性。drm_info -j-j参数能输出JSON格式方便脚本解析后做自动化检查。我写过一个巡检脚本每小时跑一次drm_info把连接状态、分辨率、色彩空间存进文件用来定位偶发黑屏发生前链路状态是否有变化。3.3 用IGT和kmscube做压力验证modetest能设模式但真实场景下的视频流连续输出、多plane叠加、旋转缩放这些压力还是得靠更重的工具。IGT GPU Tools里有一堆kms_*测试用例比如kms_plane、kms_cursor_crc、kms_color是验证KMS功能完备性的标准集。kmscube则是跑一个不断旋转的立方体GPU持续渲染并输出到屏幕适合快速验证整个渲染显示链路是否通畅。调试一个偶发花屏问题时我开kmscube连续跑半小时同时后台开CRC校验抓帧最后在日志里抓到一处pipe underflow的报错问题就定位在内存带宽不足上。没有这种连续负载单纯点一下屏幕很难复现问题。4. 帧数据验证截帧、像素比对与CRC校验驱动跑起来了屏也亮了但画面内容不对这阶段的核心问题变成屏幕上显示的数据到底是不是我预期的那一帧。这时候就要做帧数据验证。4.1 手工抓取framebuffer并解析先说最简单的办法读/dev/fb0dd if/dev/fb0 of/tmp/frame.raw bs4096 count1000这是直接把当前framebuffer的原始数据导出来。拿到raw文件后可以用Python脚本解析RGBA/BGRX格式的数据检查是不是全黑、是不是固化在某个pattern或者特定区域颜色是否异常。例如判断一帧里面正常的非黑像素占比import sys data open(/tmp/frame.raw, rb).read() w, h, bpp 1920, 1080, 4 pixels [] cnt 0 non_black 0 for i in range(0, len(data) - bpp 1, bpp): b, g, r, a data[i:i4] if r g b 30: non_black 1 print(non-black ratio: %.4f%% % (non_black * 100.0 / (w * h)))全黑屏和内容正常从这个比值上立刻就能区别开。花屏则往往表现为非黑像素比例异常或者某块区域的颜色值出现规律性错位。但这里有个必须强调的坑现代显示驱动里用户态的Surface和内核正在扫描输出的Buffer往往不是同一个。你读/dev/fb0拿到的可能只是写入端的数据不是当前扫描出去的画面。多缓冲开启后硬件正在读的可能是另外一块DMA buffer。想真正抓“当前正在显示的那一帧”得找到scanout buffer的物理地址或者用驱动为调试提供的debugfs接口来抓取。我调过的一个平台就提供/sys/kernel/debug/dri/0/state和单独的buffer dump节点比读/dev/fb0可靠得多。4.2 用CRC机制做自动比对比手工解析raw数据更高效的是CRC校验。很多显示控制器硬件支持按帧计算CRCIGT工具里kms_crc_crc就是干这个的。以Intel/AMD平台为例igtkms_crccrc-primary这个测试会计算光标移动前后画面的CRC差异用来验证合成路径是否把内容正确提交。嵌入式平台也有类似机制有的通过drm_crtc_get_crc导出到debugfs。实践中我会先抓一帧“标准参考画面”的CRC值然后运行待测用例再抓一次CRC两次不一致就说明画面发生变化配合时间戳可以判断是否有一帧丢失。花屏问题如果频繁出现CRC还能定位到具体是哪一层出了问题先屏蔽所有plane只留primary再逐个叠加plane每开一层抓一次CRC和参考值对比锁定出问题的plane。这个方法不用示波器、不用肉眼盯屏幕效率非常高。5. 时序与链路数据EDID、DPCD与寄存器读写的排查思路软件状态、帧内容都验证过了如果屏还出问题大概率硬件链路上有东西对不上。这一层调试的核心就是读数据和改数据。5.1 EDID解析先确认显示器“自称”是什么HDMI/DP外接屏场景EDID是显示器向主机描述自己能力的小卡片分辨率上限、支持刷新率、色彩格式全在这里。读写EDID的工具最常用的是edid-decode# 从 /sys/class/drm/ 里读出已解析好的EDID cat /sys/class/drm/card0-HDMI-A-1/edid | edid-decode如果输出显示显示器支持的最大分辨率和你设置的不一致那模式设置失败的原因就找到了。还有一类问题是EDID里的timing参数解析后导致画面偏移或者刷新率闪烁edid-decode的--extract选项能直接生成二进制块换个平台复现问题很方便。嵌入式MIPI DSI屏没有EDID时序完全靠驱动的初始化序列来定。这时候时序验证就得靠示波器测量H/V sync和像素时钟或者靠驱动自带的MIPI DSI debugfs接口dump当前时序参数。通常/sys/kernel/debug/dri/0/DSI-1/下能看到lane状态和当前data rate。5.2 DP链路训练读取DPCD寄存器DisplayPort的物理链路训练失败是eDP内嵌屏和DP外接屏最常见也最难调的故障之一。DPCD寄存器就是DP链路里显示器固件提供给主机的状态寄存器很多关键信息都在里面。常用寄存器地址和作用地址含义0x0000DPCD版本0x0001最大lane count0x0002最大link rate0x00100-0x00107link status / lane状态0x00200-0x00203Sink侧链路控制0x00600-0x00611Sink设备能力扩展很多DRM驱动会把DPCD内容dump到debugfs比如cat /sys/kernel/debug/dri/0/DP-1/dpcd手动读不到的时候可以用i2c工具通过DP的AUX通道读DP AUX在逻辑上挂在I2C总线上i2cget -y -f 0 0x0a 0x0001链路训练失败时重点查0x00100附近的link status字段。如果lane0和lane1显示CRSClock Recovery Success而更高lane失败说明链路带宽不足或者面板端某个lane的物理连接有问题。一次eDP调试中我读到lane1的CRS一直成功但lane2始终失败最后发现是连接器上一颗电阻虚焊导致差分对阻抗不匹配。这种问题用软件工具定位到lane级别再回到硬件去查范围一下就缩小了。5.3 寄存器/总线调试工具的思维方式寄存器操作是显示驱动调试最底层的动作。不管你是用RT-Thread、裸机还是Linux最终驱动都是在读写一堆寄存器。好的调试习惯是把“读寄存器→改寄存器→确认结果”做成一个完整的闭环否则很容易出现“觉得改对了其实没生效”的幻觉。我见过很多工程师往驱动里临时塞一段寄存器的初始化代码结果屏不亮就挨个怀疑寄存器值——这太低效了。正确做法是先建立一份寄存器状态快照把所有相关寄存器当前值读出来和芯片手册对比再动手改。这个快照相当于总线调试工具提供的“寄存器视图”改之前先看清现状。在这个思路下我写了一个简单脚本批量读取显示控制器常用寄存器组输出一份格式化文本保存下来每次调试前后各存一份用diff对比差异立刻就能看出哪些写操作真正生效了哪些被后续流程覆盖了。很多“改了却没用”的寄存器问题都是这样定位到“被另一个驱动或bootloader覆盖写入”的。6. 性能、稳定性与远程调试场景功能调通只是第一步显示驱动的稳定性才是量产前的拦路虎。偶发黑屏、帧率波动、长时间运行后bandwidth不足这类问题最难缠因为它们不总是复现。这阶段需要的是性能分析、状态转储和远程日志这三件套。6.1 用perf和ftrace跟踪帧率与带宽帧率不稳往往不是显示控制器的问题而是上游渲染来不及。用ftrace的DRM事件可以跟踪提交到完成的时间trace-cmd record -e drm:* -e dpu:* trace-cmd report如果是MSM/DPU这类带独立硬件合成器的平台dpu:*事件能看到CRTC提交、带宽计算、Clk请求变化能清晰看出每帧在哪个环节耗时。perf则适合统计调用频率和等待时间。例如用perf top观察kworker和irq线程占比如果显示相关中断占用过高说明中断风暴正在发生常见原因是HPD引脚抖动导致频繁触发模式重设。我处理过一个“屏幕每隔几秒闪一下”的问题用ftrace抓HPD中断频率发现每秒几十次后来在驱动里加上了去抖逻辑才真正解决。6.2 状态转储与“抓现场”的崩溃调试思路遇到驱动崩溃或者画面卡死临时去开系统日志往往来不及。我的习惯是提前布置好“现场收集”机制。现代系统里panic时可以通过pstore保存内核日志但还不够最好把DRM的完整状态也dump出来。这跟很多二进制调试工具里“动态转储现场”的思路一致在系统进入异常状态前先把进程内存、寄存器、状态栈整个搬出来离线分析而不是盯着黑屏猜。对显示驱动而言运行时把每个CRTC/Plane/Connector的属性、当前缓冲地址、中断状态位都导出来就是“抓现场”。实际做法是预先准备一个shell脚本#!/bin/bash ts$(date %s) mkdir -p /data/debug/$ts cat /sys/kernel/debug/dri/0/state /data/debug/$ts/state.txt cat /sys/kernel/debug/dri/0/DP-1/dpcd /data/debug/$ts/dpcd.txt dmesg /data/debug/$ts/dmesg.txt cat /proc/interrupts /data/debug/$ts/interrupts.txt modetest -M $(ls /dev/dri | grep card | head -1) -p /data/debug/$ts/mode.txt把脚本做成服务周期性自动收集黑屏发生时立刻手工执行一次对比前后两份快照差异往往就是问题触发器。这套做法曾经帮我定位过一个极难复现的偶发黑屏两份快照对比后发现CRTC的active标志没变但plane的framebuffer地址变成了0最终追到是framebuffer被提前释放导致的use-after-free。6.3 nc转发日志与脚本化自动调试很多嵌入式设备只有一个串口调试时插着线线不方便或者设备部署在远端日志根本拿不回来。netcatnc是解决这个问题的利器。把设备串口或syslog转发到PC# 设备端把内核日志持续输出到TCP dmesg -w | nc -u -l -p 12345 # PC端接收并保存 nc -u 192.168.1.100 12345 dmesg.log也可以用更简单的方式从设备主动拉到PC# 设备端 cat /dev/kmsg | nc 192.168.1.50 9000 # PC端 nc -l -p 9000 /tmp/kmsg.log实际使用中一定要留意kmsg是全量内核消息体量非常大如果设备网络带宽有限日志转发本身可能造成卡顿。我通常先做一次过滤dmesg -w | grep -E drm|dpu|display|panel|link | nc 192.168.1.50 9000这样PC端拿到的就是干净的相关日志。配合脚本自动化可以做到“问题一发生就自动记录上下文”。我在Lua或者Shell里写监控逻辑轮询/sys/class/drm/card0-DP-1/status一旦从connected变为disconnected就立刻把status、dmesg尾部、dpcd状态、modetest输出打包上传到服务器。这个自动化收集装置在我处理远程设备故障时价值极大不再需要用户在手忙脚乱中手动抓日志。提示自动监控脚本最好做成独立进程别挂在主显示服务里。否则一旦显示服务崩溃监控也跟着死就失去意义了。写在最后工具说到底只是放大器真正有价值的是排查思路。回到开头说的分段定位法出问题先问“哪一段”再选对工具。日志确认驱动状态modetest确认KMS对象帧工具确认内容链路工具确认物理层性能工具确认资源和时序远程工具负责把现场搬回PC。每一类工具解决一类环节的问题组合起来就是一条完整的排查链路。我个人这些年体会最深的一点是显示驱动调试最怕的不是问题复杂而是信息混乱。所有能自动收集的工具都建议提前布置好所有能留下快照的手段都值得在问题出现之前先跑一遍。等黑屏真的发生时你手上有可以对比的现场数据比什么都强。如果你手头也有一套自己常用的显示驱动排查工具链和方法欢迎在评论区补充分享。写这类文章本身就是一种整理我也是在整理过程中把之前零散的经验串成了体系希望对你也有用。