3. DRM架构总览

从 FrameBuffer 迁移到 DRM,第一步不是急着改代码,而是先建立一张“架构地图”。FrameBuffer 的模型非常扁平:一个 /dev/fb0,一块显存,一个固定的分辨率与像素格式,应用通过 mmap 写入像素即可。而 DRM(Direct Rendering Manager)是一个分层的、面向现代显示与 GPU 的子系统,它把“显示控制”和“显存管理”拆成两条主线,再通过统一的设备节点暴露给用户空间。理解这三层关系,后续的驱动移植、模式设置、缓冲区分配才不会迷路。

一、DRM 子系统的整体组成

DRM 在内核中并不是一个单一文件,而是由多个协作模块构成。最上层是 DRM Core,它提供字符设备注册、ioctl 分发、对象生命周期管理、文件私有数据等基础设施。其下分为两大功能域:KMS(Kernel Mode Setting)负责显示管线,GEM(Graphics Execution Manager)负责显存与缓冲区对象管理。再往下是具体硬件的 DRM Driver,例如 i915、amdgpu、rockchip、msm 等,它们实现 CRTC、Encoder、Connector、Plane 以及 GEM 对象的具体操作。此外还有 GEM DMA / SHMEM / VRAM helper 等通用辅助层,帮助驱动快速实现缓冲区分配与映射。

与 FrameBuffer 相比,DRM 的关键变化是:显示资源被抽象为可枚举、可查询、可原子提交的对象,而不是一个隐式的全局帧缓冲。用户空间不再“猜”硬件状态,而是通过属性(property)和原子提交(atomic commit)精确控制。

二、KMS 与 GEM:显示控制与显存管理

KMS 管理的是“画面怎么显示”。它的核心对象包括:

  • CRTC:扫描输出控制器,负责从某个 framebuffer 读取像素并按时序输出,对应显示控制器。
  • Encoder:把 CRTC 的像素流转换为 HDMI、DP、MIPI DSI 等接口信号。
  • Connector:物理连接器,负责探测显示器是否插入、读取 EDID、维护连接状态。
  • Plane:图层,支持叠加、缩放、格式转换,是现代显示管线的核心。
  • Property:每个对象的可配置参数,如 CRTC_X、SRC_W、connector_id。

GEM 管理的是“像素放在哪里”。它用 GEM Object 表示一块可被 GPU/显示控制器访问的缓冲区,提供创建、映射、导出为 dma-buf、设置缓存属性等操作。KMS 的 framebuffer 本质上是对一个或多个 GEM 对象的引用,并附带像素格式、宽高、pitch 等元数据。因此,KMS 与 GEM 的关系可以概括为:GEM 提供内存,KMS 决定这块内存如何被扫描输出。

迁移提示:FrameBuffer 时代应用直接写 /dev/fb0;DRM 时代应用需要先通过 GEM 创建 dumb buffer 或 GPU buffer,再创建 framebuffer,最后通过 KMS 提交到 CRTC。步骤变多,但可控性和性能显著提升。

三、DRM 设备节点与 libdrm 接口关系

DRM 在用户空间暴露的主设备节点是 /dev/dri/card0,它承载 KMS、GEM 以及驱动私有 ioctl。另一个常见节点是 /dev/dri/renderD128,即 render node,用于无显示权限的 GPU 计算与渲染,不涉及 KMS 模式设置。此外还有 /dev/dri/controlD64(较少使用)以及 /dev/fb0 兼容节点(由 fbdev 仿真层提供)。

用户空间一般不直接拼 ioctl,而是使用 libdrm。libdrm 是对 DRM ioctl 的薄封装,提供 drmModeGetResources、drmModeSetCrtc、drmModeAtomicCommit、drmIoctl、drmPrimeHandleToFD 等函数。它把内核的 drm_mode_* 结构体映射为用户空间可用的 API,并处理版本协商、对象 ID 查询、属性枚举等繁琐细节。Mesa、Weston、X Server、KMS 示例程序都构建在 libdrm 之上。

# 查看 DRM 设备与能力
ls -l /dev/dri/
cat /sys/class/drm/card0/device/vendor
modetest -M rockchip -c        # 列出 CRTC
modetest -M rockchip -p        # 列出 Plane
modetest -M rockchip -e        # 列出 Encoder
modetest -M rockchip -c        # 列出 Connector 及模式

# 典型 libdrm 调用顺序(伪代码)
fd = open("/dev/dri/card0", O_RDWR);
res = drmModeGetResources(fd);
crtc = res->crtcs[0];
conn = res->connectors[0];
drmModeSetCrtc(fd, crtc, fb_id, 0, 0, &conn, 1, &mode);

四、内核与用户空间的完整数据流

下面这张图把本节的核心对象放在同一条链路上:应用通过 libdrm 访问 /dev/dri/card0,内核 DRM Core 分发到 KMS 与 GEM,KMS 对象最终驱动显示硬件,GEM 对象则通过 dma-buf 与 GPU、V4L2、Wayland 等子系统共享。理解这张图,就理解了 DRM 迁移的主干。

用户空间 应用程序 Weston / X / 播放器 libdrm ioctl 封装 / 对象枚举 Mesa / EGL GPU 渲染与合成 dma-buf 共享 V4L2 / 相机 / 编码 设备节点层 /dev/dri/card0(KMS+GEM) /dev/dri/renderD128(渲染) /dev/fb0(兼容仿真) 内核空间 DRM Core:字符设备注册 · ioctl 分发 · 对象生命周期 · 文件私有数据 drm_dev_register / drm_ioctl / drm_gem_object KMS:显示管线 CRTC Encoder Connector Plane Property Atomic Commit GEM:显存与缓冲区 GEM Object dumb buffer mmap dma-buf PRIME 导出 cache 属性 open / ioctl drm_ioctl 分发 framebuffer

五、迁移中的常见误区与复盘

第一个误区是“把 DRM 当成 FrameBuffer 的替代品,只换设备节点”。实际上 DRM 的编程模型完全不同:FrameBuffer 是“写内存即显示”,DRM 需要先枚举资源、创建 GEM 对象、创建 framebuffer、再提交模式。第二个误区是混淆 card0 与 renderD128:前者用于显示控制,后者用于渲染,权限与用途不同。第三个误区是忽略原子提交:旧式 drmModeSetCrtc 在复杂场景下容易闪烁,现代驱动应优先使用 atomic API。第四个误区是忘记 dma-buf 的同步:跨设备共享缓冲区时,必须处理 fence 与 cache 一致性,否则会出现撕裂或花屏。

对比项 FrameBuffer DRM/KMS + GEM
设备节点 /dev/fb0 /dev/dri/card0、renderD128
显示控制 固定分辨率与格式 CRTC/Encoder/Connector/Plane 可枚举
显存管理 隐式、单一 framebuffer GEM Object、dumb buffer、dma-buf
用户空间接口 mmap + ioctl libdrm、atomic commit、property
多图层与合成 不支持 Plane 叠加、GPU 合成
跨设备共享 困难 dma-buf / PRIME 原生支持
复盘建议:在动手迁移前,先用 modetest 把 card0 上的 CRTC、Connector、Plane、Property 全部打印出来,对照本节架构图逐项确认。能说清“哪个对象对应哪块硬件、哪个 ioctl 由 libdrm 封装”,迁移就已经成功了一半。

总结来说,DRM 的架构可以记成一句话:DRM Core 是入口,KMS 管显示,GEM 管内存,libdrm 是用户空间的桥,设备节点是两者的交汇点。掌握这五者的关系,后续的驱动移植、缓冲区分配、原子提交与性能调优,都会围绕这张图展开。