3. DRM KMS核心概念

从 fbdev 迁移到 DRM KMS,最大的认知门槛不是 API 名字变了,而是显示控制模型从“一块帧缓冲 + 一组寄存器”变成了“一组可枚举、可组合、可原子提交的对象”。理解这些对象以及它们之间的连接关系,是后续写驱动、调显示、做模式设置的基础。

一、DRM 设备:一切对象的容器

DRM 设备(struct drm_device)是内核中一个显示控制器的抽象实例,通常对应一个 PCI 设备、一个平台设备或一个 SoC 显示子系统。它由 drm_dev_alloc() 创建、drm_dev_register() 注册,注册后会在 /dev/dri/ 下生成 card0、renderD128 等字符设备节点。用户空间通过 open("/dev/dri/card0") 拿到一个 DRM 文件描述符,之后所有 KMS 操作都围绕这个 fd 展开。

DRM 设备内部维护着 mode config(模式配置)数据结构,它记录了当前系统中所有的 CRTC、Encoder、Connector、Plane、Framebuffer 以及它们之间的绑定关系。换句话说,DRM 设备是“显示资源的注册表”,而 KMS 对象是这张表里的条目。

二、KMS 对象模型:从像素到屏幕的链路

KMS 的核心对象可以按“数据流方向”串成一条链:Framebuffer 提供像素数据,Plane 把像素搬运到 CRTC 的扫描输出通道,CRTC 负责时序生成与扫描,Encoder 把 CRTC 输出的像素流转换成 Connector 能接受的信号格式,Connector 则代表物理接口(HDMI、DP、DSI、eDP 等)并负责 EDID 读取、热插拔检测和链路训练。

下面这张图展示了 KMS 对象模型以及它们之间的连接关系,同时标出了用户空间通过 ioctl 操作这些对象的入口。

KMS 对象模型与数据流 Framebuffer → Plane → CRTC → Encoder → Connector → 物理屏幕 Framebuffer 像素数据 + 格式 drm_framebuffer Plane 图层搬运 / 缩放 drm_plane CRTC 时序生成 / 扫描输出 drm_crtc Encoder 像素流 → 信号编码 drm_encoder Connector 物理接口 / EDID / HPD drm_connector 物理屏幕 HDMI / DP / DSI / eDP 最终显示画面 Blob 属性 不可变二进制数据 drm_property_blob 用户空间 ioctl 入口 drmModeGetResources() · drmModeGetConnector() · drmModeGetEncoder() · drmModeGetCrtc() · drmModeAddFB2() · drmModeSetCrtc() · drmModeAtomicCommit() 所有 KMS 对象都通过 DRM 文件描述符 + ioctl 访问,对象 ID 由内核分配并返回给用户空间 数据流 属性关联

三、各对象职责与关键参数

理解每个对象的职责边界,能避免在驱动里把逻辑放错位置。下面用表格梳理各对象的核心职责、典型参数和常见操作。

对象 核心职责 关键参数 / 属性 典型操作
Framebuffer 描述一块可扫描输出的像素缓冲 宽、高、像素格式、pitch、GEM handle drmModeAddFB2() / drmModeRmFB()
Plane 把 Framebuffer 的某个区域搬运到 CRTC 扫描通道 src/dst 坐标、格式、旋转、alpha、zpos drmModeSetPlane() / 原子提交
CRTC 生成显示时序,驱动扫描输出 mode、active、gamma、背景色 drmModeSetCrtc() / drmModeSetCursor()
Encoder 把 CRTC 像素流转换为 Connector 可接收的信号 possible_crtcs、possible_clones 由内核在模式设置时自动绑定
Connector 代表物理接口,负责 EDID、HPD、链路训练 connection 状态、modes、EDID blob drmModeGetConnector() / drmModeGetConnectorCurrent()
Blob 属性 存放不可变二进制数据,如 EDID、LUT、模式列表 blob ID、长度、数据指针 drmModeGetPropertyBlob() / drmModeCreatePropertyBlob()

四、Blob 属性:KMS 的“不可变数据容器”

