做显示驱动开发,最怕遇到的就是“屏不亮”。代码编译通过、内核起来、驱动也 probe 了,但屏幕就是一片黑,这时候你要是不熟悉调试工具,就只能对着代码干瞪眼,靠猜来定位问题,效率极低。显示驱动之所以难调,是因为问题可能出在任何一层:I2C 读不到 EDID、时序参数算错、时钟频率对不上、帧缓存地址写错、背光没亮、reset 脚时序不对……任何一个环节出问题,表现都是“黑屏”。而调试工具,就是帮你快速定位这些环节到底是哪一个出了问题。
这篇是显示驱动系列的第 5 篇,主要聊我在实际调试中离不开的几类工具:从最常用的 modetest 和 dmesg,到 debugfs、trace 等内核级工具,再到一些图形测试程序。我会结合真实调试场景,讲清楚每个工具在什么情况下用、怎么用、输出怎么看,以及对应的排查思路。内容偏实操,适合刚接触显示驱动、手里正好有块屏怎么都点不亮的朋友,也适合做了几年 BSP 想系统梳理一下调试方法的同行。
1. 显示驱动调试的整体思路——先分清“驱动没跑起来”和“时序对不对”
1.1 调试困境:黑屏不等于驱动没写对
我刚接触显示驱动的时候,遇到黑屏第一反应就是怀疑自己的驱动代码有问题,于是拼命查 probe 流程、查设备树、查寄存器配置,折腾大半天毫无进展。后来被一位老工程师点醒:显示驱动的问题要分层看,黑屏可能发生在任何一个环节,而驱动 probe 成功只是万里长征的第一步。
一次典型的“屏不亮”经历了这些环节:I2C/DDC 通道能不能读到 EDID,pinctrl 和 GPIO 的 reset/背光引脚是否配置正确,时钟树和 pixel clock 有没有按 panel 的 datasheet 配好,TCON 的时序参数比如 HBP、HFP、VBP、VFP 对不对,RGB/ LVDS / MIPI 数据通道是不是正确,最后才是 Driver IC 有没有把图像刷到玻璃上。
驱动 probe 只是表明软件发现了这个设备,不代表硬件链路就通了。所以第一条经验是:屏不亮,不要一上来就改代码,先花 10 分钟用工具确认驱动到底跑到了哪一步,内核有没有报错,设备有没有注册成功,再决定下一步往哪个方向排查。
1.2 从硬件链路到软件栈:按层次定位问题
成熟的调试思路是把显示链路分成几个层次,自下而上或者自上而下逐一确认:
- 电气层:屏供电、背光、逻辑板的电压和电流是否正常,排线有没有接好。这一层出了问题,后面所有软件调试都白搭。
- 链路层:MIPI DSI / LVDS / HDMI / DP 的物理链路是否建立,时钟和数据通道是否工作,有没有信号质量异常。
- 控制器层:SoC 内部的 display controller、TCON、DSI host 等模块是否完成初始化,时钟和中断是否正常。
- 驱动软件层:DRM/KMS 或 FBDEV 框架下,crtc、encoder、connector 是否注册成功,modeset 是否执行完成。
- 应用层:图形栈(weston、X11 或测试程序)是否真的往 framebuffer 里写了内容,格式是否匹配,扫描出的图像是否正确。
显示驱动调试工具的作用,就是帮助你快速建立“问题出在第几层”的判断。工具用的熟练,你甚至不需要示波器就能排除一半以上的软件问题,再把硬件疑点缩小到一个很小的范围。我自己的习惯是先用 dmesg 和 modetest 做一轮扫描,基本能确定 80% 的问题方向,剩下 20% 再用 trace 和 debugfs 深入细节。
2. 必备工具清单与核心用法
2.1 modetest:DRM/KMS 调试的瑞士军刀
modetest 是 libdrm 提供的一个命令行测试工具,也是显示驱动开发中使用频率最高的工具,没有之一。它的核心功能是枚举 DRM 设备的所有资源:connector、encoder、crtc、plane,以及每个 connector 支持的显示模式,还可以指定 mode 去做一次模式设置并测试画面输出。
用法上,最常用的是这几个参数组合:
# 列出设备支持的全部资源 modetest -M xxx -p # 查看某个 connector 的详情,包括 EDID、modes、link status modetest -M xxx -c # 在指定 connector 上设置指定 mode 并输出测试画面 modetest -M xxx -s 42:1920x1080@60其中 -M 指定 DRM 主设备号,比如 rockchip 平台常见的是 -M rockchip,或者用 -Mcard0。不加 -M 的话,modetest 会自动选择第一个 DRM 设备,但如果你板子上有多个显示控制器,最好明确指定。
-modetest -p 的输出非常关键,它会显示当前所有 connector、encoder、crtc 的状态。比如某个 connector 的状态是 “connected”,说明驱动读到 EDID 或者通过其他方式检测到了屏幕。如果显示 “disconnected”,说明链路检测失败,要么是屏没接好,要么是检测逻辑有问题。如果你确认屏幕物理连接正常但状态一直是 disconnected,那问题大概率在 HPD 检测或者 EDID 读取这条链路上。
还有一个容易被忽略但非常实用的参数:modetest -s 可以把画面切到测试模式,比如纯色、彩条。做显示驱动调试的时候,能切出纯色画面本身就是很大的进展。我调试 MIPI DSI 屏时,probe 正常但屏幕一直白屏,用 modetest -s 切了一个红色画面,结果屏幕真的显示了红色,这说明链路和控制器的基本功能没问题,问题出在后续的应用层或者 framebuffer 内容上。
2.2 dmesg 与日志分级:先看内核说了什么
dmesg 是排查任何内核驱动问题的第一站,显示驱动也一样。驱动在 probe、modeset、热插拔事件、EDID 解析等关键路径上通常都会打印日志,这些日志能告诉你两个重要信息:程序走到了哪一步,有没有报错。
我调试时习惯把 dmesg 的显示相关日志单独过滤出来看:
dmesg | grep -i "drm\|display\|dsi\|hdmi\|edid\|panel\|backlight"不过这只是一个初步过滤,实际的日志量可能很大,而且很多有用的信息藏在 dev_dbg 里,默认情况下根本不会打印。所以驱动的调试开关就很重要。比如使用内核动态调试:
# 打开某个文件的全部调试输出 echo 'file drivers/gpu/drm/xxx.c +p' > /sys/kernel/debug/dynamic_debug/control # 打开某个函数 echo 'func xxx_probe +p' > /sys/kernel/debug/dynamic_debug/control开了动态调试之后,你能看到驱动在 probe 过程中每个关键步骤的记录:读了哪些寄存器的值、计算出的 pixel clock 是多少、跟 panel 的时序怎么对齐的、哪个 check 失败了。这些信息在源码注释里都不会写,但对排查具体问题非常有用。
显示驱动开发中有一种典型情况:驱动 probe 成功,modetest 也列出了模式,但一旦切 mode 就出现花屏或者闪屏。这时候 dmesg 里往往只有一行“mode set failed”或者直接什么都没有。如果你碰上这种情况,一定要把动态调试打开重跑一遍,大部分问题会在这个过程中现出原形。
2.3 debugfs 与 trace:深入内核路径
dmesg 能看到的问题大多是“硬错误”——probe 失败、寄存器读写失败、超时等。但显示驱动里很多问题是软性的,比如时序参数就差那么一点、帧率偏低、显示有撕裂感,这些用 dmesg 基本看不出来。这时需要借助 debugfs 和 trace。
DRM 框架本身在 debugfs 上提供了很多接口,路径一般在 /sys/kernel/debug/dri/0/ 下面。不同平台可能略有区别,但常见的节点包括:
- framebuffer:列出所有 framebuffer 的对象信息
- connectors:connector 和 encoder 的状态
- edid_override:覆盖 EDID 数据
- clients:当前打开 DRM 节点的进程
- gem_names / gem_buffers:GEM 缓冲区映射情况
我最常用的是查看 connector 的 EDID 缓存和 link rate。比如 MIPI DSI 屏读 EDID 失败的时候,可以通过 debugfs 确认驱动到底读到了什么,是“全 F”(0xFF 0xFF ...)还是部分数据错误。如果是 HDMI 屏,debugfs 里能看到 link rate、lane count、 scrambling 状态,对排查高清分辨率下闪烁的问题特别有帮助。
trace 则适合追踪代码执行路径。显示驱动的 probe 和 modeset 流程可能会跨越多个驱动文件,函数调用链很长,单靠看代码很难理清。用 ftrace 可以方便地追踪关键函数:
# 设置追踪函数并开启 echo 'dev_driver_connected' > /sys/kernel/debug/tracing/set_ftrace_filter echo function_graph > /sys/kernel/debug/tracing/current_tracer echo 1 > /sys/kernel/debug/tracing/tracing_on追踪结果会显示函数被调用时的完整调用栈和耗时。比如你怀疑 modeset 过程中某个操作卡了很久,ftrace 能精确到哪个函数占了多少毫秒。这种数据比“看起来好像卡住了”靠谱得多。
3. 实操流程:一个“屏不亮”问题的完整排查过程
3.1 环境准备:确认内核配置和工具链
调试开始前,先确认你的内核镜像里已经打开了对应的 DRM 配置。如果内核没打开 DRM 框架,modetest 连设备都找不到,后面什么都别谈。常见的内核配置项包括 CONFIG_DRM、CONFIG_DRM_XXX(对应具体平台)、CONFIG_FB、CONFIG_DRM_LOAD_EDID_FIRMWARE 等。
确认配置之后,确保板子上有 modetest 可执行文件。有些 buildroot / yocto 镜像默认没有带 modetest,需要用 libdrm 编译出来之后单独打包进去。如果没有现成的,也可以用目标平台的交叉编译工具编译一下,编译命令比较简单:
meson setup build -Dtests=true ninja -C build modetest编译出的二进制是静态链接的,拷贝到板子上直接就能跑,不会出现运行时缺库的问题。
硬件连接方面也做一个快速确认:屏幕的电源、背光、I2C 引脚、复位引脚、数据通道跟 SoC 侧有没有一一对应,尤其是 MIPI DSI 屏,lane 的顺序接错会导致完全无显示。我遇到过不少次,设备树里 lane-mapping 没有跟着硬件实际连接改,导致屏幕只能点亮背光但没有任何图像。
3.2 第一步:用 modetest 确认基本状态
拿到一块点不亮的屏,我第一步基本就是跑 modetest -p。这一步会直接告诉你系统的 DRM 链路搭起来没有:
modetest -M xxx -p输出中重点看这几行:
- Connector 的状态:connected / disconnected
- 当前 mode,以及支持的 mode 列表
- Encoder 和 crtc 的绑定关系
- Plane 的数量和格式支持
如果 modetest 输出里根本没有这个 connector,说明设备树里 panel 节点没有被正确解析,驱动 probe 可能失败了。这时用 dmesg 检查 probe 阶段的报错,重点看 panel 的 compatible 是否匹配、reset 引脚号是否正确、电源控制是否成功。
如果 connector 状态是 disconnected,但物理上是接了屏的,那么大概率是 HPD 检测或 EDID 读取有问题。此时可以用 modetest -c 查看 connector 详情,看驱动是否尝试读取 EDID、读到了什么数据。有时 EDID 校验失败是因为 I2C 地址错误,有些屏是 0x50,有些是 0x36(MIPI DSI 的 DCS 读 EDID),设备树里对应关系搞错了就会出现这种情况。
如果 connector 已经 connected,但你发现 mode 列表里找不到想要的分辨率,那就是 EDID 解析或 panel 的 fixed mode 配置问题。这时候继续用 modetest -s 指定一个相近的分辨率试试,看能否点亮,如果点了有画面但比例不对,问题多半在 mode 的 clock 和时序参数上。
3.3 第二步:检查 crtc / encoder / connector 链路
modetest -p 确认基础状态之后,就要看管线的连通性了。DRM 的 modeset 过程本质上是把 pipeline 里的 crtc、encoder、connector 全部连接起来。任一环断了,屏幕都不会输出。
有一次我调试 LVDS 屏,modetest -p 显示 connector 是 connected 的,encoder 也存在,但 crtc 一直没绑上,当时看代码怎么都发现不了问题。后来用 modetest -s 主动做一次 mode set 才发现,驱动里 crtc 的 possible_clones 配置有误,导致 crtc 和 encoder 的匹配失败,modeset 被拒绝。这个过程让我记住了:看 DBG 输出不如直接做一次 mode set 更快。如果 mode set 执行成功,说明 pipeline 的连接关系没问题;如果失败,报错信息会直接告诉你是哪一环连不上。
实际执行:
modetest -M xxx -s 42:1024x768@60其中 42 是 connector 的 ID,1024x768@60 是目标模式。执行成功后屏幕应该会切到对应的测试画面。如果你用的是直接带屏幕的板子,应该能看到彩条测试图;如果只看得到背光但没有图像,问题就缩小到了 controller 到 panel 之间的传输链路。
这一步还有一个变通的办法:加 -v 参数让 mode set 强制跳过一些检验,有些平台上 crtc 的空闲状态没正确更新会导致 mode set 失败,用 -v 能绕过去但并不能解决根本问题。我个人的建议是 mode set 失败不要急着 —v 绕过,先搞清失败的原因。
3.4 第三步:验证背光链路和帧缓冲内容
如果 mode set 成功但屏幕还是黑的,就得分别验证背光和 framebuffer 内容。
背光链路通常比较简单,通过 sysfs 节点就能直接控制:
# 查看当前背光亮度 cat /sys/class/backlight/xxx/brightness # 设置最大亮度 echo 255 > /sys/class/backlight/xxx/brightness如果背光节点不存在,说明 backlight 驱动没有正确绑定,检查设备树中 backlight 节点的 compatible 和 pwm 通道配置。如果背光能亮但屏幕一直是白的,那问题在 LCD 驱动和 TCON 的配置上,重点看像素格式和扫描方向。
背光链路确认之后,再看 framebuffer 有没有数据。有些平台在启动过程中根本没有分配 framebuffer,也没有往里面写内容,这时用 modetest 虽然切换了模式,但屏幕上没有测试图像。正确的做法是再跑一遍带 pattern 输出的模式设置,或者用 weston 启动一个画面。你手动往 /dev/fb0 写数据也是可行的,不过要小心格式和分辨率匹配:
# 生成一张纯色图像然后直接写入 framebuffer dd if=/dev/zero of=/dev/fb0 bs=1024 count=768这样 framebuffer 就全黑了,屏幕如果是好的会立刻变黑,而不是保持之前的亮屏状态。如果你看到屏幕响应了“变黑”这个动作,说明链路基本没问题,不响应才是链路问题。
4. 常见问题速查表与避坑指南
4.1 高频问题场景对照表
调试工具用熟了之后,问题定位会快很多。我把工作中最常见的显示驱动问题和对应的排查方法整理成一个表格,方便对照排查。
| 现象 | 可能原因 | 首选排查工具 | 备注 |
|---|---|---|---|
| 屏完全不亮 | 供电/背光/复位异常 | dmesg + sysfs 背光节点 | 先从硬件和 GPIO 确认 |
| 背光亮但无图像 | 数据 lane 没通或 TCON 配置错误 | modetest -s | 看 mode set 是否成功 |
| connector 显示 disconnected | HPD 或 EDID 读取问题 | modetest -c | 检查 I2C 地址和 HPD GPIO |
| modetest 找不到 connector | panel 设备树节点解析失败 | dmesg grep panel | 检查 compatible 和复位 GPIO |
| mode set 报错 | crtc/encoder 不匹配 | 看 dmesg + modetest -v | 检查 possible_crtcs 配置 |
| 画面花屏 | 时序参数 HBP/HFP 错误 | 对比 datasheet + dmesg | 大多数是 clock 和 porch 配置 |
| 画面偏移 | 扫描参数错误 | 对比时序表格 | HSW / VSW 设置不匹配 |
| 闪屏 | backlight 频率低或 blank/unblank 流程异常 | sysfs + ftrace | 注意 pwm 频率 |
| 高分辨率闪烁 | 信号质量差或 link rate 错误 | debugfs 查看 link 状态 | HDMI/DP 时常见 |
| 屏幕亮度不均匀 | PWM 频率或占空比设置 | 示波器测 PWM | 软件层面调 duty |
使用表格的时候要记住一点:同一现象可能是多个原因叠加的结果。我遇到过屏幕花屏,最开始以为是时序问题,调了很久后才发现是设备树里 dsi lane 数配错了,数据只有一半传到屏上。所以不要只看一个原因,多个工具交叉验证才是排查的正道。
4.2 容易踩坑的细节
调试显示驱动,有几个细节特别容易坑人,这里单独拿出来说:
第一个是 EDID 读取失败别急着怀疑 I2C 总线。很多开发板为了节省 GPIO,把 HPD 和 I2C 分时复用,如果 HPD 状态没处理好,I2C 总线会被屏端拉死,读 EDID 永远是超时。这种情况我在某 ARM 平台上遇到过一次,折腾了一整天,后来用示波器一看 I2C 时钟和数据线的电平状态就明白了。所以遇到 EDID 读取问题,先量一下 I2C 有没有波形——如果 SDA 一直被拉低,问题多半不在驱动而在物理层。
第二个是 mode 参数要和屏的 datasheet 严格对应。有些屏对 HBP/HFP 的最小值有要求,时序参数差个十几像素就有可能出现右边多出一条竖线或者画面轻微抖动。很多驱动里为了兼容多款屏,把时序写成了“通用”值,结果每一款屏都有一点小毛病。我的建议是做项目适配的时候每个屏都要单独验证一遍时序,不要想当然。
第三个是 debugfs 接口名称有平台差异。不是所有内核版本的 debugfs 节点都叫一样的名字,比如有些平台的 EDID 节点在 /sys/kernel/debug/dri/0/xxx-edid 下,有些在 /sys/kernel/debug/xxx/ 下。写脚本的时候先确认节点的准确路径,否则脚本在不同的内核版本上可能完全失效。
第四个是 trace 开启后要记得关闭,否则系统性能会下降。有一次我开着 function_graph 忘了关,跑图形测试时帧率掉了 30%,完全查不到原因,后来才发现是 ftrace 一直在后台记录。调试结束之后务必执行:
echo nop > /sys/kernel/debug/tracing/current_tracer这些小细节本身不是很高深的知识,但踩一次就记忆深刻。
5. 工具之外的调试经验
工具再全,也只是辅助手段,调试显示驱动真正靠的还是对链路的理解。我对刚入行的朋友有一个建议:在实验室里准备一块“标准屏”——型号固定、时序明确、工作状态完全正常的屏,所有的新驱动先在这个屏上验证,过了之后再接到目标屏上调试。这样能最大限度地把“屏的兼容性”和“驱动的正确性”两个变量分开,遇到问题不至于混在一起。
还有一个小技巧是我自己一直在用的:修改驱动参数的时候,每次只改一个变量。比如调时序,先只动 HBP,验证效果,再动 HFP。一次改多个参数,很容易陷入一种“改了但又不知道哪个生效”的状态里,浪费大量时间。调试显示驱动本身就是一件非常需要耐心的事情,保持一个变量的改动节奏,你会发现很多问题其实没那么复杂。
如果工具输出和代码逻辑看起来都对,但画面始终不对,回去看看硬件连接一定是对的。我见过太多问题,最后发现就是排线松了,或者屏端芯片虚焊。遇到界面怎么调都调不好,回到硬件层面用万用表量一下,可能五秒钟就把问题找到了。工具是帮我们缩小范围,真正的定位还是靠经验和逻辑把问题一步步逼到源头。