4、显示驱动框架:DRM/KMS框架介绍、Framebuffer、Plane、CRTC、Encoder、Connector

好,咱们今天来啃一块硬骨头——显示驱动框架。说实话,很多做8155开发的朋友,一开始都被这套东西绕晕过。我当年第一次接触DRM/KMS时,看着那一堆英文缩写,心里直犯嘀咕:这玩意儿到底是干嘛的?

其实说白了,DRM/KMS就是Linux内核里负责管理显示的那一套机制。你想想看,8155要驱动三块屏幕,每块屏幕可能分辨率不同、刷新率不同、甚至接口类型也不同。如果没有一个统一的框架来管这事,那代码得写成什么样?

DRM全称是Direct Rendering Manager,KMS全称是Kernel Mode Setting。这两个东西合在一起,就是咱们今天要聊的主角。

4.1 DRM/KMS框架概览

先说说DRM。它最初是为图形加速设计的,后来慢慢把显示控制这部分也收了进来。KMS呢,就是负责显示模式设置的那部分——分辨率、刷新率、颜色深度这些。

我个人习惯把DRM/KMS理解成一个「显示流水线」。数据从应用程序下来,经过内核,最终打到屏幕上。这条流水线上有五个关键角色:

  • Framebuffer:存放像素数据的内存区域
  • Plane:可以叠加的图像层
  • CRTC:负责扫描输出,生成时序信号
  • Encoder:将像素数据编码成特定接口信号
  • Connector:物理接口,比如HDMI、DP、DSI

这五个角色是怎么配合的?我画个简单的流程给你看:

应用程序 → Framebuffer → Plane → CRTC → Encoder → Connector → 显示器

嗯,这里要注意,实际硬件上可能不止一套这样的流水线。8155这种SoC,内部通常集成了多个显示控制器,每个控制器可以独立驱动一块屏幕。

4.2 Framebuffer:像素数据的家

Framebuffer,直译过来就是「帧缓冲」。它本质上是一块内存区域,里面存着每个像素的颜色值。应用程序把画面画到Framebuffer里,显示硬件再从里面读出来显示。

我在项目中遇到过一个问题:Framebuffer的分配位置很讲究。如果分配在DDR里,访问延迟高,影响刷新率。如果分配在SRAM里,空间又不够。8155的解决方案是——用DDR,但通过硬件优化来降低延迟。

创建Framebuffer的典型代码:

struct drm_framebuffer *fb;
uint32_t handles[4], pitches[4], offsets[4];

/* 分配内存并获取handle */
drmModeAddFB(fd, width, height, depth, bpp, stride, &fb_id);

/* 或者用更现代的API */
drmModeAddFB2(fd, width, height, pixel_format,
              handles, pitches, offsets, &fb_id, 0);

这里有个坑:drmModeAddFB只支持有限的像素格式,而drmModeAddFB2支持更多格式。我建议你直接用后者,省得后面踩坑。

4.3 Plane:图层叠加的艺术

Plane,你可以把它理解成一个「透明胶片」。每张胶片上画着一层内容,把它们叠在一起,就是最终画面。

为什么需要Plane?你想想看,车载系统上经常要同时显示导航地图、音乐播放器、倒车影像。如果每次都要把所有内容合成一张图再显示,那CPU/GPU的负担得多重?有了Plane,每个应用只管自己的图层,硬件自动完成叠加。

Plane有三种类型:

类型 用途 典型场景
Primary Plane 主图层,必须存在 桌面背景、主界面
Overlay Plane 叠加图层,可选 视频播放、悬浮窗
Cursor Plane 光标图层 鼠标指针、触摸反馈

我曾经在调试8155双屏显示时,发现副屏的画面总是闪烁。查了半天,原来是副屏的Overlay Plane没有正确设置Z-order。说白了就是图层叠放顺序搞反了,底层画面盖住了上层。

4.4 CRTC:显示时序的指挥官

CRTC,全称Cathode Ray Tube Controller。名字虽然古老,但功能依然核心——它负责生成显示时序信号。

什么是时序信号?就是告诉显示器:「现在该扫描第几行第几列了」。CRTC内部有一个计数器,按照设定的分辨率、刷新率,精确地产生行同步、场同步信号。

在8155上,每个显示接口都对应一个CRTC。比如:

  • CRTC0 → 主屏(LVDS或eDP)
  • CRTC1 → 副屏(HDMI)
  • CRTC2 → 第三屏(DP或DSI)

配置CRTC时,最关键的是mode参数。mode里包含了分辨率、刷新率、时钟频率等信息。我习惯用drmModeGetConnector获取显示器支持的mode列表,然后选一个最合适的。

