做显示驱动开发,最磨人的其实不是写代码,而是排问题。硬件点亮了,屏幕没反应;时序配好了,画面撕裂;EDID读不出来,分辨率锁在640x480——这些现场,几乎每个RD都遇到过。我自己的经验是,显示驱动调试工具用得溜不溜,直接决定一次故障从出现到定位要花一个小时还是一天。这篇文章就梳理一下我这些年摸爬滚打出来的显示驱动调试工具清单和使用心得,给刚入坑的新手指条路,也跟还在到处找工具的老手对一下各自的组合拳。
这篇文章主要覆盖内核日志、drm.debug参数、modetest、IGT工具集、ftrace这几个层面,适合正在做Linux DRM/KMS驱动开发、嵌入式显示方案适配、或者遇到屏幕点不亮、分辨率异常、花屏闪烁问题的工程师参考。全文不涉及具体芯片平台,讲的方法在i.MX、Rockchip、MTK、全志这类常见方案上基本通用,只要你跑的Linux内核版本不是太老。
1. 调试工具的整体思维:先弄清要“看什么”
很多刚接触显示驱动的人有个误区,一上来就到处找工具下命令,结果日志刷了一大屏,真正有用的信息反而被淹没了。我自己的习惯是,动手之前先分清楚当前问题属于哪个层面,再决定用哪一类工具去观察。
1.1 显示驱动排障的三个维度
显示驱动可以粗暴地拆成三条链路:控制链路、数据链路、链路训练(Link Training)。
控制链路负责模式设置、上下电、背光、中断和状态机流转,典型故障是点不亮、花屏、无信号、休眠唤醒异常。这类问题通常靠dmesg、drm.debug、状态节点去查,因为控制链路的每一步都会在内核日志里留下痕迹。
数据链路负责帧缓冲、DMA搬运、扫描、格式转换、图层合成,典型故障是画面撕裂、分辨率错乱、双buffer不同步、颜色不对。这类问题光看日志不够,必须用modetest、IGT、或者跑一个实际的渲染测试来观察帧的输出情况。
链路训练说白了就是DP/HDMI这类数字接口的信号协商过程。链路训练失败会表现出闪屏、间歇性黑屏、握手失败,要抓这种问题通常得靠drm.debug里的DP日志分类,配合硬件侧的协议分析仪。软件侧能拿到的信息有限,所以更需要把内核日志这个通道用好。
我的经验是,接到一个bug单,先别急着试各种工具,先问三个问题:是控制没跑通,还是数据没送对,还是信号协商失败?这个问题想清楚了,工具选型的范围立刻缩小一半。
1.2 工具分层:应用层、内核层、硬件层
另一个容易混淆的点是工具所处的位置。我习惯把调试工具分成三层:
- 应用层工具:modetest、kmscube、igt-gpu-tools、GST/FFmpeg播放器。它们通过DRM/KMS接口发命令给内核,适合验证“用户空间发起的模式设置、提交、翻转是否正常”,也能用来做压力测试复现问题。
- 内核层工具:dmesg、/sys/kernel/debug/dri节点、ftrace、tracefs。它们直接暴露驱动内部状态、函数调用轨迹、寄存器读写的中间结果,适合定位“驱动为何这么走”。
- 硬件层工具:示波器、逻辑分析仪、协议分析仪。到这一步基本是在验证时序参数、电压、信号完整性了,软件工具帮不上忙,但软件侧给出的现象能为硬件测量指方向。
三层工具是配合关系,不是选一个就完事。我之前遇到过一例花屏问题,modetest能正常输出模式,kmscube跑起来也好像没问题,但实际显示内容在特定分辨率下偏移。最后是靠dmesg里一条EDID信息加drm.debug的时序配置,才确认是驱动在计算hfrontporch时四舍五入的差异,和物理屏幕实际的扫描范围没对齐。
所以工具分层不是让你按顺序各跑一遍,而是让你明确当前最需要的观察深度在哪一层,直接拿对应的工具出来用。
2. dmesg与drm.debug:内核日志里的第一手线索
几乎所有显示驱动排障,第一件事都是看日志。dmesg是最基础的,但它默认输出的是info及以上级别,很多调试信息藏在debug级里,不打开根本看不到。所以光会敲dmesg不够,还得会用drm.debug。
2.1 dmesg的基本使用
dmesg的用法不多,但有几个组合是我每次必用的:
# 清空历史日志,让接下来只有本次操作的输出 sudo dmesg -c # 带时间戳和层级过滤查看 sudo dmesg -T -l err,warn # 持续观察,配合触发操作 sudo dmesg -wdmesg -c这个命令用处很大。尤其是在复现问题时,先清空日志,再触发一次操作,然后dmesg看到的就全是和这次操作相关的信息,不用在一大堆开机日志里翻找。-T把时间戳转成人类可读格式,分析时序关系时候很关键。
显示驱动的错误日志通常长这样:
[drm:drm_connector_get_modes] [CONNECTOR:36:HDMI-A-1] probed modes : 0 [drm:dw_hdmi_probe] failed to get edid第一条说明连接器一个模式都没有探测到,第二条的failed to get edid就直接指向问题根源。dmesg的级别信息已经能帮我们做初步定性,但要看到更细的执行过程,就得开drm.debug了。
2.2 drm.debug 参数详解
drm.debug是DRM子系统提供的动态调试开关,通过模块参数控制。它的值是位掩码,每一位对应一类日志:
| 值 | 类别 | 输出内容 |
|---|---|---|
| 0x01 | DRIVER | 驱动自身重要操作 |
| 0x02 | KMS | KMS核心状态变化 |
| 0x04 | PRIME | 缓冲区导入导出 |
| 0x08 | ATOMIC | 原子提交流程 |
| 0x10 | VBL | 垂直回扫事件 |
| 0x20 | STATE | 状态对象详情 |
| 0x40 | LEAK | 对象泄漏追踪 |
| 0x80 | DP | DisplayPort链路训练 |
开启方式有两种:
# 方式一:内核启动参数,在bootargs里加 drm.debug=0x1f # 方式二:运行时开启,不用重启 sudo echo 0x1f > /sys/module/drm/parameters/debug我的做法是刚开始排查时先开0x1f,也就是把所有日志全打开,因为刚开始不知道问题在哪类里。定位到具体方向后,再关掉多余类别,比如只有DP问题就只留0x80,避免大量无关日志干扰分析。这一点特别重要,0x1f全开的时候日志量非常大,高分辨率下ATOMIC和VBL的日志几乎是刷屏级的,如果抓取方式不对,日志文件两分钟就能到几GB。
抓取drm.debug日志的经验做法:
# 先清空,再开启debug sudo dmesg -c sudo echo 0x1f > /sys/module/drm/parameters/debug # 触发问题复现,然后保存日志 sudo dmesg -T > /tmp/drm_debug_$(date +%Y%m%d_%H%M%S).log2.3 实操案例:EDID读取失败的日志分析
我处理过的一个实际案例是HDMI无输出。现象是主板接HDMI显示器完全没有画面,用dmesg看到的是:
[drm] Failed to get EDID for connector [drm:drm_helper_probe_single_connector_modes] [CONNECTOR:29:HDMI-A-1] probed modes : 0这个报错说明连接器一个模式都没探测到,问题基本确定在EDID读取链路。下一步开drm.debug=0x02,重新插拔HDMI,日志里出现:
[drm:drm_edid_is_zero] EDID is zero, dump看到这条日志,就基本排除了软件层面配置问题,直接怀疑硬件侧的DDC通道或者HDMI连接器焊接。后来用示波器测DDC引脚,果然发现串联电阻虚焊导致SCL电平异常。整个排查过程不到半小时,dmesg定方向,drm.debug定细节,最后交给硬件测量收尾。这就是工具链配合的价值。
3. modetest:KMS链路排查的标配工具
modetest是libdrm自带的小工具,却是我平时用得最多的一个。它的本职是枚举DRM设备的能力,顺带能做模式设置、页面翻转、测试图案输出。老驱动开发者有句话说得挺实在:你连modetest的输出都读不懂,就别谈调试显示驱动了。
3.1 modetest安装与基础用法
modetest一般随libdrm-tools或libdrm-utils包发布:
# Debian/Ubuntu sudo apt install libdrm-tools # 嵌入式buildroot # 在menuconfig里勾选libdrm → drmtest工具基本用法是:
# 查看某个DRM设备的资源概览 modetest -M imx-drm # 查看连接器、编码器、CRTC、平面的详细情况 modetest -M imx-drm -p # 指定连接器列出支持的模式 modetest -M imx-drm -c # 选择一个pipe并设置模式,输出测试画面 modetest -M imx-drm -s 32:1920x1080-M后面跟的是DRM设备名,通常可以在/sys/class/drm/下看到,比如card0对应-M card0,但更推荐用平台的驱动名,比如-M imx-drm,这样输出更清晰。
3.2 输出信息解读
modetest-p的输出分四段,分别是Encoders、Connectors、CRTCs、Planes。我重点看Connectors和CRTCs。
Connectors段的关键字段:
Connectors: id encoder status type dpms size modes 33 32 connected HDMI-A on 720x576 15 modes: name refresh (Hz) hdisp hsyncstart hsyncend htot vdisp vsyncstart vsyncend vtot 1920x1080 60.00 1920 1952 1976 2272 1080 1083 1088 1143每一行mode里面的hsyncstart/end、htot、vsyncstart/end、vtot就是通过modetest能直接看到的时序参数。跟面板规格书比对是判断时序配置是否正确的最快路径。
CRTCs段的关键字段:
CRTCs: id fb pos size 32 46 (0,0) (1920x1080)fb是framebuffer ID,pos和size代表输出区域。如果这里显示的size和连接器模式不一致,说明CRTC配置的扫描区不对,画面比例和裁剪问题通常从这能看出苗头。
Planes段的关键字段是每个plane的formats列表,查某分辨率是否支持某个像素格式时很管用。比如我要确认ARGB8888是否在plane 0上支持,直接看那行里有没有ARGB8888字符就行。
3.3 用modetest做模式设置与故障复现
modetest不只是查看工具,它还能主动发起模式设置。遇到“屏幕显示分辨率不对”这类问题,我用modetest验证系统本身能不能设出目标模式:
# 手动把某个连接器设置为指定分辨率 modetest -M imx-drm -s 33:1920x1080@AR24 # 如果失败,看返回的errno和dmesg里的KMS日志如果modetest能设置成功,说明DRM内核层面的模式设置能力是好的,问题大概率在应用层或数据层,比如Wayland合成器、weston的配置或framebuffer格式不匹配。如果modetest都设置失败,那就是内核驱动侧的问题,直接开drm.debug=0x02抓KMS日志,看是在drm_atomic_check还是mode_valid阶段被拒绝的。
我踩过一次坑:一块LVDS屏在modetest里能列出1366x768模式,但-s设置时总是报Invalid argument。开drm.debug看,是intel_panel_mode_valid阶段被拒,原因是vbios里LVDS的native mode是1360x768,和面板EDID里的1366x768不匹配,导致mode_valid回调强制拒绝了非native模式。这种问题靠眼睛看屏是不可能定位的,必须靠modetest在软件层面把链路走一遍才能暴露。
4. IGT-GPU-Tools:显示驱动回归测试的利器
如果说modetest是手电筒,那IGT就是整个工具箱。IGT-GPU-Tools是Intel发起的一套DRM测试套件,但它的覆盖面早已超出Intel,很多DRM厂商都在用它做回归测试和功能验证。它不只是跑测试,里面很多子测试可以直接拿来手动触发某个操作,比如强制做一次页面翻转、跑一次CRC校验。
4.1 IGT工具集是什么,从哪里装
IGT分成两大部分:igt-gpu-tools里的测试程序和lib/下的测试库。它做的最核心的一件事,就是把DRM的各个功能点拆成一个一个可执行用例,让驱动开发者能按需单独运行。
安装方式:
# Ubuntu/Debian sudo apt install igt-gpu-tools # 需要跑完整测试套件时,建议源码编译 git clone https://gitlab.freedesktop.org/drm/igt-gpu-tools.git嵌入式平台如果发行版里没有预编译包,通常是用交叉编译的方式自己编。编译依赖libdrm、pixman、cairo,这些在buildroot里一般都有。
4.2 常用用例与执行方式
我实际用途最多的是这么几类:
# 原子提交框架的基础验证 sudo kms_atomic # 页面翻转压力测试 sudo kms_flip --run-subtest flip-vs-modeset # 管道CRC完整性验证,检测画面内容是否被改动 sudo kms_pipe_crc_basic # 平面功能测试 sudo kms_plane # 显示相关属性测试 sudo kms_properties这些用例可以直接跑,但要注意一点:IGT的用例内部会假设某个连接器存在,所以在没有显示器的无头测试环境里很多用例会直接跳过。我建议在一开始先跑一遍kms_flip --list-subtests看哪些用例在目标平台可用,再针对性地跑。
跑IGT用例时,强烈建议接上交互命令先退出当前图形环境:
# 停止图形合成器,避免IGT测试和桌面抢占DRM master sudo systemctl stop gdm # 或 lightdm/weston这一点我疏忽过好几次,不退出桌面直接跑,测试用例会随机失败、超时、或者报Permission denied去抢master失败,结果根本分不清是驱动问题还是环境干扰。
4.3 自己写IGT子测试的入门思路
IGT不只是跑别人写好的用例,它还提供了完整的测试库。显示驱动遇到特殊问题时,写一个自己的最小用例来复现,定位效率比反复用通用工具试要高得多。
比如我想验证“特定分辨率下做10次原子翻转会不会触发状态机报错”,最简单的思路是拿kms_flip的框架改一改,或者直接写一个几十行的C程序,用libdrm接口做:
- 打开DRM设备,获取resources
- 找到目标连接器和CRTC
- 创建一个FB,set模式
- 循环调用
drmModePageFlip
这种写法在IGT库里有现成的辅助函数,比如igt_create_fb、igt_output_connector、igt_display_commit,不需要自己从零搭。我之前处理屏幕偶发撕裂问题时,就是写了个200行的IGT子测试,循环做翻页并打印每帧的CRC值,很快定位到是两层plane合成时的stride对齐问题。这个步骤的价值是把偶发问题从“看运气复现”变成了“稳定复现”。
5. ftrace与内核函数追踪:把视线沉到驱动内部
drm.debug能告诉我们内核做了什么决定,但还不够细。真到了要查“这个函数为什么没被调用”“中断为什么没触发”“某个操作的调用链是什么”的时候,得用ftrace。
5.1 ftrace的基本配置流程
ftrace是内核自带的事件追踪框架,不需要装额外软件。基本使用流程:
# 挂载tracefs sudo mount -t tracefs nodev /sys/kernel/debug/tracing # 关闭追踪,清空以前数据 echo 0 > tracing_on echo > trace # 选择function tracer,它可以跟踪内核函数调用 echo function > current_tracer # 或者选择function_graph,能看清调用层级 echo function_graph > current_tracer # 过滤只跟踪显示驱动相关函数 echo 'drm_atomic_*' > set_ftrace_filter echo 'drm_mode_*' > set_ftrace_filter # 打开追踪 echo 1 > tracing_on # 触发你的显示操作,比如modetest -s一次 # 停止追踪并读取结果 echo 0 > tracing_on cat trace > /tmp/ftrace_out.logfunction_graph是我最常用的tracer,因为它输出的是一个缩进式的调用树,能直接看到drm_atomic_commit调了哪些函数、哪个分支提前返回了、在哪一步出现了-EINVAL。这对理解驱动内部逻辑特别有用。
5.2 显示驱动场景下的追踪实战
我遇到过一个比较刁钻的问题:休眠唤醒之后屏幕不亮,但没有任何dmesg报错。这种问题最烦人,因为驱动没有报错说明流程走到了一半,只是后半段可能提前跳过了。
我用function_graph过滤了驱动核心的几个函数:
echo 'mtk_drm_crtc_atomic_resume' > set_ftrace_filter echo 'mtk_drm_atomic_commit_tail' > set_ftrace_filter echo function_graph > current_tracer echo 1 > tracing_on # 触发一次睡眠/唤醒从ftrace输出看到,mtk_drm_crtc_atomic_resume里面调了一个mtk_drm_fb_restoreDirty,返回后直接跳到return标签,跳过了后半段对clk_prepare_enable的调用。再对照代码一看,是这个函数里一个错误标志位没复位,第二次唤醒时就走进了提前返回分支。
这个问题的诡异之处在于:不报错,不代表没有问题;没走完流程,才是真正的现象。如果只看dmesg,这个bug可能永远定位不了。ftrace的价值就在这里,它让你看见“到底走没走到那里”。
5.3 和dmesg配合的方法
ftrace和dmesg配合有个技巧:ftrace里能看到流程走向,dmesg里能看到每一步的状态码。我的习惯是两边同时开,ftrace负责说“发生了什么”——函数调用顺序,dmesg负责说“结果如何”——返回值、状态码、警告信息。
实际操作中,我一般是先把dmesg里的错误时间点记下来,然后反推到ftrace日志里。比如dmesg在12:03:45.123打印了一条timedout waiting for vblank,就在ftrace里找到同一时间点的调用链,往上看是哪个函数发起了这个等待、上一级函数有没有异常。这种时间轴对齐的技巧能省很多翻日志的时间。
另外要注意,ftrace的打开会显著增加内核执行开销,特别是在function和function_graph模式下,整个系统的性能会下降一到两个数量级。所以在生产环境或对实时性敏感的场景里,不要长时间开着ftrace跑,只在需要复现问题的那几十秒内开启,复现完立刻关闭。
6. 工具组合与疑难问题实战速查
单个工具认识是第一步,怎么组合起来用才是功力所在。我做显示驱动这几年最大的体会是:没有哪个工具能一次性解决所有问题,但工具组合不合理,一定会在某个环节卡住。
6.1 常见故障场景与工具选择速查表
| 现象 | 首选工具 | 辅助手段 | 核心排查点 |
|---|---|---|---|
| 点不亮、无信号 | dmesg + drm.debug=0x02 | modetest -c | 连接器/编码器状态、EDID读取 |
| 分辨率异常 | modetest -p | drm.debug=0x08 | 模式列表、时序参数、CRTC size |
| 花屏、撕裂 | IGT kms_flip | kms_pipe_crc_basic | 翻转边界、stride对齐、合成顺序 |
| 间歇性黑屏 | dmesg + drm.debug=0x1f | ftrace function_graph | 链路握手、电源状态转换、中断丢失 |
| 休眠唤醒异常 | ftrace function_graph | dmesg -T | 恢复函数调用链、时钟/电源恢复顺序 |
| 颜色不对、通道错乱 | IGT kms_plane | 读取framebuffer dump | 像素格式、字节序、plane混色顺序 |
| EDID读取失败 | dmesg + drm.debug=0x02 | i2c工具(i2cget) | DDC通道、i2c地址、上拉电阻 |
6.2 一次全链路排查的组合流程
以“HDMI分辨率1920x1080间歇性闪屏”为例,完整的工具组合流程是:
第一步,dmesg -c清空,正常播放一段时间,dmesg看有没有报错。如果出现link training failed的类似字样,直接开drm.debug=0x80抓DP日志。
第二步,闪屏问题一般不是每次都能复现,需要加长时间观察。这时用dmesg -w持续输出,再叠加watch -n 1 cat /sys/kernel/debug/dri/0/state观察状态对象是否有异常翻转。
第三步,如果怀疑跟垂直回扫有关,开drm.debug=0x10,看VBL中断是否准时触发。如果VBL中断事件丢失或延迟,再配合ftrace过滤vblank相关函数,看中断处理链路。
第四步,如果日志层面没找到打破口,用IGT的kms_flip做压力测试,把闪屏复现变成可控操作,再结合dmesg和ftrace的输出做时间轴对齐。
这套组合流程是我处理HDMI闪屏时的标准动作,成功率很高。核心思想就是先用粗粒度日志定性,再用细粒度工具定量,最后用测试用例固化复现路径。
6.3 我常用的几个习惯与心得
最后说几个我自己的习惯,不一定适合所有人,但对效率提升确实有帮助:
第一,调试之前先确认当前DRM master在谁手里。如果图形界面还在跑,modetest和IGT用例可能拿不到设备权限,看起来像是驱动有问题,实际上是被应用层占住了。我一般先停掉桌面或合成器,再开始调试,这个动作能排除一大批假故障。
第二,日志不止要保存,还要带时间戳。dmesg -T比裸dmesg好用得多,尤其在和ftrace、IGT输出做时间对齐时,没有时间戳的日志基本是废的。
第三,做修改之后,不要急着跑完整测试,先跑一遍modetest确认基本模式设置能力没被破坏,再跑相关IGT用例。这是个最小回归思路,能避免把新bug混进旧问题里一起排查。
第四,遇到问题第一反应不要改代码,先确认“现象是什么、哪条链路断、日志说了什么”。我犯过的不少错误就是上来就改驱动参数,结果改了半天才发现是屏的参数或者线材的问题。显示驱动调试,顺序对了,一半问题已经解决了。
第五,注意保存当时的完整环境信息,包括内核版本、dts配置、面板参数、drm.debug设置。同一块屏在不同内核版本上的行为可能不一样,记录环境才能复现、才能对比。
显示驱动调试是个需要耐心和经验积累的活,工具只是帮你看清楚现场。希望这篇文章能给正在跟显示问题较劲的你提供一点有用的思路,少走几步弯路。如果你也有自己非常顺手的调试组合,欢迎在评论区交流,互相补充。