3. DRM框架入门:DRM核心概念、CRTC/Encoder/Connector/Plane对象模型

好,咱们今天聊聊DRM框架。说实话,很多刚接触显示驱动的朋友,一上来就被这些缩写给唬住了——CRTC、Encoder、Connector、Plane……听着像是一堆硬件术语的堆砌。其实没那么玄乎。

DRM,全称是Direct Rendering Manager,是Linux内核里负责显示输出的核心框架。我当年第一次接触它的时候,也觉得这玩意儿抽象得很。但后来我发现,你只要搞懂它的对象模型,整个显示链路就清晰了。

3.1 DRM的核心思想

DRM的设计理念,说白了就是「把显示链路拆成几个标准组件」。每个组件负责一件事,组合起来就能完成从显存到屏幕的完整输出。

你想想看,一个显示系统需要做什么?

  • 从显存里读出像素数据
  • 把数据转换成显示器能识别的信号格式
  • 通过物理接口把信号送出去
  • 控制显示器的刷新、时序、分辨率

DRM就把这些步骤拆成了四个核心对象:Plane、CRTC、Encoder、Connector。每个对象对应一个职责,互不重叠。

核心要点:DRM的对象模型,本质上是对显示硬件管线的软件抽象。你写驱动,就是在实现这些对象的回调函数。

3.2 四个核心对象详解

3.2.1 Plane(图层)

Plane是最底层的对象。它代表一个「显示图层」。每个Plane绑定一块显存区域,里面存着像素数据。

我习惯把Plane想象成「一张透明胶片」。你可以叠好几张胶片,每张上面画不同的内容,然后合成最终画面。硬件上,这对应着显示控制器的图层叠加功能。

Plane有几个关键属性:

  • framebuffer:绑定的显存对象,存着实际像素
  • crop:从framebuffer里裁剪哪块区域
  • position:这个图层放在屏幕的哪个位置
  • z-order:图层叠加顺序,谁在上谁在下
// 一个典型的Plane初始化代码片段
struct drm_plane *plane;
unsigned int formats[] = {
    DRM_FORMAT_XRGB8888,
    DRM_FORMAT_ARGB8888,
};

plane = drm_universal_plane_init(dev, &plane_funcs, formats,
                                 ARRAY_SIZE(formats), NULL,
                                 DRM_PLANE_TYPE_PRIMARY, NULL);
if (IS_ERR(plane))
    return PTR_ERR(plane);

我的经验:写Plane驱动时,最容易被坑的是格式列表。我曾经漏掉一个硬件支持的格式,结果上层应用死活刷不出画面。排查了两天才发现是格式枚举没写全。所以,建议你把硬件手册里支持的格式全部列出来,一个别漏。

3.2.2 CRTC(显示控制器)

CRTC是整条显示管线的「大脑」。它负责从Plane里读取像素数据,生成时序信号,然后送给Encoder。

CRTC的核心工作有两件:

  1. 扫描输出:按行按列从framebuffer里读像素,生成视频流
  2. 时序控制:生成VSync、HSync等同步信号,控制刷新率

每个CRTC可以绑定多个Plane,但同一时刻只能有一个CRTC处于激活状态。嗯,这里要注意:有些SoC的显示控制器支持多个CRTC,但每个CRTC只能驱动一个显示通道。

// CRTC的mode_set回调,设置显示模式
static int my_crtc_mode_set(struct drm_crtc *crtc,
                            struct drm_display_mode *mode)
{
    // 设置水平/垂直同步参数
    // 配置像素时钟
    // 设置显示分辨率
    writel(mode->hdisplay, REG_HSIZE);
    writel(mode->vdisplay, REG_VSIZE);
    writel(mode->clock, REG_PIXCLK);
    return 0;
}

避坑指南:我曾经在调试一个平板项目时,发现屏幕一直闪。查了半天,结果是CRTC的时序参数里,前后肩(front porch / back porch)设置反了。硬件手册上写的单位是像素时钟周期,我当成微秒算了。所以,时序参数的单位一定要和硬件手册对齐,差一点都不行。

3.2.3 Encoder(编码器)

Encoder负责把CRTC输出的并行像素数据,转换成某种标准接口的信号格式。比如LVDS、MIPI DSI、HDMI、eDP等等。

说白了,Encoder就是「信号翻译官」。CRTC输出的数据格式是统一的,但不同的显示器需要不同的信号协议。Encoder就干这个转换活。

Encoder有几个关键点:

  • 类型:标识它支持哪种接口(DRM_MODE_ENCODER_LVDS、DRM_MODE_ENCODER_DSI等)
  • 可能的crtcs:这个Encoder可以连接到哪些CRTC
  • 桥接:有些Encoder后面还会接一个bridge芯片,做信号调理

3.2.4 Connector(连接器)

