1. 显示驱动调试这件事,到底在调什么
做显示驱动开发的人都有一个共同体会:代码写完了只是开始,真正折磨人的是调不通、花屏、闪屏、分辨率不对、旋转方向反了、多屏不同步这些破事。你对着内核日志一行行翻,寄存器手册翻到卷边,最后发现问题可能只是某个时序参数差了2,或者某个plane的格式没对上。这时候,一套趁手的调试工具就是救命稻草。
这篇内容围绕显示驱动调试工具展开,重点聊DRM子系统的调试手段、modetest这类核心工具的用法、Android环境下的特殊调试路径,以及我在实际项目中踩过的坑和总结出来的排查套路。不管你是刚接触显示驱动的新手,还是已经调过几块屏的老手,这里面的工具组合和排查思路都能直接拿去用。
显示驱动调试的核心诉求其实就几个:确认硬件是否正常工作、验证驱动是否正确配置、定位问题出在哪一层。听起来简单,但实际调试中,问题可能出在硬件连接、时序参数、驱动逻辑、框架层配置、甚至应用层的buffer格式上。工具的作用就是帮你快速缩小范围,而不是靠猜。
我见过太多人一上来就改代码,改了半天发现是硬件排线松了。也见过有人对着屏幕发呆,不知道从哪下手。所以这篇内容会从工具的角度切入,把整个调试链路串起来,让你知道什么阶段该用什么工具、怎么看输出、怎么判断问题归属。
2. DRM子系统调试工具全景梳理
2.1 为什么DRM是显示驱动的核心战场
Linux显示驱动的主流框架就是DRM(Direct Rendering Manager)。它管理GPU、显示控制器、显示输出之间的数据流,负责framebuffer的分配、plane的合成、CRTC的时序控制、encoder和connector的链路管理。你可以把它理解成一个交通枢纽:应用层要把画面显示出来,得经过DRM的层层调度,最后从物理接口输出到屏幕。
DRM的架构分层很清晰:Framebuffer层管buffer,Plane层管图层合成,CRTC层管扫描时序,Encoder层管信号编码,Connector层管物理接口。调试的时候,你得先搞清楚问题出在哪一层,才能选对工具。比如屏幕完全不亮,可能是CRTC没输出时序;画面花屏,可能是Plane格式不对;分辨率识别错误,可能是Connector的EDID读取有问题。
Android在Linux内核之上跑,显示部分同样走DRM框架,但上面套了一层SurfaceFlinger和HWC(Hardware Composer)。所以Android的显示调试比纯Linux环境更复杂,你既要看内核层的DRM状态,也要看框架层的合成策略。
2.2 modetest:DRM调试的瑞士军刀
modetest是libdrm自带的一个测试工具,可以说是DRM调试的入门必备。它能列出当前系统所有的DRM设备、CRTC、Encoder、Connector、Plane信息,还能直接设置显示模式、测试画面输出。
最基本的用法:
# 列出所有DRM设备的信息 modetest -M rockchip -c # 列出connector信息 modetest -M rockchip -c # 列出CRTC信息 modetest -M rockchip -p # 列出plane信息 modetest -M rockchip -P # 在指定connector上显示测试画面 modetest -M rockchip -s 42@35:1920x1080这里的-M指定DRM驱动名称,不同平台不一样,Rockchip平台是rockchip,Allwinner平台可能是sun4i-drm,Intel平台是i915。-s参数后面的格式是connector_id@crtc_id:分辨率。
modetest最实用的场景是:当你怀疑驱动没正确初始化显示链路时,直接用modetest绕过上层框架,手动设置一个模式看看屏幕能不能亮。如果modetest能正常显示,说明内核DRM驱动基本没问题,问题在上层;如果modetest也不行,那就要往内核层查。
注意:modetest需要root权限运行,而且在使用前最好先停掉上层显示服务,否则会有资源冲突。在Android环境下,需要先停掉SurfaceFlinger。
2.3 内核debugfs:DRM状态的透视镜
debugfs是内核提供的一个调试文件系统,DRM子系统在里面暴露了大量有用的信息。挂载debugfs:
mount -t debugfs none /sys/kernel/debug然后进入DRM目录:
cd /sys/kernel/debug/dri/0 ls你会看到一堆文件,常用的有:
state:当前DRM的完整状态,包括所有CRTC、Plane、Connector的配置framebuffer:当前注册的framebuffer列表gem_names:GEM buffer的名字映射clients:当前打开DRM设备的客户端mm_dump:显存使用情况
state文件是我用得最多的,它能把当前显示管线的完整状态dump出来。比如你怀疑某个plane没使能,直接看state里对应plane的crtc_id是不是0,如果是0就说明没绑定到任何CRTC。
不同平台的DRM驱动还会在debugfs里加自己的调试节点。比如Rockchip平台会有/sys/kernel/debug/dri/0/summary,能直接看到VOP(Video Output Processor)的寄存器状态和当前配置。这些平台特有的节点往往比通用节点更有用,建议拿到平台后先翻一遍debugfs里有什么。
2.4 drm_info:快速查看DRM能力
drm_info是一个用户态工具,能以更友好的格式输出DRM设备的能力信息。它比modetest的输出更结构化,适合快速了解一个陌生平台的显示能力。
drm_info输出会包含所有CRTC、Connector、Encoder、Plane的详细属性,包括支持的格式、支持的缩放能力、支持的旋转角度等。当你需要确认某个plane支不支持某种像素格式时,drm_info比翻代码快得多。
2.5 内核日志与ftrace:追踪驱动执行流
dmesg是最基础的调试手段,但很多人只看错误级别,忽略了info级别的输出。DRM驱动在初始化阶段会打印大量信息,包括识别到的Connector类型、EDID读取结果、支持的显示模式列表等。
dmesg | grep -i drm dmesg | grep -i vop dmesg | grep -i edid如果dmesg信息不够,可以用ftrace追踪DRM内部的函数调用:
cd /sys/kernel/debug/tracing echo function > current_tracer echo drm_atomic_commit > set_ftrace_filter echo 1 > tracing_on # 触发一次显示操作 echo 0 > tracing_on cat traceftrace能帮你确认某个函数有没有被调用、调用顺序对不对、返回值是什么。比如你怀疑atomic commit失败了,用ftrace追一下drm_atomic_commit的调用链,看看是在哪个环节返回了错误。
3. Android环境下的显示调试特殊路径
3.1 SurfaceFlinger与HWC的调试接口
Android的显示路径是:App → SurfaceFlinger → HWC → DRM。所以调试Android显示问题,不能只看内核DRM,还要看SurfaceFlinger和HWC的状态。
SurfaceFlinger提供了dumpsys SurfaceFlinger命令,能输出大量有用信息:
adb shell dumpsys SurfaceFlinger输出内容包括:当前所有图层的列表、每个图层的buffer格式和尺寸、合成方式(GPU合成还是HWC合成)、显示设备的配置等。如果你遇到画面撕裂、图层叠加异常、合成效率低等问题,这个命令是首选。
更精细的调试可以用:
# 查看显示设备信息 adb shell dumpsys SurfaceFlinger --display-id # 查看图层信息 adb shell dumpsys SurfaceFlinger --list # 查看HWC信息 adb shell dumpsys SurfaceFlinger --hwcHWC的调试信息通常在dumpsys SurfaceFlinger的输出末尾,会列出每个显示设备的HWC能力、当前使用的合成策略、每个图层的合成方式。
3.2 Android DRM调试的权限问题
Android环境下访问DRM设备节点需要root权限。/dev/dri/card0默认只有system用户可读写。如果你在userdebug版本上调试,可以先adb root,然后adb shell进去操作。
但要注意,Android的SurfaceFlinger会一直占用DRM设备,你直接跑modetest会报"Device or resource busy"。解决办法是先停掉SurfaceFlinger:
adb shell stop adb shell modetest -M rockchip -c调试完了再adb shell start恢复。这个操作在开发阶段很常用,但切记不要在量产版本上这么干。
3.3 通过sysfs查看显示状态
Android的sysfs里也有一些显示相关的节点,比如:
# 查看HDMI连接状态 cat /sys/class/drm/card0-HDMI-A-1/status # 查看当前分辨率 cat /sys/class/drm/card0-HDMI-A-1/modes # 查看EDID cat /sys/class/drm/card0-HDMI-A-1/edid | hexdump -C这些节点在调试HDMI热插拔、分辨率协商问题时特别有用。比如HDMI插上后屏幕不亮,先看status是不是"connected",如果是"disconnected"说明HPD信号没检测到,问题在硬件或HPD驱动。
3.4 Android特有的显示调试命令
除了标准的DRM工具,Android还有一些特有的调试命令:
# 查看显示相关服务状态 adb shell dumpsys display # 查看窗口管理器中的显示信息 adb shell dumpsys window displays # 强制设置分辨率(需要root) adb shell wm size 1920x1080 # 强制设置密度 adb shell wm density 240 # 查看当前显示帧率 adb shell dumpsys SurfaceFlinger --latencydumpsys display能输出当前显示设备的详细配置,包括逻辑分辨率、物理分辨率、刷新率、旋转角度等。当你遇到应用显示异常但内核DRM状态正常时,这个命令能帮你确认框架层的配置。
4. 实操:从零开始调试一块新屏
4.1 硬件确认与基础检查
拿到一块新屏,第一步不是改代码,而是确认硬件连接。具体检查项:
- 排线是否插紧,方向是否正确(FPC排线插反是新手常犯的错误)
- 背光是否正常(用万用表测背光供电,或者用手电筒照屏幕看有没有隐约画面)
- 供电电压是否匹配(3.3V还是1.8V,搞错了可能烧屏)
- 复位信号和使能信号是否正常(用示波器看时序)
这些基础检查能排除掉大部分"屏幕完全不亮"的问题。我见过一个案例,调了一整天驱动,最后发现是排线插反了。
4.2 设备树配置与驱动匹配
硬件确认没问题后,接下来是设备树配置。以Rockchip平台为例,需要在设备树里配置VOP、MIPI DSI、Panel等节点:
&dsi0 { status = "okay"; panel@0 { compatible = "simple-panel"; reg = <0>; backlight = <&backlight>; reset-gpios = <&gpio1 10 GPIO_ACTIVE_LOW>; enable-gpios = <&gpio1 11 GPIO_ACTIVE_HIGH>; dsi,flags = <(MIPI_DSI_MODE_VIDEO | MIPI_DSI_MODE_VIDEO_BURST)>; dsi,format = <MIPI_DSI_FMT_RGB888>; dsi,lanes = <4>; display-timings { native-mode = <&timing0>; timing0: timing0 { clock-frequency = <148500000>; hactive = <1920>; vactive = <1080>; hback-porch = <148>; hfront-porch = <88>; hsync-len = <44>; vback-porch = <36>; vfront-porch = <4>; vsync-len = <5>; hsync-active = <0>; vsync-active = <0>; de-active = <1>; pixelclk-active = <0>; }; }; }; };这些时序参数必须严格按照屏幕规格书填写。clock-frequency是像素时钟,计算公式是:(hactive + hback-porch + hfront-porch + hsync-len) × (vactive + vback-porch + vfront-porch + vsync-len) × 刷新率。以1920x1080@60Hz为例,总水平像素是1920+148+88+44=2200,总垂直行数是1080+36+4+5=1125,像素时钟就是2200×1125×60=148,500,000Hz。
时序参数填错会导致画面偏移、闪烁、甚至完全不显示。如果规格书没给全,可以参考同类屏幕的参数,然后用modetest微调。
4.3 用modetest验证驱动加载
设备树配好后,编译烧录,启动系统,先看dmesg有没有报错:
dmesg | grep -i "panel\|dsi\|vop"如果驱动加载正常,应该能看到panel识别成功的日志。然后用modetest确认DRM设备注册成功:
modetest -M rockchip -c输出里应该能看到你的panel对应的Connector,状态是"connected"。如果状态是"disconnected",说明驱动没识别到屏幕,检查设备树配置和硬件连接。
确认Connector识别后,用modetest直接输出测试画面:
modetest -M rockchip -s <connector_id>@<crtc_id>:1920x1080如果屏幕能显示彩条测试画面,说明整个显示链路是通的。如果不行,看modetest的报错信息,常见的有:
- "Permission denied":权限不够,用root运行
- "Device or resource busy":SurfaceFlinger占用了设备,先stop
- "Invalid argument":参数不对,检查connector_id和crtc_id
- "No such device":DRM设备没注册,检查内核配置
4.4 上层框架适配与验证
内核层调通后,启动SurfaceFlinger,看Android画面是否正常。如果内核modetest正常但Android画面异常,问题就在框架层。
常见的框架层问题:
- 分辨率不对:检查
dumpsys display里的逻辑分辨率和物理分辨率是否匹配 - 旋转方向反了:检查HWC的旋转配置和panel的orientation属性
- 画面撕裂:检查vsync配置和HWC的合成策略
- 图层叠加异常:检查plane的格式支持和HWC的plane分配策略
这时候dumpsys SurfaceFlinger和dumpsys display就是主要工具。对比内核DRM状态和框架层状态,找出不一致的地方。
5. 常见问题排查与避坑指南
5.1 屏幕完全不亮
这是最常见也最让人抓狂的问题。排查思路按以下顺序:
- 背光是否亮:用手电筒照屏幕,如果有隐约画面说明背光问题,检查背光电路和PWM配置
- 供电是否正常:测屏幕的VCC、VDD、AVDD等供电引脚
- 复位和使能时序:用示波器看reset和enable信号的时序是否符合规格书要求
- MIPI信号:用示波器或MIPI分析仪看差分信号有没有输出
- 驱动是否加载:dmesg看panel驱动有没有probe成功
- DRM是否注册:modetest看connector有没有识别
这个顺序是从硬件到软件,从底层到上层,能帮你快速定位问题层级。
5.2 画面花屏或闪烁
花屏通常是数据格式或时序问题:
- 像素格式不匹配:检查panel的format配置和VOP的输出格式是否一致,RGB888对RGB888,不要一个RGB888一个RGB565
- 时序参数偏差:特别是porch参数,差几个像素就可能导致花屏
- 时钟频率不对:像素时钟偏差过大会导致画面抖动或闪烁
- DDR带宽不足:高分辨率高刷新率时,如果DDR带宽不够,会出现周期性花屏
闪烁问题还要检查背光PWM频率,频率太低人眼会感知到闪烁,一般建议PWM频率在1kHz以上。
5.3 分辨率识别错误
HDMI或DP接口常见的问题。排查步骤:
# 查看EDID是否读取成功 cat /sys/class/drm/card0-HDMI-A-1/edid | hexdump -C # 查看支持的模式列表 cat /sys/class/drm/card0-HDMI-A-1/modes如果EDID读取失败(全0或乱码),检查DDC通道的I2C通信。如果EDID正常但模式列表不对,检查驱动里的模式过滤逻辑。
有时候显示器支持的时序比较特殊,标准EDID解析不出来,需要在驱动里手动添加模式。
5.4 多屏显示不同步
多屏场景下,如果两个屏幕刷新率不同或者时序不同步,会出现画面撕裂或延迟。解决办法:
- 确认两个CRTC是否独立工作
- 检查是否使用了同一个时钟源
- 如果是镜像模式,确认合成策略是否正确
- 如果是扩展模式,确认每个屏幕的plane分配是否合理
5.5 常见问题速查表
| 问题现象 | 可能原因 | 排查工具 | 解决方向 |
|---|---|---|---|
| 屏幕完全不亮 | 背光/供电/时序/驱动 | 万用表、示波器、dmesg | 从硬件到驱动逐层排查 |
| 画面花屏 | 格式不匹配/时序偏差 | modetest、drm_info | 检查format和timing配置 |
| 画面闪烁 | PWM频率低/时钟不稳 | 示波器、dmesg | 调整PWM频率和时钟配置 |
| 分辨率错误 | EDID读取失败/模式过滤 | sysfs、dmesg | 检查DDC通信和模式列表 |
| 旋转方向反了 | orientation配置错误 | dumpsys display | 修改panel的rotation属性 |
| 多屏不同步 | 时钟源不同/plane冲突 | dumpsys SurfaceFlinger | 检查CRTC和plane分配 |
| 热插拔无响应 | HPD检测失败 | sysfs status | 检查HPD电路和驱动 |
6. 工具组合与调试策略
6.1 按问题层级选工具
调试显示问题,关键是快速定位问题层级。我的经验是按以下策略选工具:
- 硬件层:万用表、示波器、MIPI分析仪
- 内核DRM层:modetest、drm_info、debugfs、dmesg、ftrace
- Android框架层:dumpsys SurfaceFlinger、dumpsys display、dumpsys window
- 应用层:systrace、gfxinfo、SurfaceFlinger latency
先用modetest确认内核层是否正常,如果正常就往上层查,不正常就往底层查。这个二分法能帮你快速缩小范围。
6.2 调试前的准备工作
每次调试前,建议先做以下准备:
- 确认调试版本是userdebug或eng,有root权限
- 准备好屏幕规格书,特别是时序参数表
- 准备好硬件原理图,方便查引脚定义
- 确认串口或adb连接稳定,方便看日志
- 备份当前可工作的设备树配置,方便回退
这些准备工作看起来琐碎,但能帮你省下大量来回折腾的时间。
6.3 日志分析技巧
看日志不要只看错误,info级别的日志往往更有价值。DRM驱动在初始化时会打印每个阶段的详细信息,包括:
- Connector识别结果
- EDID读取结果
- 支持的显示模式列表
- CRTC和Plane的分配情况
- 时钟配置结果
把这些信息串起来看,能还原出整个初始化流程,快速定位哪一步出了问题。
另外,dmesg的时间戳很有用。如果某个操作后出现异常,对比时间戳能帮你确认是哪个操作触发的。
6.4 实操心得
心得一:先确认硬件再动代码。我见过太多人一遇到问题就改驱动,结果改了半天发现是硬件问题。养成先测硬件、再看日志、最后改代码的习惯。
心得二:modetest是底线工具。如果modetest都显示不了,别指望上层能正常。反过来,如果modetest正常但Android异常,问题一定在框架层,不用往内核查。
心得三:善用debugfs的平台节点。每个平台的DRM驱动都会在debugfs里加自己的调试节点,这些节点往往比通用工具更有用。拿到新平台先翻一遍debugfs。
心得四:时序参数不要凭感觉填。像素时钟、porch参数、同步极性,每一个都要按规格书来。差之毫厘谬以千里,显示时序就是这么敏感。
心得五:多屏调试先单屏调通。不要一上来就调双屏,先把单屏调稳定,再叠加第二屏。这样问题定位会简单很多。
心得六:保留一份可工作的配置。每次调通一个配置就备份,后面改出问题了可以快速回退。显示驱动调试经常需要反复试参数,有备份能省很多时间。
6.5 进阶调试手段
当基础工具不够用时,还有一些进阶手段:
- 寄存器dump:直接读VOP/DSI控制器的寄存器,对比规格书确认配置是否正确
- MIPI协议分析:用MIPI分析仪抓包,看DSI命令和数据是否正确
- 带宽分析:用性能计数器分析DDR带宽占用,确认是否满足显示需求
- 功耗分析:用功耗计测量显示子系统的功耗,优化电源管理
这些手段需要额外的硬件设备,但在调复杂问题时会非常有用。
7. 不同平台的调试差异
7.1 Rockchip平台
Rockchip的DRM驱动在debugfs里提供了丰富的调试节点:
# 查看VOP状态 cat /sys/kernel/debug/dri/0/summary # 查看VOP寄存器 cat /sys/kernel/debug/dri/0/regssummary节点能直接看到每个VOP的当前配置,包括输入格式、输出接口、时序参数等。调Rockchip平台时,这个节点是必看的。
7.2 Allwinner平台
Allwinner的显示驱动在debugfs里的节点路径不同:
cat /sys/kernel/debug/dri/0/sunxi_drmAllwinner平台的显示调试相对简单,但要注意它的Display Engine版本差异,不同版本的DE在plane能力和格式支持上有区别。
7.3 通用建议
不管什么平台,调试思路是一样的:先确认硬件,再看内核DRM状态,最后查框架层。平台特有的工具和节点能提高效率,但核心方法论是通用的。
8. 写在最后
显示驱动调试这件事,工具只是手段,核心是对整个显示链路的理解。你得知道数据从应用层到屏幕经过了哪些环节,每个环节可能出什么问题,才能选对工具、快速定位。
我个人的经验是,把modetest、debugfs、dmesg这三个工具用熟,能解决80%的显示问题。剩下的20%需要更专业的设备和对平台细节的深入理解。但不管多复杂的问题,排查思路都是一样的:从硬件到驱动,从底层到上层,逐层确认,逐步缩小范围。
最后分享一个小技巧:每次调通一个配置,把完整的dmesg日志、modetest输出、debugfs状态都保存下来。下次遇到类似问题,对比正常和异常的日志,差异点往往就是问题所在。这个习惯帮我省下了大量重复排查的时间。