第三章:Linux DRM框架入门:DRM/KMS架构、CRTC/Encoder/Connector/Plane概念解析

3.1 为什么我们需要DRM?

做嵌入式多屏开发,绕不开DRM。说白了,DRM就是Linux内核里专门管显示的那一套框架。

我刚开始接触嵌入式显示时,用的是最原始的framebuffer。那时候觉得挺简单——往内存里写像素数据,屏幕上就能显示。但后来项目要求支持双屏异显、视频叠加、旋转缩放,framebuffer就完全不够用了。

DRM(Direct Rendering Manager)的出现,就是为了解决这些问题。它把显示这件事拆成了几个清晰的模块,每个模块各司其职。你想想看,就像搭积木一样,想要什么功能就拼什么模块。

核心要点:DRM分为两大块——KMS(内核模式设置)和GEM(图形执行管理器)。KMS负责显示管线的配置,GEM负责显存管理。我们做多屏驱动,主要跟KMS打交道。

3.2 DRM/KMS架构概览

先看一张我手绘的架构图(嗯,虽然这里没法画图,但我用文字描述一下):

应用程序 (weston, kmscube, 自定义App)
        ↓
    DRM接口 (ioctl)
        ↓
    DRM核心层 (drm_core)
        ↓
    KMS驱动 (CRTC, Encoder, Connector, Plane)
        ↓
    硬件 (GPU, Display Controller, HDMI/DP/LVDS)

用户空间的程序通过/dev/dri/card0这样的设备节点,用ioctl跟内核通信。内核里的DRM核心层负责调度,具体的硬件操作由各个驱动模块完成。

我个人习惯把KMS比作一个「显示流水线」:

  • CRTC:流水线上的核心处理器,负责生成时序信号
  • Encoder:信号编码器,把数字信号转成特定接口格式
  • Connector:物理接口,比如HDMI口、DP口
  • Plane:图层叠加器,负责把多个图层合成到一起

3.3 CRTC——显示控制器的核心

CRTC(Cathode Ray Tube Controller)这个名字有点历史感,但功能很现代。它负责生成显示所需的时序信号——行同步、场同步、像素时钟。

我在项目中遇到过一个问题:客户要求在一块LVDS屏上显示1080p60的视频,但屏幕一直闪。查了半天,发现是CRTC配置的时序参数跟屏幕规格书对不上。后来我仔细核对了drm_display_mode结构体里的每个字段,才搞定。

struct drm_display_mode {
    int clock;       // 像素时钟 (kHz)
    int hdisplay;    // 水平有效像素
    int hsync_start; // 行同步开始
    int hsync_end;   // 行同步结束
    int htotal;      // 水平总像素
    int vdisplay;    // 垂直有效行数
    int vsync_start; // 场同步开始
    int vsync_end;   // 场同步结束
    int vtotal;      // 垂直总行数
    // ... 还有其他字段
};

小技巧:调试CRTC时序时,可以用modetest -M <driver_name>命令查看当前配置的时序参数。我曾经靠这个命令半小时就定位了一个时序不匹配的问题。

3.4 Encoder——信号转换的桥梁

Encoder的作用,就是把CRTC输出的并行数字信号,转换成特定接口需要的格式。

举个例子:

  • HDMI Encoder:把RGB信号加上TMDS编码
  • LVDS Encoder:把并行信号转成差分对
  • eDP Encoder:嵌入式DisplayPort,带压缩功能

你想想看,为什么需要Encoder?因为CRTC输出的信号是「通用」的,而物理接口是「专用」的。Encoder就是中间的翻译官。

我记得有一次调试一个双屏项目,主屏用HDMI,副屏用LVDS。主屏显示正常,副屏一直黑。后来发现是Encoder的possible_crtcs掩码没配好,导致副屏的Encoder绑到了主屏的CRTC上。这种低级错误,排查起来真让人头大。

3.5 Connector——物理世界的接口

Connector代表的是物理连接器。它负责检测设备是否连接、读取EDID信息、管理热插拔事件。

常见的Connector类型:

类型 接口 典型应用
DRM_MODE_CONNECTOR_HDMIA HDMI Type A 电视、显示器
DRM_MODE_CONNECTOR_DisplayPort DP/eDP 笔记本、高端显示器
DRM_MODE_CONNECTOR_LVDS LVDS 嵌入式屏、工业屏
DRM_MODE_CONNECTOR_DSI MIPI DSI 手机、平板

注意:Connector的热插拔检测(HPD)一定要处理好。我曾经遇到一个项目,HDMI热插拔时系统会死机。排查后发现是HPD中断处理函数里调用了drm_kms_helper_hotplug_event(),但没有加锁保护。嗯,这种并发问题,稍不注意就会踩坑。

3.6 Plane——图层的魔法

Plane是DRM/KMS里最灵活的概念。它代表一个独立的显示图层。

每个Plane有自己的:

  • 帧缓冲(framebuffer)
  • 位置(crtc_x, crtc_y)
  • 大小(crtc_w, crtc_h)
  • 透明度(alpha)
  • 旋转/缩放参数

为什么需要Plane?因为硬件支持图层叠加。比如:

  • 主图层显示桌面背景
  • 叠加图层显示视频画面
  • 光标图层显示鼠标指针

硬件会自动把这些图层合成到一起,不需要CPU参与。这比用软件合成效率高得多。

我做过一个项目,需要在视频画面上叠加OSD菜单。如果用软件方式,CPU占用率会飙升到80%。后来改用硬件Plane叠加,CPU占用率直接降到5%以下。这就是Plane的价值。

// 创建一个Plane的示例代码片段
struct drm_plane *plane;
unsigned int possible_crtcs = 1 << 0; // 绑定到CRTC 0

plane = drm_universal_plane_init(dev, &plane_funcs,
                                 possible_crtcs,
                                 &plane_helper_funcs,
                                 formats, num_formats,
                                 NULL, type, NULL);

3.7 它们如何协同工作?

现在我们把四个概念串起来:

  1. 用户空间程序通过DRM ioctl提交一个atomic commit请求
  2. 内核检查所有资源是否可用(CRTC、Encoder、Connector、Plane)
  3. 如果可用,执行状态切换——更新CRTC时序、切换Encoder模式、配置Plane参数
  4. 硬件开始工作:CRTC生成时序 → Encoder编码 → 通过Connector输出到屏幕
  5. Plane负责把多个图层合成后送给CRTC

整个过程是流水线式的。任何一个环节出问题,屏幕都不会正常显示。

避坑指南:我曾经在调试一个四屏拼接项目时,发现其中一块屏总是显示花屏。排查了两天,最后发现是CRTC的vblank中断没有正确注册。没有vblank中断,DRM的atomic commit就无法完成页面翻转。嗯,从那以后,我调试多屏问题时第一件事就是检查vblank中断。

3.8 实战建议

如果你刚开始接触DRM/KMS,我建议你:

  • 先用modetest工具熟悉硬件能力——它能列出所有CRTC、Encoder、Connector、Plane的信息
  • 写一个简单的atomic commit程序,尝试切换显示模式
  • dmesg查看内核日志,DRM驱动会打印很多调试信息
  • 遇到问题时,先确认硬件连接是否正常,再检查软件配置

记住一句话:DRM/KMS的调试,80%的时间花在排查配置错误上,只有20%是真正的硬件问题。把基础概念理解透彻,能帮你省下大量调试时间。

下一章,我们会深入讲解如何编写一个实际的DRM驱动,包括设备树配置、probe流程、以及如何注册CRTC和Plane。到时候我会分享一些我在实际项目中踩过的坑,希望能帮你少走弯路。