干显示驱动开发这行的人都知道Linux显示链路是个“横跨多层”的活光看应用层代码根本看不出所以然可一旦到了内核态、进了硬件寄存器又容易一头雾水。这次我借着龙芯LSDC这套DRM显示驱动把从处理器内部的显示控制器一路讲到HDMI接口出图像的全链路拆开揉碎讲清楚硬件、内核、用户态之间到底是怎么配合的。无论你是刚接触国产平台显示开发的新手还是想排查黑屏、花屏、分辨率缺失这类问题的老手这篇内容都值得花几分钟读完。1. 全链路视角下的LSDC与DRM架构1.1 LSDC在龙芯整机平台中的位置LSDC的全称一般对应龙芯平台上的显示控制器它并不是一颗独立的GPU而是集成在处理器SoC内部的一块显示输出引擎。你可以把它理解成一个带有DMA能力、图层合成能力和时序生成能力的“画面搬运工”内核里的显存缓冲区经过它读取、合成、扫描输出最终变成屏幕上的像素。在整机形态上龙芯桌面处理器的显示输出一般走两种路径。一种是处理器自带的显示控制器直接引出信号经过板级走线连接到HDMI或VGA、DP接口另一种是通过独立的显卡扩展卡但后者大多走PCIe总线和SoC自带的LSDC在架构上差异明显。本文讨论的是前者也就是“从SoC内部一路到HDMI座子”的集成方案。这块显示控制器和CPU核心、内存控制器、PCIe控制器等共享系统总线。它工作起来需要几样东西可访问的显存地址空间、时钟源、中断信号、电源域以及板级固定的IO引脚配置。在Linux下这些资源由设备树或ACPI描述驱动在probe阶段获得它们再交给DRM框架统一管理。这个“先拿资源、再建对象、后配模式”的顺序就是整个驱动初始化的脉络。1.2 为什么显示链路一定要用DRM框架有人会问显示输出不就是往寄存器里写个地址、写个时序参数吗为什么非得用DRM这一大套东西原因在于Linux下显示资源是“共享且互斥”的。桌面环境下可能有多个进程都想往屏幕上画内容也可能有控制台、桌面环境、视频播放器同时存在。DRM的核心价值在于提供一套标准化的资源管理接口把“谁能使用显示引擎”“谁能改分辨率”“缓存怎么分配与同步”这些事沉淀成内核机制避免上层各玩各的。DRM框架里的几个核心对象和LSDC硬件有清晰的对应关系CRTC对应显示控制器里的时序发生器与扫描输出阶段负责产生像素时钟、行场同步信号。Plane对应图层也就是一帧画面的数据来源。LSDC内部通常有几个硬件图层支持不同的颜色格式和缩放标记。Encoder对应信号编码转换环节比如并行RGB信号到HDMI TMDS信号的转换。Connector对应物理接口比如HDMI座子、DP座子负责热插拔检测和EDID读取。硬件如此驱动层要做的就是把这几个“对象”一一实例化并注册进DRM核心。用户态用modetest也好、桌面环境也好最终都是通过DRM ioctl去枚举和配置这套对象的组合。可以这么说DRM就是这份全链路“地图”的主干道而LSDC驱动只是把硬件能力翻译成地图上的几个节点。2. 硬件链路的核心细节2.1 显示控制器的内部管线拆解LSDC内部的显示管线大致可以分成取数、合成、输出三个阶段。取数阶段由多个独立的DMA通道组成每个通道对应一个Plane。DMA从系统内存的帧缓冲中按固定 stride 读取像素数据经过缓存行、像素格式转换后送入合成器。这里的“stride”很关键它表示图像一行数据在内存中占用的字节数。如果用户态只给了分辨率而忘了按对齐规则设置stride边框区域就可能出现色彩错位。合成阶段把多个图层按Z序叠在一起。LSDC支持主图层、光标图层和若干叠加图层。主图层一般作为桌面帧缓冲光标图层专门放鼠标指针叠加图层可以用来做视频画面、硬件菜单等。合成器输出是完整的RGB像素流有些型号还会经过gamma校正和色彩矩阵调整。输出阶段则把合成好的像素流按目标分辨率转换成带时序的并行信号再交给HDMI等接口编码器。这里涉及像素时钟、水平/垂直消隐参数的生成所有参数最终都要写进CRTC相关的寄存器组。2.2 像素时钟与HDMI带宽计算显示链路里最容易忽略、也最容易出问题的就是时钟计算。以1080p60为例有效像素为1920x1080但实际传输时需要加上水平和垂直消隐区域。标准CVT或VIC 16时序下水平总像素约2200垂直总行约1125所以像素时钟大约为2200x1125x60接近148.5MHz。这个148.5MHz就是CRTC输出的像素时钟。到了HDMI侧TMDS编码会把每个8bit像素扩展成10bit符号于是HDMI链路上的数据速率就是148.5x101.485Gbps。HDMI使用4对差分线中的3对传数据因此每对线上实际速率大约495Mbps。这是在所有HDMI版本下都能跑的速率所以1080p60的兼容性最好。上升到4K60时情况就完全不同。3840x216060的像素时钟大约594MHzTMDS符号率达到5.94Gbps这时候HDMI 1.4的带宽天花板10.2Gbps理论值、实际约8.16Gbps确实能勉强容纳但需要开启压缩或降低色彩深度。HDMI 2.0把带宽提升到18Gbps才能以RGB 4:4:4、8bit的方式输出4K60而到了HDMI 2.1又引入了FRL传输模式用更少的差分对跑更高速度。做驱动适配时拿到一个显示器的EDID首先要算出目标时序的像素时钟再和当前连接方式的带宽做比较否则必然黑屏。2.3 从CRTC到HDMI连接器的信号通路信号从CRTC产生后通常先进入Encoder。对LSDC集成方案来说Encoder可能是一个内置的HDMI发送器也可能只是一个桥接逻辑。Encoder的输出是TMDS差分信号由板级走线连到HDMI座子。HDMI连接器除了TMDS数据线还有DDC通道和HPD引脚。DDC基于I2C协议用来读取显示器的EDIDHPD是热插拔检测信号显示器插入或拔出时会拉高或拉低。驱动里的connector检测逻辑就是通过HPD引脚的中断触发再去DDC上读EDID。EDID读不到模式设置就没法生成这是绝大多数“插上没反应”的根源信号通路上还有一个容易忽略的点TMDS走线的阻抗匹配和差分对内等长。驱动与软件层解决不了硬件走线带来的眼图恶化板级如果在HDMI差分对附近走过强干扰源画面就会出现水波纹或随机闪烁。这些问题在代码层面很难定位得优先排硬件。3. 内核驱动的注册与模式设置流程3.1 驱动探测与DRM设备注册LSDC驱动在内核里的身份是platform driver或PCI驱动取决于SoC的集成方式。集成在SoC内部的一般用platform driver设备树里会有一段节点描述它包含寄存器基地址、中断号、时钟引用等。驱动probe时要做的事可以按顺序列出来获取并映射寄存器资源。获取时钟并启动很多显示控制器必须要有时钟才能访问寄存器。请求中断HPD、vblank、错误中断都挂在同一个或几个中断号上。初始化硬件状态机至少把显示引擎停在一个安全状态。调用drm_dev_alloc分配DRM设备再通过drmm_add_privacy_screen之类的方式注册各类回调。创建CRTC、Plane、Encoder、Connector等对象。最后调用drm_dev_register向用户空间暴露/dev/dri/card0。这里有个容易犯错的地方注册DRM对象之前一定要先确认硬件处于“不输出也不访问内存”的安全状态。如果硬件还在扫描某个无效显存地址drm_dev_register一旦触发用户态枚举驱动可能直接就访问异常了。踩过这个坑的人都知道初始化顺序宁可多花几步也不能图省事。3.2 设备树配置要点设备树里LSDC节点一般长这样display-controllerxxx { compatible loongson,lsdc; reg 0x0 0x1f000000 0x0 0x10000; interrupts 0 43 4; clocks clk_dc; clock-names dc; };reg定义寄存器空间interrupts定义中断号。clocks这一项尤其重要某些平台上如果没在设备树里配置显示控制器时钟驱动里读寄存器全是零或者写进去不生效调试时根本无从下手。另一种常见情况是中断号配置和实际SoC手册不一致导致HPD事件完全没有中断上报。在硬件平台调试时我一般会先用devmem把寄存器手敲一遍确认资源通路是通的再去套DRM框架。3.3 mode_set和显示状态切换的完整过程当用户态通过DRM ioctl设置显示模式时内核侧会走一系列回调。大致流程是用户态调用drmModeSetCrtc传入crtc_id、fb_id、connector_id列表和mode。内核先做资源校验这个crtc是否可连这些connector这个fb的格式、尺寸是否被crtc支持带宽是否足够然后DRM核心进入modeset流程把planes、crtc、encoder、connector按顺序编程。编程顺序一般是先配Plane的源地址与尺寸再配CRTC的时序再使能Encoder和Connector。顺序如果颠倒可能出现输出端接到了空时序信号或瞬间闪屏。如果涉及切换分辨率还涉及vblank资源的重新计算。vblank是垂直消隐期间的中断信号用户态做Page Flip页面翻转时需要它来保证画面不撕裂。LSDC的vblank频率直接依赖于像素时钟和垂直总行数所以同一块屏分辨率一变vblank周期也变了。3.4 fbdev兼容与Page Flip机制虽然现代Linux桌面已经尽量往DRM/libdrm走但控制台、部分嵌入式环境仍然依赖/dev/fb0。DRM子系统的fbdev模拟层可以把DRM的framebuffer映射成fbdev接口这样系统起来时即使没有桌面环境也能看到本地控制台输出。Page Flip是另一个关键机制。它允许用户态提前把下一帧的buffer地址绑定到Plane然后等vblank瞬间切换。LSDC在切换新缓冲地址时要特别注意DMA没有在读取旧地址的中途被切走否则顶部几行会残留上一帧内容。现代DRM里atomic接口会统一处理这类依赖但老旧的legacy接口仍然存在这些细节。建议新项目一律走DRM atomic API能省掉大量同步问题的排查时间。4. 实操从模组上电到HDMI输出的完整验证4.1 内核配置与基础工具准备想亲手验证这条链路首先得确认内核打开了DRM和对应驱动选项。内核配置里多半是这样几项CONFIG_DRMyCONFIG_DRM_LSDCy或对应内核版本中龙芯显示驱动的选项名CONFIG_FBm或y如果需要fbdev兼容层CONFIG_DRM_FBDEV_EMULATIONy启动后检查/dev/dri目录是否生成card0以及/sys/class/drm/下是否有card0-HDMI-A-1这类connector节点。调试时必备工具包括libdrm自带的modetest、drm_info或者内核debugfs接口。modetest最常用-M参数指定驱动名-c参数枚举connector-p枚举plane-e枚举encoder。比如modetest -M loongson -c这条命令会列出当前所有connector、连接状态、EDID是否有效、支持的mode列表。如果这里都看不到分辨率和刷新率后面的一切都无从谈起。4.2 用modetest手动点亮HDMI屏幕拿到一块裸屏或者一台显示器时我习惯按这个顺序做全链路验证先确认设备节点存在dmesg里没有probe阶段的错误。modetest -M loongson -c查看connector状态。状态如果是connected说明HPD和EDID读取已经通了如果disconnected优先排查HPD引脚或设备树中断配置。选择一个支持的分辨率比如1920x1080用modetest把某个connector和crtc绑定起来再把一张测试图或纯色帧缓冲送上去。观察屏幕是否点亮颜色是否正常有没有花屏、抖动。modetest手动点屏的好处是绕开了桌面环境一旦出问题可以快速判断是内核模式设置的问题还是上层合成器的问题。实际项目里这种“先内核、后用户态”的排查顺序比一上来盯着桌面日志瞎猜高效得多。4.3 验证帧缓冲与显示合成点屏成功后进一步验证DMA和合成逻辑。可以用modetest -P参数把某个Plane挂到另一块buffer上看看硬件图层是否正确叠加。如果主图层正常、叠加图层花屏那多半是叠加图层的像素格式或stride配置有问题而不是主板时序问题。如果要验证视频播放场景可以在Wayland或X11环境跑一个硬件解码播放器然后观察CPU占用率和画面是否撕裂。CPU占用如果异常高说明画面合成没有走硬件图层全部在用CPU做合成这种情况下DMA带宽和图层配置需要重新检查。很多嵌入式项目里这个阶段的长时间跑机测试比功能测试更快暴露问题。我用过一块800x480的LVDS转HDMI小板刚开始怎么都能点亮跑了半小时后偶尔闪屏后来定位到是显示控制器的时钟源在温度升高后漂移导致行场同步出现偶发异常。这类问题在静态功能测试中根本发现不了。4.4 结合debugfs细看内核状态内核里还有一个很好用的调试入口/sys/kernel/debug/dri/0/state。这个文件会把当前DRM对象的状态全部打印出来包括每个Plane的src/dst坐标、格式、visible状态CRTC的mode、active状态connector的link status等。显示异常时先看这个文件cat /sys/kernel/debug/dri/0/state可以看到哪些对象没在预期状态。比如Connector是“connected”但CRTC是“disabled”说明上层没把模式真正使能如果Plane的visible是false说明buffer配置不满足硬件条件。这是定位“画面没出来但设备正常”的利器比反复改用户态代码快得多。5. 常见问题与排查技巧实录5.1 黑屏但驱动加载正常这恐怕是显示开发里出现频率最高的问题。典型特征是dmesg没有报错modetest也能枚举到connector和mode但屏幕就是黑的。排查顺序建议这样走用modetest挂上一个已知支持的简单分辨率排除分辨率不匹配。阅读dmesg看有没有mode set失败、时钟计算失败、link training失败之类的关键字。检查HPD状态和EDID读取是否稳定有些显示器EDID前256字节没问题扩展块读不出来也会导致某些分辨率被隐藏。检查fbuffer地址和DMA是否可达如果显存分配在只有CPU可访问的区间显示引擎读不到自然黑屏。最后查PHY电源和时钟很多SoC显示输出需要独立的PLL和模拟电源驱动程序里如果漏了开这些资源CRTC时序生成是正常的但信号根本没有从引脚送出去。黑屏问题里硬件层面走线、显示屏供电、甚至HDMI线材老化都可能在特定分辨率下导致黑屏不要只盯着驱动代码。5.2 花屏、条纹、色彩错位花屏问题的定位思路和黑屏不太一样它说明信号链已经通了但数据内容或时序细节不对。常见原因包括stride不对、颜色格式不匹配、显存访问位宽设置错误。比如用户态传了RGBA8888的buffer但驱动里默认按ARGB8888解释红蓝通道就会对调画面明显偏色。再比如硬件支持按64字节对齐的扫描线地址但上层给的stride按16字节对齐一半数据“错位”画面就是斜条纹。遇到花屏我建议先把颜色格式和stride打印出来确认再缩小数据通路范围。如果静态图花屏而视频画面正常多半是合成路径问题如果所有画面都花优先怀疑DMA和显存控制器配置。5.3 分辨率缺失或无法设置某个刷新率显示器支持2560x1440但modetest里只列到1920x1080这种问题通常有三个原因。第一是EDID解析不完整很多显示器在1080p以上走的是EDID扩展块驱动如果只解析基块高分辨率自然看不到。第二是驱动有模式白名单或最大像素时钟限制比如只允许像素时钟不超过某个值那么4K显示器也会被降级。第三是CRTC支持的最大分辨率本身有限制这是硬件能力决定的驱动改不了。处理思路是先用drm_info或edid-decode读完整EDID确认显示器确实给出这个mode再看驱动内部对mode的过滤条件是像素时钟还是最大地址宽度限制。曾经遇到一个项目显示器的EDID里同时给了3840x216030和60但驱动只能跑到30后来排查发现是PHY的TMDS时钟上限只支持340MHz而非驱动代码问题。5.4 HPD不稳定导致拔插识别异常HPD信号虽然只是根引脚但问题非常多。最常见的是HPD抖动也就是拔插瞬间电平反复跳变导致内核频繁触发connector检测甚至误判连接状态。解决HPD抖动有几个层次硬件上在HPD引脚加RC滤波、软件上驱动维持一个检测延迟比如100ms内的跳变只算一次以及内核里配合DRM的connector detect轮询机制。很多平台会既支持HPD中断又支持轮询我一般会打开轮询作为兜底即使HPD中断丢了系统也能定时刷新连接状态。6. 调试工具与全链路验证清单6.1 必备调试工具清单做全链路分析时工具列表我整理成了一张表配合使用基本能覆盖80%以上问题排查目标推荐工具关键输出设备节点生成/dev/dri、/sys/kernel/debug/dricard0、connector节点驱动加载日志dmesgprobe错误、模式设置日志Connector/EDID状态modetest -c、edid-decode连接状态、EDID块内核状态快照/sys/kernel/debug/dri/0/stateCRTC/Plane/Connector状态时钟/电源状态devmem、clk_summaryPLL配置、时钟使能状态上层合成链路weston --debug、Xorg log合成器崩溃点、模式协商记录调试最重要的不是用某个花哨工具而是建立“硬件→驱动→用户态”的匹配思维。每个层面都有对应的验证方法不要一上来就翻到最后一个环节找原因。6.2 从开机到输出的完整验证流程我自己习惯的验证流程分成四步第一步是验证电源与复位。系统启动后立刻用devmem确认显示控制器寄存器可以读写如果读写返回全0或访问异常优先排查设备树地址和时钟。第二步是验证模式枚举。modetest -c能看到连接状态并列出mode就说明寄存器通路、HPD、EDID链路都工作正常。第三步是验证单帧输出。modetest手动挂一个纯色buffer到crtc确认屏幕点亮且颜色正确证明从显存到像素输出的主数据通路没问题。第四步是验证完整栈。启动桌面环境或Wayland合成器跑播放、截图、分辨率切换等完整场景。这套流程每次都能在最短时间内把问题限定到某一层再针对这一层深入排查。比起“先试一下换个分辨率”的随机式排障这样有章法得多。6.3 长期稳定性与信号质量建议功能点亮只是起点长时间运行才是真正考验显示链路的地方。建议在实际部署前至少做几项压力验证长时间播放动态视频、频繁切换分辨率、反复插拔HDMI、让SoC和电源进入低功耗状态再唤醒。这些场景最容易暴露vblank中断丢失、DMA带宽不足、时钟漂移和热插拔状态机异常等问题。信号质量方面如果手头有示波器可以测HDMI差分对的眼图确保走线和连接器工艺没有导致信号劣化。如果没有仪器条件至少确保板级设计时TMDS走线做过阻抗控制HDMI座子附近不要走高速数字线。电子行业有句话叫“硬件决定下限软件决定上限”在HDMI这类高速接口上尤其成立。7. 个人经验补充开发LSDC DRM驱动这段时间我最大的体会是显示链路的问题十个里有八个并不是最后的“某个寄存器写错”而是链路前面的某个假设不成立。可能是设备树的中断号抄错了可能是EDID解析只做了一半也可能是HDMI线材本身在某个分辨率下就不够稳定。所以我现在遇到问题第一反应永远是“先确认链路每一层的证据再怀疑代码逻辑”。还有一个小技巧调试时尽量把drm.debug参数打开。内核里支持drm.debug级别控制打开后dmesg会打印所有DRM ioctl和对象状态变更记录能很清楚看到用户态向内核发了什么请求、内核又是怎么处理的。配合debugfs的状态文件几乎可以在不做任何代码改动的情况下看清整条链路的运行轨迹。
阅读完成 · 觉得有帮助?