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 对象模型:它们怎么连在一起?

这四个对象不是孤立的。它们通过“连接”组成一条完整的显示链路。我习惯用“管道”来理解:

  1. 应用程序把图像数据写入一个Buffer(内存)。
  2. 这个Buffer被挂载到一个Plane上。
  3. Plane把数据喂给CRTC。
  4. CRTC按行扫描,把数据推给Encoder。
  5. 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威力的地方。好,今天就到这里,动手试试吧。