4. DRM子系统入门

从 Framebuffer(fbdev)迁移到 DRM,本质上不是换一套 API,而是从“一块显存 + 一个固定显示模式”的简单模型,升级为“显存对象 + 显示管线 + 原子提交”的现代图形栈。理解 DRM 的关键,是先分清它内部两条并行的主线:负责显示控制的 KMS(Kernel Mode Setting)和负责显存管理的 GEM(Graphics Execution Manager)。前者决定“画面怎么显示”,后者决定“画面数据放在哪、怎么被引用和共享”。

DRM = KMS(显示管线与模式设置) + GEM(显存对象与缓冲区管理) + 统一的用户空间接口(drm ioctl / DRM 设备节点)。

一、DRM 整体架构:从用户空间到硬件的分层

DRM 在 Linux 中表现为 /dev/dri/card0、/dev/dri/renderD128 等字符设备。用户空间通过 libdrm 或直接 ioctl 访问内核 DRM 核心,核心再把请求分发给具体的 KMS 驱动(如 i915、rockchip、sun4i、msm)和 GEM 后端。驱动最终操作 CRTC、Encoder、Connector、Plane 这些硬件抽象对象,把像素送到屏幕上。

与 fbdev 相比,DRM 的分层更清晰:用户空间不直接写显存,而是创建 GEM 对象、提交 framebuffer、配置显示管线;内核负责同步、引用计数、格式校验和原子提交。这种分层让多图层合成、热插拔、多屏输出、GPU 与显示共享缓冲区成为可能。

DRM 分层架构与 KMS/GEM 对象关系 用户空间 libdrm / ioctl Wayland / X / KMS GEM 缓冲区共享 drmModeAtomicCommit 原子提交 内核 DRM 核心 KMS:显示管线与模式设置 CRTC · Encoder · Connector · Plane · Mode GEM:显存对象管理 drm_gem_object · handle · mmap · dma-buf 具体 DRM 驱动 i915 / amdgpu rockchip / sun4i msm / vc4 panel / bridge 驱动 显示硬件 Display Controller MIPI DSI / HDMI LCD Panel Framebuffer 扫描输出

二、KMS 四大对象:CRTC、Encoder、Connector、Plane

KMS 把显示管线拆成四个可独立配置的对象,这是 DRM 最核心的抽象:

  • CRTC(Cathode Ray Tube Controller):代表一个显示控制器通道,负责从 framebuffer 读取像素、按扫描时序输出。它持有当前 mode、framebuffer 和输出位置。
  • Encoder:把 CRTC 产生的像素时序转换成某种物理接口信号,如 MIPI DSI、HDMI、LVDS、eDP。一个 CRTC 可以接多个 Encoder,但同一时刻通常只激活一个。
  • Connector:代表物理输出端口及其连接状态,如 HDMI-A-1、DSI-1、eDP-1。它负责读取 EDID、检测热插拔、暴露可用 mode 列表。
  • Plane:图层抽象,负责把某个 framebuffer 按缩放、裁剪、格式转换后叠加到 CRTC 输出上。主 Plane 通常用于全屏扫描输出,Overlay Plane 用于视频、UI 等叠加层。

四者的关系可以概括为:Plane 提供像素来源,CRTC 负责合成与扫描,Encoder 负责信号编码,Connector 负责物理输出。用户空间通过 drmModeGetResources() 枚举这些对象,再用 drmModeSetCrtc() 或原子提交把它们绑定起来。

三、GEM:显存对象与缓冲区共享

GEM 解决的是“显存怎么分配、怎么被多个模块引用”的问题。用户空间通过 DRM_IOCTL_MODE_CREATE_DUMB 或驱动专有 ioctl 创建 GEM 对象,得到一个 handle;再通过 drmModeAddFB() 把 GEM 对象包装成 framebuffer,供 KMS 使用。GEM 对象支持 mmap 到用户空间、支持 dma-buf 导出为文件描述符,从而在 GPU、显示控制器、视频编解码器之间零拷贝共享。

/* 典型 GEM + KMS 使用流程(伪代码) */
drmModeCreateDumb(drm_fd, width, height, bpp, &create);
drmModeMapDumb(drm_fd, create.handle, create.pitch, &map);
memset(map, 0x00, create.size);              /* 填充画面 */
drmModeAddFB(drm_fd, width, height, 24, 32,
             create.pitch, create.handle, &fb_id);
drmModeSetCrtc(drm_fd, crtc_id, fb_id, 0, 0,
               &connector_id, 1, &mode);

四、用户空间接口与原子提交

传统 KMS 接口是“逐个设置”的:先设 CRTC,再设 Plane,再设属性。这种方式在多图层、多屏场景下容易出现中间状态闪烁。DRM 引入了 Atomic Mode Setting:用户空间把所有变更打包成一个 atomic request,通过 drmModeAtomicCommit() 一次性提交,内核校验通过后统一生效。配合 DRM_MODE_ATOMIC_TEST_ONLY,还可以先测试配置是否合法,再决定是否真正提交。

对比项 Framebuffer(fbdev) DRM/KMS
显示模型 单块显存 + 固定模式 多 Plane + CRTC + Encoder + Connector
显存管理 预留连续内存,mmap 直接写 GEM 对象 + dma-buf 共享
模式设置 启动时固定,运行时难改 运行时动态切换,支持热插拔
多图层 通常不支持 Plane 叠加、缩放、格式转换
提交方式 直接写显存 原子提交,支持 TEST_ONLY
典型接口 /dev/fb0 /dev/dri/card0、renderD128

五、移植场景与常见误区

在从 fbdev 移植到 DRM 时,最常见的场景是:原来板级代码直接配置 LCD 控制器寄存器并映射一块 framebuffer,现在需要改成实现一个 DRM 驱动,注册 CRTC、Encoder、Connector 和 Plane,并通过 drmm_kms_init() 或 drm_dev_register() 暴露给用户空间。对于 MIPI DSI 屏,通常还要配合 panel 驱动和 bridge 驱动,把时序、初始化序列、背光控制从板级代码迁移到标准框架中。

常见误区一:把 DRM 当成“另一个 fbdev”,仍然只创建一个全屏 framebuffer,忽略 Plane 和原子提交,导致多图层和热插拔能力无法发挥。
常见误区二:在 GEM 对象生命周期上出错,framebuffer 还在扫描输出时就释放了 GEM handle,造成花屏或内核报错。
常见误区三:忽略 Connector 的 connection 状态和 EDID 解析,直接硬编码 mode,导致换屏或热插拔后无法显示。
调试建议:先用 modetest -M <driver> 枚举 CRTC/Encoder/Connector/Plane,确认对象拓扑;再用 modetest -s <connector>@<crtc>:<mode> 做最小显示验证;最后再接入自己的应用或合成器。这样可以把“驱动问题”和“用户空间配置问题”快速分离。

掌握 DRM 的关键,不是记住所有 ioctl,而是建立“对象模型 + 数据流”的直觉:GEM 管数据,KMS 管管线,Plane 管图层,CRTC 管扫描,Encoder 管信号,Connector 管端口。理解这条链路之后,再看任何具体驱动的移植代码,都能快速定位它到底在配置哪一段。