做显示驱动的开发者,多半都经历过这样的场景:屏幕黑一下、闪一下、花一下,你盯着面板,它隔几秒又自己恢复了。问题看起来是一块屏的事,真正要查的却是从内存帧数据、显示控制器到物理链路的整条通路。显示驱动难调,难就难在它“看不见”——不像网络驱动能 ping,不像存储驱动能 read/write,画面异常经常没有任何报错,只能靠调试工具一点一点把系统内部的状态“翻译”出来。这篇不是工具手册,是我这些年调试显示驱动过程中反复在用的工具梳理,以及对应的使用思路。内容以 Linux 平台 DRM/KMS 显示驱动为主线,适合刚接触显示驱动、准备做 bringup,或者在处理各种屏幕异常的内核开发者参考。我会按静态信息、动态追踪、自动化验证、实战排查四个层次来讲,最后用一个我实际处理过的间歇性黑屏案例,把这套工具链串起来看。
1. 显示驱动调试在调什么:先把问题分类,工具才谈得上
我曾见过不少人拿到一个显示异常的问题,第一反应就是抓 dmesg。其实这是效率最低的入口。显示驱动的状态非常多,日志只能反映其中很小一部分。如果把问题分类想清楚,选工具的准确度会高很多,排查时间能省下一大半。
1.1 状态类问题:模式、连接器与数据通路
状态类问题指的是显示链路中各个对象的状态不对。DRM/KMS 体系里有几个核心对象:CRTC、Encoder、Connector、Plane,它们之间的拓扑关系决定了“哪条通路在干活”。状态类问题典型表现是:屏幕完全黑着没有任何反应、外接屏插上不亮、系统分辨率不对、鼠标能动但桌面不出来。
这类问题最需要的是把每个对象的状态全部摊开看,而不是猜。比如系统启动后外接 HDMI 没点亮,你至少要知道:Connector 有没有检测到设备,检测状态是 connected 还是 disconnected;CRTC 有没有被使能;Encoder 有没有绑定到 CRTC 上;Plane 上有没有挂 framebuffer。这四件事,每一个都有可能单独出问题,而它们之间又是联动关系。状态类问题如果靠打日志去查,往往要反复编译内核,效率极低;直接查内核维护的状态节点,反而快得多。
1.2 时序类问题:分辨率、刷新率与信号质量
时序类问题和状态类问题不一样,状态类问题是“有没有”,时序类问题是“对不对”。面板加上电,驱动也能检测到连接,但画面就是抖动、闪烁、显示区域偏移、水波纹,或者在某些分辨率下不正常,这类基本都指向时序。
时序问题涉及的参数有 pixel clock、porch、blanking、帧率等。嵌入式平台常见的做法是直接看屏幕 datasheet,从里面抄一组初始化参数填进驱动,但拷贝过程很容易出问题。比如 porch 数值填错,屏幕不会完全不亮,而是显示区域偏移或者边缘有彩条;pixel clock 超了带宽,画面会噪点、闪动。这里有个关键点:时序参数在系统里的存在形式不止一份。控制器有一份,面板初始化代码里有一份,modetest 查询到的 mode 列表里还有一份,三份必须对得上。调试时序问题,离不开能把 mode 参数原样打印出来的工具。
1.3 内容与稳定性问题:撕裂、闪烁、卡顿
最后一类是内容层面的问题,特征是链路已经正常工作、画面也在出,但画面本身不正常:播放视频撕裂、鼠标移动卡顿、窗口拖动有残影、长时间运行后画面突然冻结。这类问题往往涉及 buffer 管理、vblank 同步、fence 机制、内存带宽,比前两类更隐蔽,因为它们很多时候是偶发的,复现都不容易。
内容类问题最常见的两个方向:一个是撕裂,本质是屏幕刷新过程中读到了两个不同 buffer 的内容,原因是更新 buffer 的时机没有跟 vblank 对齐;另一个是 flink 或 fence 超时,表现为画面冻结后报 "flip_done timed out",这类基本可以确定是 CRTC 的 vblank 中断没有按时产生,或者产生了但驱动没有正确处理。处理这类问题,需要工具能够验证 vblank 事件是否持续稳定,还要能做长时间的自动化压力测试,靠肉眼盯画面是盯不出结果的。
把问题分成这三类之后再回头看工具,思路就清晰了:状态类问题用静态查看工具,时序类问题用模式列举和参数打印工具,内容类问题用事件追踪和自动化测试工具。接下来我从静态信息开始,逐个讲。
2. 静态信息四件套:让系统自己说哪里不对
静态工具的特点是“低风险、信息量大”,它们本身不会改变系统行为,只是把内核里已经存在的状态暴露出来。我的习惯是,接到任何显示问题先做一轮静态信息采集,再做任何修改。这相当于给现场留档,后面改出问题还能对比。
2.1 dmesg 与 drm.debug:日志不是越多越好,而是要对位看
dmesg 是大多数人的第一反应,但它能提供的信息其实比想象中少。DRM 子系统的默认日志级别很低,很多关键路径根本没有打印。真正想让内核把显示相关的话说全,需要打开 drm.debug 参数。
drm.debug 是一个按位掩码控制的开关,可以在启动时传,也可以运行期写:
# 运行期打开,0x1f 是常用全量级别 echo 0x1f > /sys/module/drm/parameters/debug # 也可以在启动参数里加 # drm.debug=0x1f这个参数的控制粒度很细:每一位对应一个子模块,比如原子提交、驱动核心、KMS 状态切换、vblank、状态校验等。我实际使用中不会上来就开全量,日志风暴会把真正的问题淹没。更稳妥的做法是从 0x04 起步,只开 KMS 相关的日志;如果怀疑原子提交流程有问题,再叠加 0x10;如果怀疑状态切换异常,叠加 0x40。一边复现问题一边按位加,能比较快定位到问题出现的模块。
这里有个经验:开 drm.debug 之后,dmesg 里会多出大量带drm:前缀的打印,不要只看有没有报错,要关注两条主线。一条是[drm] Resume和[drm] Display之类的状态切换打印,另一条是[drm:drm_atomic_commit]这样的提交路径打印,它们能告诉你每次模式切换时都干了什么、走到了哪一步。很多黑屏问题就是卡在某一步之后没有后续打印,功能停留的位置往往就是出问题的位置。
另一个容易被忽视的点是 vblank 相关的超时日志。如果你在 dmesg 里看到类似vblank wait timed out或者flip_done timed out的提示,基本可以断定 vblank 事件有问题,这在静态信息阶段就能给后面的排查定下方向。
2.2 debugfs 的 dri 节点:把 DRM 状态“展开”给调试者
dmesg 是流水线式的打印,只能看到过去发生的事,而 debugfs 下的 dri 节点展示的是“当前这一刻”的完整状态。显示驱动调试,有一半时间是在看这些节点。
以常见的主 DRM 设备为例,对应的目录是/sys/kernel/debug/dri/0/。这个目录里的state文件是最有价值的一个,它会把当前设备上所有 CRTC、Encoder、Connector、Plane 的状态和属性一次性列出来。我排查黑屏问题时的固定动作,就是先 cat 一下这个文件:
cat /sys/kernel/debug/dri/0/state输出里能看到每个 CRTC 的 active 标志、mode 参数、是否有 vblank 计数,能看到 Connector 的 connected 状态、连接的 encoder 是谁,还能看到每个 Plane 上挂的 framebuffer、格式、坐标信息。这一份输出就能回答我在第一节里提到的“有没有”的四连问。
除了state文件,framebuffers文件会列出当前所有已分配的 framebuffer 及其引用计数,当你怀疑 buffer 泄漏或引用计数异常时很有用。另外,如果某个 connector 对应的是内屏 eDP 或者外接 HDMI,在/sys/class/drm/下还有个特殊入口:
cat /sys/class/drm/card0-HDMI-A-1/status cat /sys/class/drm/card0-eDP-1/modesstatus文件会主动触发一次连接检测后返回连接状态,modes文件列出当前支持的显示模式。这两个文件在排查“插上不亮”和“分辨率不对”时,比 dmesg 直观得多。
2.3 drm_info 与 modetest:从拓扑到属性的关系图谱
debugfs 解决的是“状态对不对”的问题,但很多时候还需要回答“为什么这个状态不对”,这时候需要看对象之间的关系和属性。drm_info 和 modetest 两个工具在查关系方面相当顺手。
drm_info 会把整个 DRM 设备的拓扑——主设备、渲染节点、CRTC、Encoder、Connector、Plane——以及每个对象上挂的所有 property 和当前值完整列出来。这些信息以树状结构呈现,比 debugfs 的纯文本好阅读得多。我通常在刚拿到一块新板子、对显示通路完全不了解时,先跑一遍 drm_info,五分钟就能建立起“这台设备的显示链路长什么样”的概念。
modetest 则是更偏查询和操作两用的工具。查询方面,它有多个参数分别列出各类对象:
# 列出所有 connector 及其支持的模式 modetest -c # 列出所有 plane modetest -p # 列出所有 encoder modetest -e实际排错时我常用的是modetest -c,它会打印 connector 的物理状态、支持的 mode 列表、EDID 里解析出来的信息。比如外接屏分辨率不对,先确认modetest -c输出的 mode 列表和屏幕原生分辨率是否一致。如果列表里根本没有 4K 60Hz,那问题多半出在 EDID 解析或者 mode 过滤逻辑上;如果列表里有但切过去黑屏,那再往时序和链路带宽方向查。这一步能把问题快速切分到“模式有没有”和“模式能不能用”两个完全不同的方向上。
3. 动态追踪:函数调用链和寄存器操作全记录
静态工具看的是“状态”,动态工具看的是“过程”。很多显示问题并不是状态冲突,而是同一时间段内多个事件交织导致的,比如 vblank 中断和 buffer 释放同时发生、runtime PM 切换时和 flip 提交竞态。这类问题的特征是无法通过当前状态解释,必须看行为发生的时间线。
3.1 ftrace 追踪 drm 路径的关键函数
ftrace 是内核自带的动态追踪机制,不需要重新编译内核就能在函数级做拦截。对显示驱动调试来说,最有价值的是用它追踪原子提交、模式切换、vblank 处理这几个路径上的关键函数,从而知道“代码到底有没有走到某个分支”。
用 ftrace 追踪 drm 函数路径,常见操作是先挂上函数过滤器,只追踪 drm 开头的函数调用,再把函数调用图打印出来:
# 挂上 function_graph 跟踪器 echo function_graph > /sys/kernel/tracing/current_tracer # 只追踪 drm_ 开头的函数 echo 'drm_*' > /sys/kernel/tracing/set_ftrace_filter # 开始记录 echo 1 > /sys/kernel/tracing/tracing_on # 触发一次显示操作,比如切分辨率、插拔屏 # 然后关闭追踪并查看输出 echo 0 > /sys/kernel/tracing/tracing_on cat /sys/kernel/tracing/trace我举一个真实的使用场景。某次遇到切分辨率后画面冻结,dmesg 里只有一句flip_done timed out,但不知道具体卡在哪个函数。用 ftrace 抓一次从用户态切分辨率到超时之间的函数调用链,发现drm_atomic_commit走到了某个驱动的 vblank 使能函数就再没有返回,而正常路径应该继续往后调用排期函数。这一下就把问题缩小到了 vblank 使能函数内部,后面在函数里逐行检查,果然是某个寄存器条件分支写反了,在特定分辨率组合下会让使能路径提前退出。
这类问题靠静态工具是看不出来的,因为状态 dump 显示 CRTC 已经 active,实际硬件中断却一直没使能,只有函数级调用链能把“程序执行有没有走到”这件事说清楚。
3.2 trace-cmd 批量采集与对比
ftrace 的命令行操作比较繁琐,复现一次问题要手动 echo 好几次,而且 ftrace 的 trace 文件只能存在内存里,量一大就会被覆盖。遇到需要长时间采集、或者要在多个路径之间做对比的场景,我更推荐用 trace-cmd 做封装。
trace-cmd 的基本用法很简单,它把启用跟踪器、设置过滤、开始记录、导出文件这几步合并了:
# 记录所有 drm_* 函数调用,保存为 trace.dat trace-cmd record -p function_graph -l 'drm_*' # 结束记录后,用 report 命令查看结果 trace-cmd reporttrace-cmd 最大的价值是可以留档。我会在“修复前”和“修复后”各抓一份 trace.dat,然后直接对比函数调用链的差异。这种对比思路在排查偶发问题时光芒万丈:因为偶发问题本身难复现,好不容易复现一次,如果没有留档,下一次不知道要等多久。抓下来的 trace 文件可以回头慢慢分析,还能发给同事,不占用现场调试时间。
另外 trace-cmd 还支持按事件而不是按函数追踪。显示子系统的某些驱动会注册 tracepoint 事件,用来记录 vblank 的触发、排队、完成等关键时刻,这让 vblank 异常排查变得非常精确。当怀疑 vblank 丢失时,我会在 trace-cmd 里同时开 tracepoint 和函数追踪,把“事件”和“函数调用”放在同一条时间线上对齐看,能看出中断到底是在哪个环节丢的。
3.3 mmiotrace 与 devmem:寄存器级别的最后一公里
函数追踪已经把代码执行路径摸清了,但有些问题的根源是代码执行了、寄存器没写对。这时候就需要寄存器级工具上场。显示控制器是一大块 MMIO 寄存器空间,前面的工具能看到内核态逻辑,能不能看到最终下发到硬件的值,就是寄存器工具的本职工作。
devmem 是最直接的手段,用来在运行时读写下地址上的物理寄存器:
# 读取物理地址 0x1234 处的 32 位值 devmem 0x1234 32 # 写入一个值 devmem 0x1234 32 0x1f它的用途分两类:一类是“看硬件到底接收了什么”,比如怀疑驱动计算出的分辨率寄存器不对,直接读相关寄存器比重新编译加打印快得多;另一类是“绕过驱动直接操作硬件做二分法”,比如确认某路时钟是否输出,直接在寄存器层面操作,如果硬件动了,说明驱动的寄存器写值有问题,如果硬件没反应,可能是时钟源或电源域的问题。
mmiotrace 则是把驱动执行过程中的所有 MMIO 读写操作完整记录下来,适合追踪“驱动在某个函数里写了哪些寄存器、值是什么”。它的使用门槛比 devmem 高一些,因为输出量非常大,但定位驱动初始化顺序问题时有奇效。有次排查屏参加载异常,驱动逻辑看了几遍找不到问题,mmiotrace 一抓,发现初始化函数里先写了一组寄存器,后面又被另一个函数用旧值覆盖回去,两种工具配合半天就锁定了根因。
这里放一个三种追踪工具的对比,方便根据场景选择:
| 工具 | 追踪粒度 | 适用场景 | 主要代价 |
|---|---|---|---|
| ftrace | 内核函数调用 | 定位代码执行路径、确认分支是否走到 | 输出量大,需要过滤 |
| trace-cmd | 函数+事件 | 长时间采集、修复前后对比、偶发问题留档 | 需要独立工具,学习成本稍高 |
| mmiotrace | MMIO 读写序列 | 驱动写寄存器顺序、值冲突、初始化流程 | 输出巨大,不适合长时间开 |
4. 自动化验证工具:把“肉眼观察”换成可复现的测试
显示调试最怕的就是“看起来好像OK了”。肉眼观察在时间上不可靠、在精度上不够细、在复现性上为零。我每次改完驱动,都会跑一遍自动化验证工具,让机器来回答“画面到底对不对”,而不是靠我自己盯屏幕。
4.1 modetest 的模式切换与 vblank 验证
modetest 不只是查询工具,它还能直接做模式设置和 vblank 测试,常用在修改驱动后的第一轮功能验证。手动切换到某个分辨率,确认屏幕能点亮并且稳定显示:
# 指定 connector 和模式进行切换 modetest -s 42:1920x1080@60这里的 42 是 connector id,后面的 1920x1080@60 是目标模式。用这个命令可以快速测试:驱动里新增的每一种 mode 是否都能正常点亮。如果某个 mode 一切换就黑屏,问题基本就锁定在时序参数或带宽配置上,跟模式列表本身有没有这个模式无关。
modetest 还带 vblank 测试模式,可以在一定时间内持续监测 vblank 事件是否按期望频率发生,并统计间隔。这个对排查“画面偶尔卡一下”的问题很有用:卡顿时如果 vblank 间隔出现明显跳变,说明 vblank 中断本身不稳定;如果 vblank 始终稳定,那问题可能出在上层 buffer 提交,跟显示控制器关系不大。这个区分很关键,它能把一半的假象问题直接排除。
4.2 IGT-gpu-tools 的核心用例选择
IGT(Intel Graphics Test)虽然最初是某厂商 GPU 的测试套件,但它在 DRM/KMS 框架通用性上做得非常好,很多子测试在其他显示驱动上也能跑。IGT 的价值不只是“能跑”,而是它把很多容易出错的操作模式封装成了标准测试,比如持续 flip、DPMS 切换、热插拔、原子提交竞争等,这些恰恰是显示驱动最容易出问题的部分。
我日常最常用的是kms_flip和kms_atomic两个测试集。kms_flip 会长时间反复切换 buffer、验证 vblank 事件和 flip 时序,对排查撕裂、冻结、flip 超时这类稳定性问题非常有效。kms_atomic 则覆盖了大量原子提交的路径,会执行各种合法和非法的状态切换,用来验证驱动在处理非预期模式时不会挂掉。
跑 IGT 不要一股脑全量跑,那是压测阶段才做的事情。我通常在功能修改后先挑子测试跑,比如:
# 跑 flip 验证的核心子测试,stress 一万次 flip ./tests/kms_flip --run-subtest basic-flip-vs-wf # 跑原子提交的状态切换测试 ./tests/kms_atomic --run-subtest plane-overlap一口气把相关子测试跑过,比手动操作一百次更可靠。IGT 的测试用例选择有个原则:改时序相关代码,重点跑 mode 切换、panel fitting、vblank 相关测试;改 buffer 管理代码,重点跑 flip、atomic、plane 相关测试;改连接检测代码,重点跑 hotplug、dpms 相关测试。
4.3 PIPE CRC:用哈希值判断画面是否真的变了
很多画面问题是肉眼难以分辨的,比如某一条颜色通道在特定灰度下异常、一个像素的偏移、某个窗口的帧内容错乱。这时候最朴素的验证手段就是 CRC 校验,IGT 里提供了一套很好的 PIPE CRC 机制。
PIPE CRC 是显示控制器硬件上的一种能力,它对当前画面内容实时计算一个哈希值。同一个画面内容对应同一个 CRC,画面内容任何像素变化,CRC 都会跟着变。这意味着可以“用指纹识别画面”,而不需要每次都用眼睛看。
典型的用法是:先用测试工具抓取某个 pipe 的 CRC 值作为基准,然后让被测对象改变画面内容,再抓一次 CRC,对比两次结果是否与预期一致。比如验证某个 plane 的 alpha 混合是否生效,设置前后 CRC 应该变化;验证某种抖动算法是否影响画质,不同配置下 CRC 应该相同或特定范围内变化。把视觉问题转化为数值问题之后,自动化就顺理成章了。
我用 PIPE CRC 排过一次很有意思的问题:一个颜色渐变出现轻微断层,肉眼看很费劲,但 CRC 一对比,发现某个逻辑 block 的灰度映射表低两位读取错位,导致灰度级不足。这种问题靠 dmesg 和 ftrace 都查不出来,因为内核日志里完全没有报错,只能靠内容校验把“画面内容差异”量化。
5. 案例复盘:一次间歇性黑屏从现象到根因的完整过程
前面讲了工具,这一节我把它们串起来用一次。这个案例来自某次嵌入式平台的显示驱动调试,问题不算罕见,但完整展现了工具组合的排查思路,值得细说。
5.1 故障现象与最初误判
平台换了新的显示主控之后,外接 HDMI 屏出现间歇性黑屏:正常显示几十秒到几分钟不等,然后画面突然黑掉,约一秒后自动恢复。恢复后显示正常,但过一段时间又复现。dmesg 里没有任何报错,没有 FIFO underrun,没有 EDID 解析错误,也没有明显的 atomic 提交失败。
最初的判断倾向于“屏的问题”或者“线材问题”,因为偶发、无报错、自动恢复这几个特征太像信号传输干扰了。换了屏、换了几条线之后故障依旧,才确认问题还是出在驱动或者硬件配置层面。这个误判过程其实是很多显示排查的通病:看到偶发问题先怀疑物理层,但物理层问题通常伴随着 dmesg 里的信号质量或链路相关打印,完全没有的话,更应该在显示链路本身找原因。
5.2 工具链定位过程
重新开始排查,先开 drm.debug 做静态观察。考虑到是偶发问题,我没有开全量日志,而是开了 KMS 和 STATE 的位,日志量可控。结果还是没发现明显报错,但在黑屏瞬间的日志里,vblank 事件打印出现了明显间隔——正常的 vblank 事件是每秒固定的次数,黑屏时有一段缺失,恢复后事件又重新出现。这告诉我画面冻结期间,vblank 中断确实停过一阵。
顺着 vblank 缺失的方向,我用 modetest 的 vblank 测试模式做持续观察,确认黑屏时 vblank 计数的停滞和画面黑屏是同步的。接下来需要弄清楚 vblank 为什么停。用 ftrace 跟踪 drm 路径,在黑屏发生时抓取函数调用链,发现 vblank 停止前的最后一个关键调用是 runtime PM 的电源切换路径,随后 vblank 使能函数被调用,但调用链在此处被截断——后续的硬件使能步骤没有执行。
到了这一步,问题基本锁定:驱动在运行时电源状态切换过程中,关闭了显示控制器的 vblank 中断,恢复时没有完整重新使能。为了确认不是寄存器层面的偶发写入错误,用 devmem 读取了 vblank 使能相关的寄存器,在故障发生时发现该寄存器的使能位确实处于关闭状态,而正常显示状态下该位是打开的。寄存器和函数调用链两条证据对上了,根因确认。
5.3 根因确认与修复
根因是电源状态恢复函数里,vblank 中断的重新使能逻辑被放到了条件分支之内,只在特定状态组合下才会执行;而大部分正常路径下这个分支不满足,导致中断一直没有重新打开。fence 机制依赖 vblank 来推进画面提交,vblank 停了,提交也就堵住了,最终表现为画面冻结。
修复方式很简单:把重新使能逻辑从条件分支里挪到公共路径,确保只要电源状态恢复,vblank 中断就无条件恢复。改完之后用同样的工具验证:先连续跑 modetest vblank 测试,确认计数稳定;再跑 kms_flip 的压力测试,连续长时间 flip 不再出现超时;最后让设备长时间挂机验证偶发问题不再复现。修复前后我在同一恢复场景下各抓了一份 trace,函数调用链的差异一目了然,这也算给这次排查画了句号。
5.4 复用这套思路的几条经验
复盘这次排查,有几个经验值得单独拎出来。
第一个是日志不要贪多。开全量 drm.debug 在这个场景下大概率会被大量无关日志淹没,反而只有针对性开位的日志才能在一堆信息里暴露出 vblank 事件的缺失。第二个是 vblank 状态是“偶发显示异常”的核心坐标。凡是画面冻结、闪烁、卡顿这类没有显式报错的问题,都要先确认 vblank 是好的,再往下查。第三个是函数调用链和寄存器值要交叉验证。函数调用链告诉我“代码想干什么”,寄存器值告诉我“硬件实际接收了什么”,两者一致才能确认根因,否则很可能只是表象。
工具本身都不复杂,复杂的是判断在哪个环节用哪个工具。dmesg 和 drm.debug 帮你确认“有没有异常事件”,debugfs 和 drm_info、modetest 帮你查“当前状态对不对”,ftrace 和 trace-cmd 帮你查“执行到哪里断了”,devmem 帮你查“硬件实际认了什么”,IGT 和 PIPE CRC 帮你回答“改动之后到底稳定不稳定”。把这五层串起来,显示驱动调试就不再是盯屏幕和猜代码的活,而是一套可以按步骤推进的工程流程。