Connector代表物理上的显示接口。比如HDMI插座、eDP排线、MIPI DSI排线等。它是整条管线的终点,直接和显示器打交道。

Connector负责的事情:

  • EDID读取:从显示器读取EDID数据,获取支持的分辨率列表
  • 热插拔检测:检测显示器是否连接或断开
  • 状态查询:返回当前连接状态、物理尺寸、色彩空间等信息
// Connector的get_modes回调,获取显示器支持的分辨率
static int my_connector_get_modes(struct drm_connector *connector)
{
    struct edid *edid;
    int count;

    // 读取EDID
    edid = drm_get_edid(connector, &i2c_adapter);
    if (!edid) {
        // 没有EDID,使用默认模式
        drm_add_modes_noedid(connector, 1920, 1080);
        return 1;
    }

    // 解析EDID,添加所有支持的模式
    count = drm_add_edid_modes(connector, edid);
    kfree(edid);
    return count;
}

3.3 对象之间的连接关系

这四个对象不是孤立的。它们组成了一条完整的显示管线:

Plane → CRTC → Encoder → Connector → 显示器

数据流向是这样的:

  1. Plane从framebuffer里取出像素数据
  2. CRTC按时序扫描这些像素,生成视频流
  3. Encoder把视频流转换成接口信号(比如MIPI DSI差分信号)
  4. Connector把信号送到物理接口上
  5. 显示器收到信号,显示画面

我画了一张图,帮你理解这个关系:

DRM显示管线对象模型 Plane 图层 / Framebuffer 像素数据来源 绑定 CRTC 显示控制器 时序生成 / 扫描 连接 Encoder 信号编码器 LVDS / MIPI / HDMI 输出 Connector 物理接口 HDMI / eDP / DSI 信号传输 显示器 / Panel 最终显示画面 连接 图例说明 Plane - 图层数据源 CRTC - 显示控制器 Encoder - 信号编码 Connector - 物理接口

3.4 对象模型在驱动中的体现

在代码层面,这四个对象对应着四个结构体:

结构体 对应对象 核心回调
struct drm_plane 图层 update_plane, disable_plane
struct drm_crtc 显示控制器 mode_set, enable, disable
struct drm_encoder 编码器 enable, disable
struct drm_connector 连接器 get_modes, detect

写驱动的时候,你只需要实现这些结构体里的回调函数。DRM核心框架会帮你管理对象之间的连接关系、状态切换、事件通知等杂事。

我的建议:刚开始写DRM驱动时,别急着把所有回调都实现。先让Plane和CRTC跑通,能看到画面输出。然后再加Encoder和Connector。一步一步来,出问题了也好定位。

3.5 一个完整的对象注册示例

下面是一个简化的驱动初始化流程,展示了四个对象的注册顺序:

static int my_display_probe(struct platform_device *pdev)
{
    struct drm_device *drm;
    struct drm_plane *primary;
    struct drm_crtc *crtc;
    struct drm_encoder *encoder;
    struct drm_connector *connector;

    // 1. 初始化DRM设备
    drm = drm_dev_alloc(&my_driver, &pdev->dev);
    drm_mode_config_init(drm);

    // 2. 创建Plane(图层)
    primary = create_primary_plane(drm);

    // 3. 创建CRTC(显示控制器)
    crtc = create_crtc(drm, primary);

    // 4. 创建Encoder(编码器)
    encoder = create_encoder(drm, crtc);

    // 5. 创建Connector(连接器)
    connector = create_connector(drm, encoder);

    // 6. 注册所有对象
    drm_dev_register(drm, 0);
    return 0;
}

你看,顺序很清晰:先有数据源(Plane),再有控制器(CRTC),然后是编码器(Encoder),最后是物理接口(Connector)。这个顺序和数据的流向完全一致。

3.6 我踩过的一个坑

最后分享一个我实际遇到的案例。有一次调试一个平板电脑的显示驱动,屏幕死活不亮。用示波器量了MIPI DSI的信号,发现Encoder有输出,但Connector的状态一直是「disconnected」。

查了半天,发现是Connector的detect回调里,我忘了处理「没有EDID」的情况。平板用的MIPI DSI屏幕没有EDID,结果detect返回了connector_status_disconnected。DRM核心一看连接器没连上,直接把整个显示管线关了。

修复很简单:对于没有EDID的屏幕,detect直接返回connector_status_connected,然后在get_modes里添加默认分辨率就行。

记住:Connector的状态检测逻辑,一定要根据你的硬件实际情况来写。不是所有显示器都有EDID,也不是所有接口都支持热插拔检测。别想当然。

好了,DRM的对象模型就讲到这里。这四个对象——Plane、CRTC、Encoder、Connector——是理解整个显示子系统的基石。你只要搞懂了它们各自干什么、怎么连在一起,剩下的就是填回调函数的事了。