Blob 属性是 KMS 中一个容易被忽略但非常重要的机制。它把那些“不适合用整数或字符串表达”的数据——比如 EDID 原始字节、gamma LUT、HDR 元数据、模式列表——封装成一个不可变对象,并分配一个 blob ID。用户空间通过 drmModeGetPropertyBlob() 读取,通过 drmModeCreatePropertyBlob() 创建,通过 drmModeDestroyPropertyBlob() 释放。

Blob 的“不可变”特性带来两个好处:一是原子提交时可以安全地引用同一个 blob,不必担心数据被中途修改;二是内核可以缓存和复用 blob,减少重复拷贝。在迁移 fbdev 时,如果原来用 fb_var_screeninfo 传递 gamma 或 EDID,现在就应该改用 blob 属性。

迁移提示:fbdev 的 fb_fix_screeninfo 和 fb_var_screeninfo 是“扁平结构体”,而 KMS 把同样的信息拆成了对象属性 + blob。迁移时不要试图把 fbdev 结构体直接映射到某个 KMS 对象,而应该按“数据流链路”重新组织:谁提供像素、谁搬运、谁生成时序、谁输出信号、谁代表接口。

五、案例:一次完整的模式设置调用链

假设我们要在 HDMI 接口上显示 1920×1080 画面,用户空间典型调用顺序如下:

int fd = open("/dev/dri/card0", O_RDWR);

drmModeRes *res = drmModeGetResources(fd);
drmModeConnector *conn = drmModeGetConnector(fd, res->connectors[0]);
drmModeEncoder *enc = drmModeGetEncoder(fd, conn->encoder_id);
drmModeCrtc *crtc = drmModeGetCrtc(fd, enc->crtc_id);

uint32_t fb_id;
drmModeAddFB2(fd, width, height, DRM_FORMAT_XRGB8888,
              handles, pitches, offsets, &fb_id, 0);

drmModeSetCrtc(fd, crtc->crtc_id, fb_id, 0, 0,
               &conn->connector_id, 1, &conn->modes[0]);

这段代码里,drmModeGetResources() 返回所有 KMS 对象的 ID 列表;drmModeGetConnector() 拿到 Connector 的当前状态和可用模式;drmModeGetEncoder() 找到与 Connector 绑定的 Encoder;drmModeGetCrtc() 找到 Encoder 当前使用的 CRTC;drmModeAddFB2() 创建 Framebuffer;最后 drmModeSetCrtc() 把 Framebuffer 绑定到 CRTC 并输出到 Connector。这条链路就是 KMS 模式设置的最小闭环。

六、常见误区与复盘

  • 误区一:把 CRTC 当成“屏幕”。CRTC 只是时序发生器,它不知道背后接的是 HDMI 还是 DSI。真正代表屏幕的是 Connector。调试时如果发现“有 CRTC 但没有输出”,优先检查 Connector 的 connection 状态和 Encoder 绑定关系。
  • 误区二:忽略 Plane 的 zpos 和格式。fbdev 通常只有一个图层,迁移到 KMS 后可能启用多个 Plane。如果 overlay 盖住了主图层,或者格式不匹配导致花屏,需要检查 Plane 的 zpos、src_w/src_h 和像素格式。
  • 误区三:直接修改 blob 数据。Blob 是不可变的,任何“修改”都必须创建新 blob 并重新提交属性。试图原地改写会导致原子提交校验失败或显示异常。
  • 误区四:忘记检查 possible_crtcs。Encoder 和 Connector 都有 possible_crtcs 位掩码,表示它们可以连接到哪些 CRTC。如果选错了 CRTC,drmModeSetCrtc() 会返回 -EINVAL。
排障建议:遇到 KMS 模式设置失败时,按“Connector → Encoder → CRTC → Plane → Framebuffer”的顺序逐级检查。先用 modetest -M <driver> 或 drm_info 查看对象拓扑,确认每个对象的 ID、状态和可用属性,再定位是绑定关系错误、模式不支持,还是 Framebuffer 格式不匹配。

掌握这些对象及其连接关系后,后续无论是写 KMS 驱动、做原子提交,还是把 fbdev 的 fb_ops 映射到 DRM 的 drm_framebuffer 与 drm_crtc 操作,都会有一个清晰的“对象地图”作为参照。