3、DRM/KMS基础:从框架到实战
各位同学,今天我们聊聊嵌入式图形开发里最核心的一块——DRM/KMS。说实话,我当年刚接触这个的时候,也被那一堆缩写搞得头大。CRTC、Encoder、Connector、Plane……听着像是一堆硬件零件,其实它们就是显示链路里的几个关键角色。
好,咱们不绕弯子,直接开干。
3.1 DRM框架:它到底管什么?
DRM,全称Direct Rendering Manager。说白了,它就是Linux内核里负责图形显示和渲染的管家。我习惯把它理解成两层:
- 上层:给用户空间的程序(比如X11、Wayland、游戏)提供接口,让它们能直接操作GPU。
- 下层:管理显示输出,也就是我们常说的KMS部分。
你想想看,如果没有DRM,每个应用程序都得自己去跟显卡驱动打交道,那不乱套了?DRM就是那个中间人,把复杂的硬件操作封装成标准的ioctl调用。
核心要点:DRM不仅仅是渲染,它还负责显示输出的配置。这两件事在DRM里是分开的——渲染走GEM或TTM,显示走KMS。
我在项目中遇到过一个问题:有个同事把DRM和KMS混为一谈,结果调试显示异常时,一直在渲染那边找原因,折腾了两天。其实问题出在KMS的配置上。嗯,这里要注意,DRM是框架,KMS是它的一部分。
3.2 KMS核心概念:显示链路拆解
KMS,Kernel Mode Setting。名字挺唬人,其实就是内核帮你管显示模式。为什么要有KMS?因为以前显示模式切换是在用户态做的,容易闪屏、撕裂。KMS把这些操作挪到内核里,原子化、安全。
KMS里最核心的四个对象,我画个图给你看:
+--------+ +-----------+ +------------+ +-----------+
| Plane | --> | CRTC | --> | Encoder | --> | Connector |
+--------+ +-----------+ +------------+ +-----------+
| | | |
图像数据 扫描控制 信号编码 物理接口
咱们一个一个说。
3.2.1 CRTC
CRTC,Cathode Ray Tube Controller。名字有历史感,但功能很现代。它负责从内存里读取帧缓冲,生成视频信号。说白了,它就是显示链路的“大脑”,决定什么时候扫描哪一行。
每个CRTC可以绑定一个或多个Plane,但同一时刻只能输出一个视频流。我建议你把它理解成“显示通道”。
3.2.2 Encoder
Encoder,编码器。它把CRTC输出的并行信号转换成某种特定接口的信号。比如LVDS、HDMI、DP、eDP。不同的Encoder对应不同的物理传输协议。
我记得有一次调试一个LVDS屏幕,怎么调都不亮。后来发现是Encoder的驱动没加载,导致信号根本没编码。嗯,这种坑踩过一次就记住了。
3.2.3 Connector
Connector,连接器。它代表物理接口,比如HDMI插座、VGA口、eDP排线。Connector负责检测有没有显示器插上,以及显示器的EDID信息(分辨率、刷新率等)。
你想想看,如果没有Connector,内核怎么知道外接的是4K屏还是1080p屏?全靠它读EDID。
3.2.4 Plane
Plane,平面。这是KMS里比较新的概念。一个Plane就是一层图像数据。你可以把多个Plane叠加起来,实现画中画、光标叠加、视频叠加等效果。
Plane分三种:
- Primary Plane:主平面,每个CRTC必须有一个。
- Overlay Plane:叠加平面,用于视频或UI叠加。
- Cursor Plane:光标平面,硬件光标,不占主平面带宽。
我的经验:在嵌入式设备上,Overlay Plane非常有用。比如做车载仪表盘,可以把导航信息叠加在仪表盘上,而不需要重新合成整个画面。性能提升很明显。
3.3 对象模型:它们怎么连在一起?
这四个对象不是孤立的。它们通过“连接”组成一条完整的显示链路。我习惯用“管道”来理解:
- 应用程序把图像数据写入一个Buffer(内存)。
- 这个Buffer被挂载到一个Plane上。
- Plane把数据喂给CRTC。
- CRTC按行扫描,把数据推给Encoder。
- Encoder编码成特定信号,通过Connector送到显示器。
在代码里,这些对象通过ID来管理。每个对象都有一个唯一的object_id。你可以通过DRM的ioctl来查询和配置它们。
关键点:一个完整的显示链路必须包含:CRTC + Encoder + Connector + 至少一个Plane。缺任何一个,屏幕都不会亮。
3.4 modeset编程实战:点亮你的屏幕
光说不练假把式。咱们直接写一个最简单的modeset程序。目标:打开DRM设备,配置显示模式,在屏幕上画一个纯色。
先看代码框架:
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <drm/drm.h>
#include <drm/drm_mode.h>
#include <xf86drm.h>
#include <xf86drmMode.h>
int main() {
int fd;
drmModeRes *resources;
drmModeConnector *connector;
drmModeEncoder *encoder;
drmModeCrtc *crtc;
drmModeModeInfo *mode;
uint32_t connector_id, encoder_id, crtc_id;
uint32_t fb_id;
struct drm_mode_create_dumb dumb;
struct drm_mode_map_dumb dumb_map;
uint32_t *fbp;
// 1. 打开DRM设备
fd = open("/dev/dri/card0", O_RDWR);
if (fd < 0) {
perror("open card0 failed");
return -1;
}
// 2. 获取资源
resources = drmModeGetResources(fd);
if (!resources) {
fprintf(stderr, "drmModeGetResources failed\n");
return -1;
}
// 3. 找到第一个连接的Connector
for (int i = 0; i < resources->count_connectors; i++) {
connector = drmModeGetConnector(fd, resources->connectors[i]);
if (connector->connection == DRM_MODE_CONNECTED) {
connector_id = connector->connector_id;
break;
}
drmModeFreeConnector(connector);
}
// 4. 获取第一个可用模式
mode = &connector->modes[0];
// 5. 找到匹配的Encoder
encoder = drmModeGetEncoder(fd, connector->encoder_id);
encoder_id = encoder->encoder_id;
// 6. 找到可用的CRTC
crtc_id = encoder->crtc_id;
crtc = drmModeGetCrtc(fd, crtc_id);
// 7. 创建Dumb Buffer(帧缓冲)
memset(&dumb, 0, sizeof(dumb));
dumb.width = mode->hdisplay;
dumb.height = mode->vdisplay;
dumb.bpp = 32;
drmIoctl(fd, DRM_IOCTL_MODE_CREATE_DUMB, &dumb);
// 8. 创建FB(FrameBuffer对象)
drmModeAddFB(fd, dumb.width, dumb.height, 24, 32,
dumb.pitch, dumb.handle, &fb_id);
// 9. 映射到用户空间
memset(&dumb_map, 0, sizeof(dumb_map));
dumb_map.handle = dumb.handle;
drmIoctl(fd, DRM_IOCTL_MODE_MAP_DUMB, &dumb_map);
fbp = mmap(0, dumb.size, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, dumb_map.offset);
// 10. 填充颜色(红色)
for (int y = 0; y < dumb.height; y++) {
for (int x = 0; x < dumb.width; x++) {
fbp[y * (dumb.pitch / 4) + x] = 0x00FF0000; // ARGB
}
}
// 11. 设置CRTC(真正的modeset)
drmModeSetCrtc(fd, crtc_id, fb_id, 0, 0,
&connector_id, 1, mode);
printf("Modeset done! Screen should be red.\n");
sleep(5);
// 12. 清理
munmap(fbp, dumb.size);
drmModeRmFB(fd, fb_id);
drmModeFreeCrtc(crtc);
drmModeFreeEncoder(encoder);
drmModeFreeConnector(connector);
drmModeFreeResources(resources);
close(fd);
return 0;
}
这段代码我简化了很多错误检查,实际项目中每一步都要判断返回值。我曾经因为忘了检查drmModeAddFB的返回值,结果在某个内核版本上程序直接崩溃,查了半天才发现是FB创建失败。
避坑指南:
- 打开设备时,一定要检查权限。很多嵌入式系统默认不允许普通用户访问/dev/dri/card0。
- 创建Dumb Buffer时,bpp和depth要匹配。我习惯用32bpp,depth=24,这样每个像素4字节,方便计算。
- drmModeSetCrtc是原子操作吗?不是。它是一次性设置,但不会保证中间状态不闪。如果需要无闪烁切换,要用Atomic API。
3.5 实战中的常见问题
我总结几个我在项目中踩过的坑:
| 问题 | 原因 | 解决方法 |
|---|---|---|
| 屏幕不亮 | Connector未连接或Encoder不匹配 | 检查connector->connection状态,遍历所有Encoder找到匹配的 |
| 显示花屏 | Dumb Buffer的pitch计算错误 | 使用drmModeAddFB时pitch必须与内核返回的一致 |
| 刷新率不对 | 选择的mode不是显示器支持的 | 遍历connector->modes数组,找到vrefresh最接近目标值的 |
| 光标闪烁 | 没有使用Cursor Plane | 用drmModeSetCursor单独设置光标,不要用主平面画光标 |
调试小技巧:用modetest工具(libdrm自带)可以快速验证硬件链路是否正常。命令:modetest -M <driver_name>。它会列出所有CRTC、Encoder、Connector和Plane的信息。我每次拿到新板子,第一件事就是跑modetest。
3.6 小结
DRM/KMS其实没那么神秘。你只要记住:
- DRM是框架,KMS是显示管理部分。
- 四个核心对象:CRTC(大脑)、Encoder(编码)、Connector(接口)、Plane(图层)。
- modeset的本质就是:创建Buffer → 配置CRTC → 显示。
下一章我们会深入Atomic API,实现无闪烁的显示切换。那个才是真正体现KMS威力的地方。好,今天就到这里,动手试试吧。