ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Linux DRM显示框架深度解析:KMS与GEM原理及实战调试

2026/9/19 19:26:50 拓冰建站 浏览量
Linux DRM显示框架深度解析:KMS与GEM原理及实战调试 1. 从一块点不亮的LCD说起为什么我要啃DRM这套框架第一次接触LCD驱动很多人是从单片机裸机点屏开始的。初始化时序、写寄存器、刷GRAM一套流程走下来屏幕能亮、能显示色块就觉得驱动不过如此。等到把同样的屏接到Linux板子上用上DRM这套框架才发现事情完全不是一回事屏幕是亮了但分辨率不对、方向反了、刷新率上不去、应用层拿不到buffer一堆问题冒出来翻遍文档也找不到头绪。问题的根源在于Linux下的显示子系统不是写寄存器点屏这么简单它是一整套分层的、面向多应用共享的图形显示架构。DRMDirect Rendering Manager就是这套架构的核心而KMSKernel Mode Setting和GEMGraphics Execution Manager是DRM里最常被提起、也最容易被混淆的两个机制。KMS管的是显示模式设置——分辨率、时序、图层合成、CRTC与连接器的绑定GEM管的是显存管理——buffer怎么分配、怎么在CPU和GPU之间共享、怎么跨进程传递。我写这篇东西是因为在实际调试一块MIPI DSI接口的IPS屏时被竖屏改横屏这个需求卡了整整两天。表面上看只是旋转90度实际上牵扯到KMS的plane旋转属性、GEM buffer的stride对齐、以及应用层对DRM API的调用方式。把这些串起来之后我才算真正理解了DRM框架的设计意图。这篇内容适合三类人一是刚接触Linux显示驱动、想搞懂DRM到底在干什么的嵌入式工程师二是做LCD模组、需要理解上层框架如何影响底层时序的硬件同学三是做应用层图形开发、想搞清楚自己调用的API背后发生了什么的人。我会从框架分层讲起把KMS和GEM各自的职责、数据结构、调用链路拆开再结合竖屏改横屏、亮度调节、buffer共享这些实际场景把踩过的坑和验证过的方法都摊开讲。2. DRM框架的分层逻辑谁在管显示谁在管内存2.1 从fbdev到DRM一次架构上的必然演进早期的Linux显示驱动用的是fbdevframebuffer device框架思路很直接内核里维护一块显存应用层通过/dev/fb0映射这块内存往里写像素硬件自动扫描输出。这套模型在单应用、单屏幕的场景下够用但一旦涉及多图层叠加、多应用共享显示、GPU硬件加速fbdev就撑不住了——它没有图层概念没有同步机制没有显存对象的抽象多个应用抢一块framebuffer必然打架。DRM的出现就是为了解决这些问题。它把显示相关的功能拆成两大块一块是模式设置Mode Setting也就是KMS负责决定屏幕上显示什么、以什么分辨率显示、哪个图层在哪个位置另一块是显存管理Memory Management也就是GEM负责决定这些要显示的图像数据存在哪里、怎么分配、怎么共享。这个拆分不是拍脑袋定的而是对应了两个本质不同的问题域。模式设置是配置态的变化频率低一次设置可以维持很久显存管理是数据态的每帧都在变需要高频分配和回收。把两者分开内核可以针对性地优化KMS的状态可以缓存和原子提交GEM的buffer可以复用和跨进程共享。2.2 KMS的五大对象CRTC、Encoder、Connector、Plane、Framebuffer理解KMS核心是理解它抽象出来的五个对象这五个对象构成了从显存到屏幕的完整链路。CRTCCRT Controller是最核心的对象它代表一个扫描输出引擎负责从显存里读取像素数据按照配置好的时序行同步、场同步、消隐区输出给下游。一块显示控制器通常有多个CRTC对应多个独立的显示通道。CRTC决定了分辨率、刷新率、时序参数这些模式信息。Encoder是CRTC和Connector之间的转换器。CRTC输出的是并行RGB信号而实际屏幕可能是MIPI DSI、LVDS、HDMI等不同接口Encoder负责把并行信号转换成对应接口的格式。一个CRTC可以接多个Encoder但同一时刻只能激活一个。Connector代表物理连接器比如DSI接口、HDMI口。它负责探测屏幕是否插入、读取EDIDExtended Display Identification Data扩展显示标识数据获取屏幕支持的分辨率列表并把可用的显示模式上报给上层。Connector是KMS里唯一直接和物理硬件打交道的对象。Plane是图层代表一个可以独立显示和定位的图像层。每个CRTC至少有一个primary plane主图层必须显示还可以有多个overlay plane叠加图层和cursor plane光标图层。Plane的存在让硬件合成成为可能——多个图层在硬件层面叠加不需要GPU参与省电且高效。Framebuffer是KMS对一块显存的抽象它描述了一块内存的格式像素格式、宽高、stride以及关联的GEM对象。Framebuffer是KMS和GEM的交汇点KMS用framebuffer来描述要显示什么而framebuffer底层指向的正是GEM分配的buffer。这五个对象的关系可以用一句话概括Framebuffer提供像素数据Plane决定数据在屏幕上的位置和层级CRTC负责扫描输出Encoder转换信号格式Connector对接物理屏幕。任何一次显示模式的设置本质上就是把这五个对象按正确的顺序绑定起来。2.3 GEM的核心抽象handle、object与dma-bufGEM这一侧相对简单它的核心抽象是GEM object代表一块可以被GPU和显示控制器访问的显存。每个GEM object有一个全局唯一的name以及一个进程内的handle。handle是进程私有的name是全局的这个设计是为了支持跨进程共享——A进程通过name把object导出B进程通过name导入各自拿到自己进程内的handle。GEM object最关键的能力是dma-buf导出。dma-buf是内核提供的跨设备、跨进程buffer共享机制GEM object可以导出一个dma-buf文件描述符这个fd可以传给其他驱动比如V4L2摄像头驱动、GPU驱动或者传给其他进程。这是现代Linux图形栈实现零拷贝的基础。GEM本身不规定显存怎么分配它只规定接口。具体的分配策略由各个DRM驱动自己实现比如有的驱动用CMAContiguous Memory Allocator分配连续内存有的用shmem有的用TTMTranslation Table Manager做显存迁移。这也是为什么不同平台的GEM行为会有差异调试时需要看具体驱动的实现。2.4 KMS与GEM如何协作一次完整的显示提交把KMS和GEM串起来看一次完整的显示提交大致是这样的流程应用层通过GEM接口分配一个buffer得到handle应用层把像素数据写入buffer可能是CPU直接写也可能是GPU渲染应用层创建一个framebuffer关联这个GEM handle指定像素格式和尺寸应用层通过KMS的atomic接口把framebuffer绑定到某个planeplane绑定到CRTCCRTC绑定到Connector提交atomic请求内核校验参数合法性驱动配置硬件寄存器硬件开始扫描输出屏幕显示内容。这个流程里GEM负责数据从哪来KMS负责数据怎么显示。两者通过framebuffer这个对象衔接。理解了这个衔接点很多调试问题就有了排查方向如果屏幕花屏可能是GEM buffer的stride不对如果屏幕不亮可能是KMS的CRTC没绑定对如果方向反了可能是plane的旋转属性没设置。3. KMS模式设置的完整链路从应用调用到寄存器生效3.1 legacy接口与atomic接口为什么新代码都该用atomicKMS的API经历过一次重大演进。早期是legacy接口用drmModeSetCrtc、drmModeSetPlane这类函数每次调用只改一个对象的状态改完立即生效。这套接口的问题是多个对象的状态变更无法保证原子性中间态可能被硬件扫描到导致屏幕闪烁而且没有回滚机制一旦某个对象设置失败前面的设置已经生效了状态就乱了。atomic接口解决了这些问题。它的核心思想是把所有对象的状态变更打包成一个atomic commit内核先做一次完整的合法性校验check阶段全部通过后才真正写入硬件commit阶段。校验不通过就整体拒绝硬件状态不变。这套机制还支持非阻塞提交和fence同步是现代显示驱动的标准做法。实际写代码时atomic的调用模式是先用drmModeAtomicAlloc分配一个请求对象用drmModeAtomicAddProperty逐个添加属性变更然后调drmModeAtomicCommit提交。属性是KMS里所有可变状态的统一抽象每个对象都有一组属性比如CRTC有ACTIVE、MODE_IDplane有FB_ID、CRTC_ID、SRC_X、SRC_Y、SRC_W、SRC_H、CRTC_X、CRTC_Y、CRTC_W、CRTC_H、rotation等。提示调试atomic提交失败时把DRM_CLIENT_CAP_ATOMIC打开后内核日志会打印具体哪个属性校验失败这是定位问题最快的方式。3.2 属性系统KMS里所有可变状态的统一入口KMS的属性系统是理解atomic接口的关键。每个DRM对象CRTC、plane、connector、encoder都有一组属性属性有类型整型、枚举、blob、bitmask等有取值范围有是否可变的标志。应用层通过属性ID来操作而不是直接调函数。以plane的旋转为例rotation属性是一个bitmask支持的值有DRM_MODE_ROTATE_0、DRM_MODE_ROTATE_90、DRM_MODE_ROTATE_180、DRM_MODE_ROTATE_270以及DRM_MODE_REFLECT_X、DRM_MODE_REFLECT_Y。设置旋转时需要先通过drmModeObjectGetProperties拿到plane的属性列表找到rotation属性的ID然后在atomic请求里添加这个属性的变更。属性系统的好处是统一和可扩展。新增功能只需要加一个属性不需要改API。但代价是应用层代码变复杂了需要先查询属性ID再操作。实际项目中通常会封装一层属性缓存把常用属性的ID缓存起来避免每次都查询。3.3 CRTC与Connector的绑定模式设置的核心步骤模式设置的核心是把CRTC和Connector绑定起来并给CRTC设置一个显示模式。在atomic接口下这个操作对应三个属性变更Connector的CRTC_ID属性设置为目标CRTC的IDCRTC的MODE_ID属性设置为目标模式的blob IDCRTC的ACTIVE属性设置为1。模式mode是一个blob属性包含时钟频率、水平时序hdisplay、hsync_start、hsync_end、htotal、垂直时序vdisplay、vsync_start、vsync_end、vtotal、标志位等信息。这些参数决定了屏幕的刷新率和时序必须和屏幕规格书严格匹配否则会花屏或者不亮。获取模式的方式有两种一是通过Connector的EDID自动获取二是手动构造。对于MIPI DSI屏幕很多模组不带EDID需要驱动里硬编码模式参数或者通过设备树配置。手动构造模式时时序参数的计算要特别注意尤其是消隐区blanking的设置太小会导致信号不稳定太大又浪费带宽。3.4 Plane的配置图层位置、缩放与旋转Plane的配置是KMS里最灵活也最容易出错的部分。一个plane的配置涉及四组坐标SRC_X、SRC_Y、SRC_W、SRC_H源区域指定从framebuffer的哪个位置取多大一块CRTC_X、CRTC_Y、CRTC_W、CRTC_H目标区域指定这块内容显示在屏幕的哪个位置、缩放成多大。这里有个容易踩的坑SRC_W和SRC_H是16.16定点数格式也就是实际像素值左移16位。比如要取100像素宽要写100 16。而CRTC_W和CRTC_H是普通整数。这个格式差异如果不注意会导致取源区域时取错位置显示出来就是花屏或者只显示一部分。旋转是通过rotation属性设置的。但要注意不是所有plane都支持硬件旋转需要先查询plane的rotation属性的有效值范围。如果硬件不支持旋转应用层只能自己旋转像素数据或者用GPU旋转这会增加功耗和延迟。3.5 一次竖屏改横屏的完整调试记录回到我开头提到的竖屏改横屏问题。屏幕是1.8寸TFT LCD分辨率128x160物理上是竖屏。但产品需求是横屏显示也就是要旋转90度。我的第一反应是在应用层旋转像素数据但这样每帧都要CPU搬运功耗高、帧率低。于是转向硬件旋转方案。排查过程是这样的第一步确认plane是否支持旋转。通过drmModeObjectGetProperties查询plane的rotation属性发现有效值包含DRM_MODE_ROTATE_90说明硬件支持。第二步在atomic请求里设置rotation为DRM_MODE_ROTATE_90。提交后屏幕确实旋转了但显示内容被裁剪了——只显示了部分区域。第三步分析裁剪原因。旋转90度后源区域的宽高和目标区域的宽高需要对调。原来竖屏时SRC_W128、SRC_H160旋转后应该变成SRC_W160、SRC_H128同时CRTC_W和CRTC_H也要对调。我一开始没改这些参数导致取源区域时超出了framebuffer边界。第四步修正参数后显示正常但发现刷新率上不去。查驱动发现旋转后的stride对齐要求变了。原来128宽按128字节对齐旋转后160宽需要按256字节对齐否则硬件读取效率低。调整GEM buffer的stride后刷新率恢复正常。这个案例说明KMS的旋转不是简单的转一下它牵扯到源/目标区域的对调、stride的对齐、以及硬件能力的边界。调试时要把这些参数当成一个整体来看改一个就要检查其他几个。4. GEM显存管理的实战细节分配、共享与同步4.1 GEM object的创建与映射dumb buffer与普通buffer的区别GEM创建buffer有两种常见方式dumb buffer和普通GEM buffer。dumb buffer是通过DRM_IOCTL_MODE_CREATE_DUMB创建的它只支持线性格式不支持tiling主要用于简单的framebuffer场景。普通GEM buffer通过驱动的私有ioctl创建支持各种格式和tiling但接口不统一不同驱动不一样。dumb buffer的好处是跨驱动通用坏处是性能一般。对于LCD这种对性能要求不高的场景dumb buffer够用。但如果要做GPU渲染或者视频播放就需要用普通GEM buffer配合dma-buf做零拷贝。创建dumb buffer时需要指定宽、高、bpp每像素位数。内核会根据这些参数计算pitch每行字节数pitch通常会做对齐比如按64字节或256字节对齐。这个对齐值很关键应用层写数据时必须按pitch来算行偏移不能简单用宽乘以bpp除以8否则会错位。映射buffer到用户空间用DRM_IOCTL_MODE_MAP_DUMB得到一个offset然后用mmap映射。映射后应用层可以直接读写像素。但要注意如果buffer同时被硬件扫描输出应用层写入时可能会有撕裂需要配合fence或者双缓冲。4.2 dma-buf跨进程、跨设备共享buffer的标准方案dma-buf是GEM最强大的能力。它允许一个GEM object导出成一个文件描述符这个fd可以传给任何支持dma-buf的驱动或进程。接收方通过dma_buf_attach和dma_buf_map_attachment拿到sg_tablescatter-gather table从而访问同一块物理内存。实际应用场景很多。比如摄像头采集V4L2和显示DRM共享buffer摄像头把数据写入buffer显示直接扫描这个buffer全程零拷贝。再比如GPU渲染和显示共享bufferGPU渲染完直接显示不需要CPU搬运。使用dma-buf时要注意同步问题。生产者和消费者之间需要fence来协调否则消费者可能读到还没写完的数据。DRM的atomic接口支持in-fence和out-fencein-fence表示等这个fence signaled后再提交out-fence表示提交完成后signal这个fence。应用层通过fence实现流水线避免阻塞。4.3 stride与格式对齐花屏问题的常见根源stride也叫pitch是每行像素占用的字节数。由于硬件对齐要求stride通常大于宽 × 每像素字节数。比如128像素宽、RGB565格式每像素2字节理论stride是256字节但硬件可能要求256字节对齐实际stride就是256如果要求512字节对齐stride就是512。应用层写数据时必须按实际stride来算行偏移。如果按理论值算第二行就会写错位置显示出来就是斜的或者花屏。这个问题在分辨率不是对齐值整数倍时特别容易出。获取stride的方式创建dumb buffer后通过DRM_IOCTL_MODE_MAP_DUMB返回的结构里有pitch字段或者通过framebuffer的DRM_FORMAT_MOD_LINEARmodifier查询。不同驱动的对齐要求不同调试时最好打印出来确认。4.4 显存回收与泄漏排查谁在持有bufferGEM object是引用计数的handle关闭、framebuffer销毁、dma-buf释放都会减引用引用归零才真正释放。如果某个环节忘了释放buffer就会泄漏表现为显存越用越少最终分配失败。排查泄漏的方法一是看/sys/kernel/debug/dri/*/gem_names列出当前所有GEM object二是用drm_gem_object_lookup的调试信息三是在驱动里加日志记录每次分配和释放。实际项目里最常见的是framebuffer没销毁就退出导致关联的GEM object一直挂着。注意在atomic接口下framebuffer的销毁要等所有引用它的plane都解除绑定后才能进行否则会返回-EBUSY。正确的顺序是先提交一个把plane的FB_ID设为0的atomic请求再销毁framebuffer。5. 那些文档里不会写的调试经验5.1 屏幕不亮时的分层排查法屏幕不亮是最常见也最让人抓狂的问题。我的排查顺序是自下而上先看背光。背光没开屏幕再正常也是黑的。检查背光GPIO、PWM占空比、背光使能寄存器。很多模组的背光使能和显示使能是分开的容易漏。再看时序。用示波器或者逻辑分析仪量MIPI DSI的时钟和数据线确认有没有信号。如果没有信号说明CRTC没输出问题在KMS配置如果有信号但屏幕不亮可能是时序参数不对或者初始化序列没发。然后看KMS状态。通过/sys/kernel/debug/dri/*/state查看当前CRTC、plane、connector的绑定状态确认ACTIVE是否为1MODE_ID是否有效FB_ID是否非零。最后看GEM。确认framebuffer关联的GEM buffer是否真的分配成功像素数据是否写进去了。可以在应用层读回buffer内容确认数据正确。这个顺序的逻辑是从最底层的硬件信号开始逐层往上确认每一层都排除掉问题自然就定位了。反过来从上往下查容易在应用层绕圈子。5.2 刷新率上不去的几个隐藏原因刷新率上不去表面看是时序问题实际原因可能有很多一是带宽不够。MIPI DSI的lane数和每lane速率决定了总带宽分辨率乘以刷新率乘以bpp就是需要的带宽超过就上不去。计算时要算上消隐区的开销实际带宽需求比有效像素带宽高20%左右。二是内存带宽不够。显示控制器读framebuffer要占内存带宽如果同时有GPU、CPU在抢带宽显示就可能欠载。这种情况下降低其他模块的带宽占用或者提高显示控制器的优先级。三是stride对齐不好。前面提到过stride不对齐会导致硬件读取效率低等效带宽下降。把stride调到对齐值刷新率可能就上去了。四是plane配置过多。多个plane叠加会增加硬件合成负担如果硬件合成器能力有限就会限制刷新率。减少plane数量或者把一些图层交给GPU合成。5.3 亮度调节与DRM的关系为什么不能直接写寄存器LCD亮度调节通常通过PWM控制背光实现。在DRM框架下背光被抽象成backlight设备通过/sys/class/backlight/*/brightness调节。但有些场景下应用层想直接控制亮度绕过sysfs。直接写背光寄存器的问题是绕过了内核的背光管理可能导致状态不一致。比如内核认为亮度是50%实际寄存器被改成了80%下次内核调节时就会跳变。正确做法是通过DRM的connector的backlight属性或者通过标准的backlight接口。如果确实需要在驱动里直接控制建议实现一个backlight设备把寄存器操作封装进去应用层通过标准接口调用。这样既保证了状态一致又提供了灵活性。5.4 多屏场景下CRTC资源的分配策略多屏场景下CRTC是稀缺资源。一块显示控制器可能只有2个CRTC但要驱动3个屏幕就需要时分复用或者动态分配。分配策略上优先保证主屏独占一个CRTC副屏共享或者按需分配。如果副屏不需要同时显示可以在切换时重新绑定CRTC。如果必须同时显示就要看硬件是否支持一个CRTC驱动多个Connector通常不支持除非是镜像模式。镜像模式下多个Connector绑定同一个CRTC显示相同内容。扩展模式下每个Connector需要独立的CRTC。资源不够时只能降低分辨率或者刷新率来节省带宽或者用GPU合成多个屏幕的内容到一个CRTC输出。6. 从框架理解到实际项目我的几点体会把KMS和GEM这套框架啃下来之后我最大的体会是不要把它当成一堆API来记而要理解它解决的问题。KMS解决的是多应用如何共享显示硬件GEM解决的是多设备如何共享显存。理解了这两个问题再看那些对象和属性就顺了。实际项目中我建议先把最小可用的显示链路跑通一个CRTC、一个Connector、一个Plane、一个Framebuffer能显示纯色就行。然后再逐步加功能旋转、缩放、多图层、dma-buf共享。每加一个功能都确认前面的还正常这样出问题时容易定位。调试工具方面modetest是必备的它能列出所有KMS对象和属性还能直接做模式设置测试。/sys/kernel/debug/dri/下的调试文件也很有用能看到当前状态和GEM对象列表。内核日志打开DRM的debug级别能看到atomic提交的详细过程。最后说一个容易被忽略的点不同SoC的DRM驱动实现差异很大。同样是KMS和GEMA平台的plane可能支持硬件旋转B平台就不支持A平台的GEM用CMAB平台用TTM。所以看文档时要看具体平台的通用文档只能告诉你框架细节还得看驱动源码。我调试时养成的习惯是遇到问题先翻驱动源码里对应的atomic_check和atomic_update函数那里有最准确的硬件能力描述。