
1. 显示驱动调试的痛点与工具选型逻辑搞嵌入式Linux显示驱动的朋友大概都有过这种体验屏幕点不亮串口打印一片安静连个报错都没有或者画面出来了但颜色偏得离谱分辨率死活对不上。这时候如果手头没有趁手的调试工具基本就是盲人摸象全靠猜和试。显示驱动这条链路从应用层到内核层再到硬件接口中间隔着DRM框架、KMS子系统、各种connector和encoder任何一个环节出问题都会导致最终显示异常。而调试工具就是帮我们把这条链路逐段打通、逐段验证的关键手段。我这些年折腾过不少平台从早期的Framebuffer到现在的DRM/KMS架构踩过的坑可以说能写一本书。显示驱动调试最核心的需求其实就三个第一确认驱动有没有正确加载、设备树有没有被正确解析第二确认显示管道的各个组件CRTC、encoder、connector、plane是否正常工作和正确连接第三确认最终的画面输出是否符合预期。围绕这三个需求社区里沉淀出了一批非常实用的工具其中modetest可以说是DRM调试的瑞士军刀而nc这种网络调试工具在远程调试场景下也能发挥意想不到的作用。这篇文章主要面向正在做嵌入式Linux显示驱动开发的工程师不管你是刚接触DRM框架的新手还是已经能改驱动但调试效率不高的老手应该都能从里面找到一些实用的东西。我会从工具选型的逻辑讲起然后逐个拆解核心工具的使用方法和底层原理再结合实际的调试场景给出完整的操作流程最后把我自己踩过的坑和总结的技巧分享出来。整篇内容基于DRM/KMS框架展开因为这是目前嵌入式Linux显示驱动的主流架构Framebuffer那套虽然简单但已经逐渐被替代了。1.1 为什么是DRM/KMS而不是Framebuffer先说说为什么现在调试显示驱动基本都绕不开DRM。早期的Framebuffer架构非常简单/dev/fb0就是一个线性缓冲区应用层往里面写数据驱动负责把数据推到屏幕上。这种模型在单图层、固定分辨率的场景下够用但一旦涉及多图层叠加、硬件光标、色彩管理、多显示输出这些需求Framebuffer就力不从心了。DRMDirect Rendering Manager把显示控制抽象成了KMSKernel Mode Setting子系统引入了CRTC、encoder、connector、plane、framebuffer这些对象每个对象各司其职通过atomic commit机制协调工作。这个抽象带来的好处是灵活代价就是调试复杂度直线上升。一个典型的显示管道是这样的framebuffer提供像素数据plane负责图层合成CRTC负责扫描输出和时序生成encoder负责把像素数据编码成特定接口的信号比如MIPI DSI、HDMI、LVDSconnector负责物理连接和EDID读取。任何一个环节配置错误屏幕都不会亮。而modetest这个工具的价值就在于它能让你在用户空间直接操作这些对象查看它们的状态手动触发模式设置从而快速定位问题出在哪个环节。1.2 工具选型的几个核心考量在选择调试工具时我通常会从这几个维度来评估信息暴露程度、操作粒度、依赖条件和可脚本化能力。信息暴露程度决定了你能看到多少底层状态比如modetest能列出所有DRM对象的详细属性而有些工具只能告诉你“成功”或“失败”。操作粒度决定了你能控制到多细比如能不能单独设置某个plane的位置和格式。依赖条件很重要有些工具需要X11或Wayland环境在嵌入式板子上根本跑不起来而modetest是直接基于libdrm的不依赖显示服务器。可脚本化能力则决定了你能不能把调试过程自动化比如批量测试不同分辨率。基于这些考量modetest基本是嵌入式Linux显示驱动调试的首选。它直接跟DRM设备节点打交道不依赖上层显示服务信息输出详细而且支持命令行参数控制各种行为。除此之外drm_info也是一个很好的补充它专注于打印DRM设备的能力和状态信息输出格式更结构化。至于nc它本身不是显示调试工具但在远程调试场景下你可以用它来传输调试数据或者触发远程命令后面我会具体讲怎么用。2. modetest核心用法与DRM对象解析modetest是libdrm包自带的一个测试工具源码在libdrm的tests/modetest目录下。它的核心功能是枚举DRM设备上的所有资源包括CRTC、encoder、connector、plane和framebuffer并且可以手动设置显示模式、测试图层合成。在嵌入式环境里通常需要交叉编译libdrm才能得到modetest不过很多板子的根文件系统里已经预装了。2.1 枚举DRM资源第一步永远是看清楚有什么拿到一块板子第一件事就是确认DRM设备节点是否存在。通常是在/dev/dri/目录下card0是主显示设备renderD128是渲染设备。如果这个目录是空的那说明DRM驱动根本没加载成功得先去查内核配置和设备树。确认设备节点存在后就可以用modetest来枚举资源了。modetest -M driver_name -c这个命令会列出所有的connector信息包括connector ID、类型HDMI、DSI、LVDS等、连接状态connected/disconnected、支持的显示模式列表。这里有个细节要注意-M参数指定DRM驱动名称如果不确定驱动名可以先不加这个参数modetest会尝试打开第一个可用的DRM设备。但如果有多个显示设备最好明确指定否则可能操作到错误的设备上。枚举encoder用-e参数枚举CRTC用-r参数枚举plane用-p参数枚举framebuffer用-f参数。实际调试中我通常会把所有信息一次性dump出来modetest -M driver_name -a这个命令会打印所有DRM对象的详细信息包括每个对象的属性ID和当前值。输出内容比较多建议重定向到文件里慢慢看。重点关注的几个信息connector的connection状态是不是connectedCRTC的mode是不是有效plane的formats支持哪些像素格式framebuffer的宽高和格式。2.2 手动设置显示模式让屏幕亮起来枚举完资源后下一步就是尝试手动设置一个显示模式。最基本的用法是这样的modetest -M driver_name -s connector_id:widthxheight比如modetest -M rockchip -s 45:1920x1080就是把connector 45设置成1920x1080的分辨率。如果这个命令执行成功屏幕应该会显示一个测试图案通常是彩条或者渐变。如果屏幕没反应但命令返回成功那可能是背光没开或者时序参数不对。如果命令直接报错那就要看错误信息是什么常见的有“Permission denied”权限不够需要root、“Invalid argument”参数不对比如connector ID写错了、“No such device”设备节点不对。这里有个很关键的细节modetest设置模式时会自动选择一个可用的CRTC和encoder来跟指定的connector配合。但有时候自动选择的结果不是你想要的比如你想用特定的CRTC来输出或者encoder的连接关系比较特殊。这时候可以用-s参数的扩展形式来手动指定modetest -M driver_name -s connector_id:widthxheightcrtc_id甚至还可以指定刷新率modetest -M driver_name -s connector_id:widthxheightcrtc_id#refresh_rate这些细节在调试多显示输出或者验证特定硬件路径时非常有用。2.3 测试图层合成验证plane功能DRM的plane机制支持多图层硬件合成这在嵌入式场景下很重要因为很多SoC的显示控制器支持多个plane叠加可以减轻GPU的合成负担。modetest可以用-P参数来测试planemodetest -M driver_name -P plane_idcrtc_id:widthxheightxyformat这个命令会把指定的plane绑定到CRTC上设置好位置、大小和像素格式。比如modetest -M rockchip -P 3040:800x600100100XR24就是把plane 30绑定到CRTC 40上显示一个800x600的图层位置在(100,100)像素格式是XRGB8888。测试plane的时候最容易遇到的问题就是格式不支持。每个plane支持的像素格式是有限的而且不同plane支持的格式可能不一样。比如有些plane只支持RGB格式有些支持YUV格式。如果指定的格式不被支持modetest会报错。这时候需要先用-p参数查看plane支持的格式列表然后选择一个支持的格式来测试。3. 从内核到用户空间的完整调试链路显示驱动调试不能只盯着用户空间的工具很多时候问题出在内核层。完整的调试链路应该覆盖内核日志、sysfs节点、debugfs节点和用户空间工具四个层面。这一章我会按照从底层到上层的顺序把每个层面的调试手段都梳理一遍。3.1 内核日志dmesg里的蛛丝马迹内核日志是显示驱动调试的第一手资料。DRM子系统在初始化和模式设置过程中会打印大量日志通过dmesg | grep -i drm可以过滤出相关的信息。重点关注这几类日志驱动probe是否成功、connector检测到了什么、EDID读取是否正常、模式设置是否成功、有没有报错或警告。一个典型的正常启动日志会包含这样的信息DRM驱动加载成功发现了一个或多个connector读取到了EDID数据注册了CRTC和plane初始化了framebuffer console。如果connector检测失败日志里会有“failed to read EDID”或者“no connector found”之类的提示。如果模式设置失败会有“failed to set mode”或者“atomic commit failed”的报错。调试的时候可以把DRM的日志级别调高让内核打印更详细的信息。通过sysfs节点可以动态调整echo 0x1ff /sys/module/drm/parameters/debug这个值是一个位掩码不同的位对应不同的调试类别。0x1ff基本打开了所有DRM相关的调试输出。不过要注意打开全部调试输出会产生大量日志可能影响系统性能调试完成后记得关掉。3.2 sysfs与debugfs内核状态的窗口sysfs和debugfs是查看内核状态的两个重要接口。跟显示驱动相关的sysfs节点通常在/sys/class/drm/目录下每个connector和CRTC都有对应的子目录。比如/sys/class/drm/card0-HDMI-A-1/目录下会有status、enabled、modes等文件。status文件显示connector的连接状态modes文件列出支持的显示模式enabled文件显示是否使能。debugfs节点在/sys/kernel/debug/dri/目录下每个DRM设备有一个子目录。里面比较有用的文件包括framebuffer显示当前framebuffer信息、gem_names显示GEM对象信息、clients显示打开DRM设备的进程。有些驱动还会在debugfs里暴露寄存器读写接口可以直接查看显示控制器的寄存器状态这对排查硬件层面的问题非常有帮助。3.3 用户空间工具的组合使用用户空间工具除了modetest还有几个值得关注的。drm_info是一个专门用来打印DRM设备信息的工具输出格式是JSON非常适合脚本处理。kmscube是一个基于DRM/KMS的OpenGL ES测试程序可以用来验证GPU渲染和显示输出的配合是否正常。weston是一个Wayland合成器如果显示驱动支持Wayland跑一下weston可以验证完整的显示栈。这些工具的组合使用策略是这样的先用modetest确认基本的显示管道能工作再用kmscube验证GPU渲染到显示的路径最后用weston验证完整的显示服务栈。如果modetest能显示但kmscube不行那问题可能在GPU驱动或者GEM缓冲区管理。如果kmscube能显示但weston不行那问题可能在Wayland合成器或者输入设备。4. 远程调试与nc的妙用嵌入式开发经常需要远程调试板子可能放在机架上或者实验室的另一头不可能每次都跑到跟前接串口。这时候网络调试工具就派上用场了。ncnetcat是一个功能强大的网络工具虽然它本身不是专门为显示调试设计的但在远程调试场景下可以发挥很大作用。4.1 用nc传输调试数据一个典型场景是板子上的modetest输出很长你想在开发机上分析。如果板子支持SSH那直接SSH过去执行命令就行。但如果板子的根文件系统很精简没有SSH服务只有busybox自带的nc那就可以用nc来传输数据。在开发机上监听一个端口nc -l -p 8888 modetest_output.txt然后在板子上把modetest的输出通过nc发过来modetest -M rockchip -a | nc 开发机IP 8888这样modetest的完整输出就保存到开发机的文件里了。同样的方法可以用来传输dmesg日志、sysfs节点内容、甚至二进制数据。4.2 用nc触发远程命令反过来也可以用nc在开发机上发送命令到板子上执行。板子上先启动一个nc监听nc -l -p 9999 -e /bin/sh然后在开发机上连接过去nc 板子IP 9999这样就得到了一个简单的远程shell可以执行各种调试命令。不过要注意这种方式没有加密和认证只适合在受信任的内网环境中使用。而且busybox的nc实现可能不支持-e参数那就需要换一种方式比如用命名管道mkfifo /tmp/nc_pipe nc -l -p 9999 /tmp/nc_pipe | /bin/sh /tmp/nc_pipe这种方式稍微绕一点但兼容性更好。4.3 远程调试的注意事项远程调试虽然方便但有几个坑要注意。第一网络延迟会影响交互体验特别是需要频繁执行命令的时候。第二如果调试过程中显示驱动崩溃导致系统挂死远程连接也会断开这时候还是得靠串口。第三nc传输的数据没有校验机制如果网络不稳定可能导致数据损坏重要数据最好加上校验和。第四有些板子的网络驱动和显示驱动共享中断或DMA通道网络流量大时可能影响显示输出的稳定性调试时要注意排除这种干扰。5. 常见问题排查与实战经验这一章我整理了一些显示驱动调试中经常遇到的问题和排查思路都是实际项目中踩过的坑。每个问题都给出了现象描述、可能原因和排查步骤希望能帮大家少走弯路。5.1 屏幕完全不亮这是最常见也最让人头疼的问题。屏幕完全不亮串口也没有明显报错感觉无从下手。我的排查思路是这样的先确认背光有没有开。很多板子的背光控制是独立的跟显示驱动没关系。用万用表量一下背光电路的电压或者直接看背光LED有没有亮。如果背光没开先解决背光问题再查显示驱动。背光正常但屏幕还是不亮那就查显示接口的信号。如果有示波器量一下MIPI DSI或者LVDS的时钟和数据线看有没有信号输出。没有示波器的话可以用modetest强制设置一个模式然后观察内核日志有没有报错。如果日志显示模式设置成功但屏幕没反应那可能是时序参数不对比如porch值、同步极性搞错了。这时候需要对照屏幕的datasheet仔细核对时序参数。还有一种情况是驱动加载了但connector状态是disconnected。这通常意味着HPD热插拔检测信号有问题或者EDID读取失败。可以先在驱动里强制把connector状态改成connected绕过HPD检测看看能不能点亮。如果能点亮说明问题在HPD电路或者EDID读取上。5.2 画面颜色异常画面能显示但颜色不对比如偏红、偏蓝、或者颜色完全错乱。这种问题通常跟像素格式有关。DRM支持多种像素格式RGB888、RGB565、ARGB8888、YUV420等等。如果framebuffer的格式和plane的格式不匹配或者plane的格式和CRTC的格式不匹配就会导致颜色异常。排查方法是先用modetest的-p参数查看plane支持的格式列表然后用-f参数查看当前framebuffer的格式确认两者是否一致。如果不一致要么修改framebuffer的格式要么选择一个支持该格式的plane。另外还要注意字节序问题有些平台是大端有些是小端像素格式的字节序搞错了也会导致颜色异常。还有一种颜色异常是色彩空间转换导致的。比如YUV格式的视频数据在RGB显示器上显示需要经过YUV到RGB的转换。如果转换矩阵不对颜色就会偏。这种问题通常需要在驱动里配置色彩空间转换矩阵或者用硬件的高质量转换单元。5.3 分辨率设置失败想设置一个特定的分辨率但总是失败modetest报“Invalid argument”或者“No mode found”。这种情况通常是这个分辨率不在connector支持的模式列表里。先用-c参数查看connector支持的所有模式确认目标分辨率是否在列表里。如果不在可能是EDID里没有这个模式或者驱动没有正确解析EDID。如果EDID里确实没有这个模式但屏幕硬件支持可以尝试用CVT或者GTF公式计算时序参数然后通过驱动或者设备树强制添加这个模式。计算时序参数的时候要注意不同屏幕对时序的要求可能不一样有些屏幕对porch值很敏感差几个像素就不显示。最好参考屏幕datasheet里的推荐值或者用标准的CVT时序。还有一种情况是带宽不够。高分辨率高刷新率需要更高的像素时钟如果显示接口的带宽不够模式设置会失败。比如MIPI DSI的lane数和每lane速率决定了最大带宽如果带宽不够要么降低分辨率要么降低刷新率要么增加lane数。5.4 多显示输出的问题多显示输出场景下问题会更复杂。常见的问题包括两个屏幕显示相同内容mirror模式配置错误、第二个屏幕不亮CRTC或encoder资源不够、两个屏幕刷新率不同步vsync共享问题。排查多显示问题时首先要确认硬件支持几个独立的显示管道。有些SoC虽然有两个显示接口但共享同一个CRTC那就只能做mirror不能做extend。用modetest调试多显示时需要手动指定每个connector对应的CRTC。先用-c和-r参数列出所有的connector和CRTC然后规划好哪个connector用哪个CRTC再用-s参数逐个设置。如果CRTC数量不够那就只能做mirror。如果encoder数量不够可能需要复用encoder但要注意encoder的带宽是否够。5.5 常见问题速查表问题现象可能原因排查步骤解决方案屏幕完全不亮背光未开测量背光电压使能背光控制屏幕完全不亮时序参数错误对照datasheet核对修正porch和同步极性屏幕完全不亮HPD信号异常检查HPD电路强制connected或修电路颜色异常像素格式不匹配对比framebuffer和plane格式统一像素格式颜色异常字节序错误检查平台字节序调整格式定义分辨率设置失败模式不在EDID中查看modes列表强制添加模式分辨率设置失败带宽不够计算像素时钟需求降低分辨率或增加lane多屏不同步vsync未共享检查CRTC配置配置共享vsync多屏只有一个亮CRTC资源不够查看CRTC数量使用mirror模式5.6 几个容易被忽略的细节第一个细节是电源管理。有些显示驱动在系统休眠后会丢失寄存器配置恢复时如果没有正确重新初始化屏幕就不亮了。调试这种问题时可以在suspend和resume的回调函数里加打印确认执行顺序和寄存器恢复情况。第二个细节是时钟配置。显示控制器的时钟源可能来自PLL如果PLL配置不对像素时钟就不对屏幕可能显示异常或者完全不亮。用clk_summary节点可以查看当前时钟树的状态确认显示相关时钟的频率是否正确。第三个细节是DMA地址。framebuffer的物理地址如果不对显示控制器读不到数据屏幕就会花屏或者黑屏。特别是在使用IOMMU或者CMA的场景下要确保framebuffer的DMA地址是显示控制器能访问的。第四个细节是中断处理。显示控制器的vsync中断如果处理不当可能导致帧同步丢失或者画面撕裂。调试时可以统计vsync中断的频率跟预期的刷新率对比确认中断是否正常触发。6. 工具链搭建与自动化调试思路前面讲的都是手动调试的方法但实际项目中手动调试效率太低特别是需要反复测试不同参数的时候。这一章我分享一下怎么搭建一个自动化的调试环境把重复性的工作交给脚本。6.1 交叉编译modetest很多板子的根文件系统里没有预装modetest需要自己交叉编译。modetest的源码在libdrm的tests目录下编译之前需要先交叉编译libdrm。以ARM平台为例编译流程大概是这样的先配置libdrm的交叉编译选项指定交叉编译器和sysroot然后make make install。编译完libdrm后再进入tests/modetest目录用同样的交叉编译环境编译modetest。编译过程中可能遇到的问题包括找不到依赖库比如libudev、头文件路径不对、链接选项缺失。解决方法是仔细检查configure的输出确认所有依赖都找到了。如果某个依赖在目标平台上不需要可以用--disable-xxx选项关掉。6.2 用脚本批量测试显示模式有了modetest之后可以写一个简单的shell脚本来批量测试connector支持的所有模式。思路是先解析modetest -c的输出提取出每个connector支持的模式列表然后逐个用modetest -s设置每次设置后检查返回值记录成功和失败的模式。#!/bin/bash CONNECTOR_ID45 MODES$(modetest -M rockchip -c | grep -A 100 Connector $CONNECTOR_ID | grep -oP \dx\d | sort -u) for mode in $MODES; do echo Testing mode: $mode if modetest -M rockchip -s $CONNECTOR_ID:$mode /dev/null 21; then echo PASS else echo FAIL fi sleep 1 done这个脚本虽然简单但在验证驱动支持的模式列表时非常有用。可以快速找出哪些模式能正常工作哪些有问题。6.3 自动化日志收集与分析调试显示问题时日志收集很重要。可以写一个脚本在系统启动后自动收集dmesg、sysfs节点、debugfs节点和modetest的输出打包保存。这样每次复现问题时都有完整的现场信息可以分析。#!/bin/bash LOG_DIR/tmp/display_debug_$(date %Y%m%d_%H%M%S) mkdir -p $LOG_DIR dmesg $LOG_DIR/dmesg.txt cp -r /sys/class/drm/* $LOG_DIR/sysfs_drm/ 2/dev/null cp -r /sys/kernel/debug/dri/* $LOG_DIR/debugfs_dri/ 2/dev/null modetest -M rockchip -a $LOG_DIR/modetest_all.txt 21 tar czf $LOG_DIR.tar.gz $LOG_DIR echo Debug info saved to $LOG_DIR.tar.gz这个脚本可以在系统启动脚本里调用也可以在手动触发时执行。收集到的信息打包后可以拿到开发机上慢慢分析。6.4 远程调试环境的搭建如果板子和开发机在同一个网络里可以搭建一个简单的远程调试环境。板子上跑一个轻量的HTTP服务器或者用nc监听开发机上用脚本发送命令并收集结果。这样不用每次都接串口调试效率会高很多。一个简单的方案是用busybox的httpd在板子上提供文件访问把调试日志和modetest输出放到HTTP目录下开发机上用curl或者浏览器就能查看。另一个方案是用nc做双向通信开发机上写一个交互式的脚本发送命令到板子执行并显示结果。注意远程调试环境搭建时要注意网络安全性不要在公网环境暴露调试接口。调试完成后及时关闭相关服务。6.5 调试脚本的版本管理调试脚本本身也是代码建议用git管理起来。每次调试新问题时有新的脚本或者修改都提交到仓库里。这样积累下来就形成了一套针对特定平台的调试工具集下次遇到类似问题可以直接复用。我在几个项目中都维护了这样的调试脚本仓库新同事入职时直接clone下来就能用省去了大量重复劳动。脚本仓库的结构可以这样组织按平台分目录每个平台下面按调试主题分脚本比如display/、network/、storage/。每个脚本头部写清楚用途、依赖和使用方法。再配一个README说明整体结构和使用流程。这样即使过了很久再回头看也能快速回忆起当时的调试思路。6.6 几个提升调试效率的习惯第一个习惯是随手记录。调试过程中看到的每一个异常现象、试过的每一个参数、得到的每一个结果都记下来。好记性不如烂笔头特别是调试周期长的问题过几天可能就忘了之前试过什么。我通常会在终端里开一个文件边调试边记录最后整理成文档。第二个习惯是最小化复现。遇到复杂问题时先想办法构造一个最小的复现步骤。比如把多余的图层关掉只保留一个最简单的显示管道看看问题是否还存在。最小化复现不仅能帮助定位问题也方便跟别人沟通和求助。第三个习惯是对比法。手头如果有两块板子一块正常一块异常可以对比它们的日志、寄存器值、sysfs节点内容。差异点往往就是问题所在。如果没有第二块板子可以对比正常工作和异常工作时的状态比如修改某个参数前后的差异。第四个习惯是善用搜索引擎和社区。显示驱动的问题很多都是共性的你遇到的问题很可能别人已经遇到并解决了。把关键的错误信息或者现象描述作为关键词搜索往往能找到相关的讨论和补丁。Linux内核的邮件列表、DRM子系统的wiki、各个SoC厂商的论坛都是很好的资源。6.7 关于调试工具的未来思考显示驱动调试工具这几年也在演进。传统的modetest虽然好用但输出格式不够结构化自动化处理不太方便。新的工具比如drm_info提供了JSON输出更适合脚本处理。另外随着Wayland的普及一些针对Wayland合成器的调试工具也在出现比如wayland-info可以查看Wayland协议的详细信息。从趋势上看显示驱动调试正在从手动向自动化、从文本向结构化、从单机向远程化发展。作为工程师保持对新技术和新工具的关注适时更新自己的调试工具箱是提升效率的重要途径。不过工具再多核心的调试思路是不变的理解架构、分层排查、对比验证、最小化复现。掌握了这些思路用什么工具都能快速定位问题。我个人在实际操作中的体会是显示驱动调试最怕的就是没有思路地乱试。先花时间把DRM的架构和显示管道的各个组件搞清楚再结合工具逐段验证比盲目地改参数要高效得多。另外调试过程中保持耐心和记录的习惯很多看似诡异的问题最后发现都是很基础的原因比如时钟没使能、电源域没开、引脚复用配置错了。基础打牢了高级的问题才有解决的根基。