drmModeConnector *connector = drmModeGetConnector(fd, connector_id);
drmModeModeInfo *mode = &connector->modes[0]; // 选第一个,通常是首选模式

drmModeSetCrtc(fd, crtc_id, fb_id, x, y, &connector_id, 1, mode);

嗯,这里要注意:drmModeSetCrtc这个调用会直接切换显示模式。如果在切换过程中没有处理好Framebuffer的更新,可能会出现短暂的黑屏。我建议你在切换前先准备好新的Framebuffer,然后用drmModePageFlip做无缝切换。

4.5 Encoder:信号转换的桥梁

Encoder,编码器。它的任务是把CRTC输出的并行像素数据,转换成特定接口需要的串行信号。

不同的接口需要不同的Encoder:

  • LVDS Encoder:把RGB信号转成LVDS差分对
  • HDMI Encoder:把RGB信号转成TMDS信号,还要嵌入音频
  • DP Encoder:把RGB信号转成DisplayPort的微包架构
  • DSI Encoder:把RGB信号转成MIPI DSI的差分信号

在8155上,Encoder通常是硬件IP核,驱动里只需要配置几个寄存器就行。但有一个细节容易忽略——Encoder的电源管理。我曾经遇到过一个问题:系统休眠后唤醒,副屏不亮了。查到最后发现是Encoder的时钟没有重新使能。

避坑指南:Encoder的初始化顺序很重要。必须先保证CRTC已经稳定输出时钟,再使能Encoder。否则Encoder可能会锁死,需要复位整个显示控制器。

4.6 Connector:物理接口的管家

Connector,连接器。它代表的是物理接口本身——HDMI插座、DP插座、LVDS排线座等等。

Connector负责检测显示器是否连接、读取EDID信息(显示器参数)、管理热插拔事件。在8155的多屏系统中,每个Connector对应一块屏幕。

获取Connector信息的典型流程:

/* 1. 获取所有Connector */
drmModeRes *res = drmModeGetResources(fd);
for (int i = 0; i < res->count_connectors; i++) {
    drmModeConnector *conn = drmModeGetConnector(fd, res->connectors[i]);

    /* 2. 检查连接状态 */
    if (conn->connection == DRM_MODE_CONNECTED) {
        /* 3. 读取EDID */
        drmModePropertyBlob *edid = get_edid(fd, conn);
        parse_edid(edid);
    }

    drmModeFreeConnector(conn);
}
drmModeFreeResources(res);

这里有个实用技巧:对于固定连接的屏幕(比如LVDS内屏),Connector的状态永远是DRM_MODE_CONNECTED。但对于HDMI外接屏,你需要监听热插拔事件。我一般用drmHandleEvent来处理:

/* 监听热插拔事件 */
fd_set fds;
FD_ZERO(&fds);
FD_SET(fd, &fds);

if (select(fd + 1, &fds, NULL, NULL, NULL) > 0) {
    drmEventContext evctx = {
        .version = DRM_EVENT_CONTEXT_VERSION,
        .vblank_handler = NULL,
        .page_flip_handler = handle_page_flip,
    };
    drmHandleEvent(fd, &evctx);
}

4.7 多屏异显的实战要点

好了,五个角色都介绍完了。咱们回到8155多屏异显的场景,看看它们是怎么配合的。

假设你要驱动三块屏幕:

  1. 主屏(LVDS,1920x1080)
  2. 副屏(HDMI,1280x720)
  3. 第三屏(DSI,800x480)

在DRM/KMS框架下,你需要:

  • 为每块屏幕分配独立的Framebuffer
  • 为每块屏幕配置独立的CRTC
  • 为每块屏幕选择合适的Encoder和Connector
  • 确保每个CRTC的时钟独立配置,互不干扰

我个人的经验是:先让单屏跑通,再逐步增加屏幕。每增加一块屏幕,都要验证时序是否独立、Framebuffer是否隔离、热插拔是否正常。

调试技巧:modetest工具可以快速验证DRM/KMS配置。命令格式:modetest -M msm -c(msm是8155的DRM驱动名)。它会列出所有Connector、CRTC、Plane的信息,非常直观。

最后说一句:DRM/KMS的学习曲线确实陡峭。但一旦你理解了这五个核心概念,8155的多屏异显就不再神秘。说白了,就是给每块屏幕配一套「Framebuffer→Plane→CRTC→Encoder→Connector」的流水线,然后让它们各自独立运行。

下一章,咱们会深入8155的显示控制器硬件,看看这些概念在芯片层面是怎么实现的。