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 操作这些对象的入口。
三、各对象职责与关键参数
理解每个对象的职责边界,能避免在驱动里把逻辑放错位置。下面用表格梳理各对象的核心职责、典型参数和常见操作。
| 对象 | 核心职责 | 关键参数 / 属性 | 典型操作 |
|---|---|---|---|
| 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 属性。
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。
modetest -M <driver> 或 drm_info 查看对象拓扑,确认每个对象的 ID、状态和可用属性,再定位是绑定关系错误、模式不支持,还是 Framebuffer 格式不匹配。
掌握这些对象及其连接关系后,后续无论是写 KMS 驱动、做原子提交,还是把 fbdev 的 fb_ops 映射到 DRM 的 drm_framebuffer 与 drm_crtc 操作,都会有一个清晰的“对象地图”作为参照。