2. DRM 核心概念:框架组成、CRTC/Encoder/Connector/Plane 模型与设备注册流程

DRM (Direct Rendering Manager) 是 Linux 内核中负责显示输出与图形加速的核心框架。理解 DRM 的组成、对象模型以及设备注册流程,是开发 LCD 控制器驱动的基石。本章将深入剖析 DRM 框架的层次结构、四大核心对象(CRTC、Encoder、Connector、Plane)的职责与协作关系,并详细梳理 DRM 设备的注册与初始化流程。

2.1 DRM 框架组成与层次结构

DRM 框架位于内核图形子系统的最底层,向上提供统一的用户空间接口(通过 /dev/dri/cardX 设备节点),向下对接具体的 GPU 或显示控制器硬件。其整体架构可分为三层:

  • 核心层 (DRM Core):提供框架基础设施,包括设备管理、文件操作、内存管理(GEM/CMA)、对象模型(KMS)以及 IOCTL 分发。
  • 驱动层 (Driver):由具体的硬件厂商或开发者实现,负责填充 struct drm_driver 和 struct drm_mode_config_funcs 等回调函数,并注册 CRTC、Encoder、Connector、Plane 等 KMS 对象。
  • 硬件层 (Hardware):实际的显示控制器、GPU、HDMI/DP 收发器、LCD 面板等物理设备。
关键点: DRM 核心层与驱动层通过 drm_device 结构体连接。驱动层只需关注硬件差异,核心层提供通用逻辑,这种分层设计极大地提高了代码复用性和可移植性。
DRM 框架层次架构图 DRM 框架层次架构 用户空间 (libdrm / Mesa / Weston / Xorg) DRM 核心层 (DRM Core) 设备管理 | GEM/CMA 内存管理 | KMS 对象模型 | IOCTL 分发 DRM 驱动层 (Driver) drm_driver | mode_config_funcs | CRTC/Encoder/Connector/Plane 实现 硬件层 (LCD 控制器 / GPU / HDMI PHY / 面板)

2.2 CRTC / Encoder / Connector / Plane 模型

DRM 的 KMS (Kernel Mode Setting) 子系统采用面向对象的设计,将显示流水线抽象为四个核心对象。它们共同协作,将帧缓冲区(Framebuffer)中的图像数据最终显示在物理显示器上。

对象 结构体 职责描述 硬件对应
CRTC drm_crtc 控制显示时序(行/场同步、像素时钟)、扫描驱动、色彩空间转换、Gamma 校正。是显示流水线的“大脑”。 LCD 控制器核心时序模块、DMA 引擎
Encoder drm_encoder 将 CRTC 输出的并行像素数据编码为特定接口标准(如 RGB、LVDS、MIPI DSI、HDMI、DP)的串行信号。 LVDS 发送器、MIPI DSI 主机控制器、HDMI 编码器
Connector drm_connector 代表物理连接器(HDMI 口、VGA 口、eDP 座子)和显示器。负责检测热插拔、读取 EDID、获取显示模式列表。 HDMI 座子、VGA 座子、eDP 连接器、LCD 面板接口
Plane drm_plane 代表一个独立的图像层(Overlay)。每个 Plane 绑定一个 Framebuffer,CRTC 将多个 Plane 合成后输出。主平面(Primary Plane)是必选的。 硬件图层(如 OSD 层、视频层)、Alpha 混合器
类比理解: 将显示流水线想象成一场电影放映。Plane 是胶片(帧缓冲区),CRTC 是放映机(控制帧率与扫描),Encoder 是信号转换器(将胶片内容转为电信号),Connector 是投影幕布接口(连接屏幕并报告其能力)。
CRTC/Encoder/Connector/Plane 模型与数据流 KMS 核心对象模型与数据流 Plane (图层) 绑定 Framebuffer 支持 Alpha 混合 CRTC 时序生成 (H/V Sync) 像素时钟 (PCLK) Gamma / 色彩管理 Encoder 并行 → 串行 LVDS / MIPI DSI / HDMI Connector EDID 读取 热插拔检测 模式列表 显示器 / 面板 数据流说明: 1. CPU/GPU 将渲染好的图像数据写入 Framebuffer。 2. Plane 从 Framebuffer 中取出像素数据,并传递给 CRTC。 3. CRTC 按照设定的时序(分辨率、刷新率)逐行扫描读取像素,并发送给 Encoder。 4. Encoder 将并行数据编码为串行差分信号(如 LVDS 或 MIPI DSI),通过 Connector 传输到显示器。

2.3 DRM 设备注册流程

在嵌入式 LCD 控制器驱动中,DRM 设备的注册通常遵循一套标准化的流程。以下是以 struct drm_driver 和 struct drm_device 为核心的注册步骤:

  1. 分配 drm_device:调用 drm_dev_alloc() 或 devm_drm_dev_alloc() 分配并初始化一个 drm_device 实例,同时传入驱动私有数据(如寄存器基址、时钟、GPIO 等)。
  2. 设置驱动回调:填充 struct drm_driver 结构体,包括 load()、unload()、open()、postclose()、gem_create_object() 等回调函数。
  3. 初始化模式配置:调用 drm_mode_config_init() 初始化 drm_device->mode_config,并设置 min_width、max_width、funcs 等字段。
  4. 创建 KMS 对象:依次创建并初始化 CRTC、Encoder、Connector、Plane 对象,并调用 drm_crtc_init()、drm_encoder_init()、drm_connector_init()、drm_universal_plane_init() 将它们注册到 DRM 核心。
  5. 建立对象连接:通过 drm_connector_attach_encoder() 将 Connector 与 Encoder 绑定;通过 drm_crtc_init_with_planes() 将 CRTC 与 Primary Plane 和 Cursor Plane 关联。
  6. 注册 DRM 设备:调用 drm_dev_register() 将设备注册到系统,此时会在 /dev/dri/ 下创建设备节点(如 card0、controlD64)。
  7. 注册输出接口:对于嵌入式 LCD 控制器,通常还需要调用 drm_fbdev_generic_setup() 或 drm_fbdev_cma_init() 创建帧缓冲设备(fbdev),提供传统的 /dev/fb0 接口。
