4. DRM子系统入门
从 Framebuffer(fbdev)迁移到 DRM,本质上不是换一套 API,而是从“一块显存 + 一个固定显示模式”的简单模型,升级为“显存对象 + 显示管线 + 原子提交”的现代图形栈。理解 DRM 的关键,是先分清它内部两条并行的主线:负责显示控制的 KMS(Kernel Mode Setting)和负责显存管理的 GEM(Graphics Execution Manager)。前者决定“画面怎么显示”,后者决定“画面数据放在哪、怎么被引用和共享”。
一、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 与显示共享缓冲区成为可能。
二、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 驱动,把时序、初始化序列、背光控制从板级代码迁移到标准框架中。
常见误区二:在 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 管端口。理解这条链路之后,再看任何具体驱动的移植代码,都能快速定位它到底在配置哪一段。