常见错误: 在注册 CRTC 之前,必须确保硬件时钟(如 LCD 控制器像素时钟)已经使能并稳定。如果时序生成模块尚未初始化,CRTC 的 mode_set() 回调可能会因访问未就绪的寄存器而崩溃。
DRM 设备注册流程图 DRM 设备注册与初始化流程 1. 分配 drm_device 2. 设置 drm_driver 回调 3. 初始化 mode_config 4. 创建 KMS 对象 (CRTC/Encoder/Connector/Plane) 5. 建立对象连接 (attach) 6. 注册 drm_dev_register() 7. 注册 fbdev (可选)

2.4 关键数据结构与代码骨架

以下是一个典型的嵌入式 LCD 控制器 DRM 驱动的初始化代码骨架,展示了核心对象的创建与注册过程:

#include <drm/drm_device.h>
#include <drm/drm_crtc_helper.h>
#include <drm/drm_encoder.h>
#include <drm/drm_connector.h>
#include <drm/drm_plane_helper.h>
#include <drm/drm_fbdev_generic.h>

/* 驱动私有数据结构 */
struct lcdc_drm_private {
    struct drm_device *drm;
    void __iomem *base;      /* 寄存器基址 */
    struct clk *pclk;        /* 像素时钟 */
    /* ... 其他硬件资源 */
};

/* CRTC 模式设置回调 */
static void lcdc_crtc_mode_set(struct drm_crtc *crtc,
                               struct drm_display_mode *mode,
                               struct drm_display_mode *adjusted_mode)
{
    struct lcdc_drm_private *priv = crtc->dev->dev_private;
    /* 配置 LCD 控制器的时序寄存器:HBP, HFP, VBP, VFP, HSW, VSW, PCLK 分频 */
    writel(0, priv->base + LCDC_TIMING_CTRL);
    /* ... 具体寄存器配置 */
}

/* 驱动 probe 函数 */
static int lcdc_drm_probe(struct platform_device *pdev)
{
    struct lcdc_drm_private *priv;
    struct drm_device *drm;
    struct drm_crtc *crtc;
    struct drm_encoder *encoder;
    struct drm_connector *connector;
    struct drm_plane *primary_plane;

    /* 1. 分配 drm_device 并关联私有数据 */
    priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
    drm = devm_drm_dev_alloc(&pdev->dev, &lcdc_drm_driver,
                             struct lcdc_drm_private, drm);
    priv->drm = drm;
    drm->dev_private = priv;

    /* 2. 初始化 mode_config */
    drm_mode_config_init(drm);
    drm->mode_config.min_width = 320;
    drm->mode_config.max_width = 1920;
    drm->mode_config.min_height = 240;
    drm->mode_config.max_height = 1080;
    drm->mode_config.funcs = &lcdc_mode_config_funcs;

    /* 3. 创建 Primary Plane */
    primary_plane = drm_universal_plane_init(drm, &lcdc_primary_plane,
                                             0, &lcdc_plane_funcs,
                                             lcdc_formats,
                                             ARRAY_SIZE(lcdc_formats),
                                             NULL, DRM_PLANE_TYPE_PRIMARY, NULL);

    /* 4. 创建 CRTC,并关联 Primary Plane */
    crtc = drm_crtc_init_with_planes(drm, &lcdc_crtc, primary_plane,
                                     NULL, &lcdc_crtc_funcs, NULL);
    drm_crtc_helper_add(crtc, &lcdc_crtc_helper_funcs);

    /* 5. 创建 Encoder */
    encoder = drm_encoder_init(drm, &lcdc_encoder, &lcdc_encoder_funcs,
                               DRM_MODE_ENCODER_LVDS, NULL);

    /* 6. 创建 Connector */
    connector = drm_connector_init(drm, &lcdc_connector,
                                   &lcdc_connector_funcs,
                                   DRM_MODE_CONNECTOR_LVDS);
    drm_connector_helper_add(connector, &lcdc_connector_helper_funcs);

    /* 7. 连接 Encoder 与 Connector */
    drm_connector_attach_encoder(connector, encoder);

    /* 8. 注册 DRM 设备 */
    drm_dev_register(drm, 0);

    /* 9. 创建 fbdev 兼容接口 */
    drm_fbdev_generic_setup(drm, 32);

    return 0;
}
注意事项: 在 drm_dev_register() 调用之前,必须确保所有 KMS 对象(CRTC、Encoder、Connector、Plane)已经完成初始化并添加到 DRM 核心的链表中。否则,用户空间通过 IOCTL 查询显示资源时可能会得到不完整的结果。

2.5 本章小结