4、KMS(内核模式设置)基础:CRTC、Encoder、Connector、Plane四大对象概念、drm_mode_config、drm_connector与drm_encoder实现

KMS(Kernel Mode Setting)是现代Linux图形显示栈的核心,它取代了早期仅依赖framebuffer的简单显示模型。在DRM(Direct Rendering Manager)子系统中,KMS将显示输出链路抽象为四个核心对象:CRTC(显示控制器)、Encoder(编码器)、Connector(连接器)和Plane(平面)。理解这四大对象及其在drm_mode_config中的组织关系,是编写DRM驱动的基础。本章将深入剖析这些对象的概念、内核实现方式以及它们如何协同工作完成一帧画面的输出。

DRM/KMS 显示链路对象关系图 用户空间 (libdrm/DRM) 内核 DRM Core struct drm_mode_config (设备级配置中心) Plane (平面) framebuffer + 缩放/裁剪 支持叠加/混合 CRTC (控制器) 扫描/时序生成 模式设置/电源管理 Encoder (编码器) 信号转换 (LVDS/HDMI) 数据格式打包 Connector (连接器) 物理接口 (HDMI/DP) 热插拔检测/EDID 绑定 连接 输出 物理显示设备 LCD Panel / HDMI Monitor 物理链路 关键数据结构 • drm_plane • drm_crtc • drm_encoder • drm_connector • drm_mode_config • drm_framebuffer 数据流: Framebuffer → Plane → CRTC → Encoder → Connector → 显示设备

上图展示了KMS四大对象在drm_mode_config中的组织关系。用户空间通过libdrm与内核DRM Core交互,而内核中的drm_mode_config作为设备级配置中心,管理着所有显示相关对象。Plane负责从framebuffer获取像素数据,CRTC生成显示时序并驱动扫描,Encoder将并行数据转换为串行信号(如LVDS、HDMI),Connector则负责物理连接和热插拔检测。下面我们将逐一深入这些对象的内核实现。

4.1 四大对象核心原理与职责划分

在KMS架构中,每个对象都有明确的职责边界,理解它们的分工是驱动开发的第一步。

  • Plane(平面):代表一个可独立控制的内存扫描源。每个Plane关联一个drm_framebuffer,负责将内存中的像素数据按指定格式(RGB、YUV等)取出,并可进行缩放、裁剪、旋转等操作。现代GPU通常支持多个硬件Plane(主平面、光标平面、视频叠加平面),用于实现无合成器的多图层显示。
  • CRTC(Cathode Ray Tube Controller,显示控制器):是显示链路的核心时序发生器。它负责生成像素时钟、行同步(HSync)、场同步(VSync)信号,并控制扫描顺序。CRTC还管理显示模式(分辨率、刷新率)、色彩深度、伽马校正和电源状态(DPMS)。一个CRTC可以连接多个Encoder,但同一时刻只能激活一条输出链路。
  • Encoder(编码器):负责将CRTC输出的并行像素数据转换为特定接口所需的信号格式。例如,LVDS Encoder将RGB并行数据转换为差分串行信号;HDMI Encoder则进行TMDS编码并嵌入音频数据。Encoder还处理时钟恢复、信号预加重等物理层细节。
  • Connector(连接器):代表物理显示接口,如HDMI、DisplayPort、VGA、eDP等。它负责检测设备是否连接(热插拔)、读取EDID数据(显示器能力信息)、管理电源(如面板背光控制)以及处理HPD(Hot Plug Detect)中断。Connector是驱动与外部世界的“边界”。
核心理解:这四个对象构成了一条完整的显示流水线:Plane从内存取数据 → CRTC控制时序扫描 → Encoder转换信号格式 → Connector物理输出。驱动开发时,需要为每个硬件单元实例化对应的DRM对象,并通过drm_mode_config将它们连接起来。

4.2 drm_mode_config:设备级配置中心

struct drm_mode_config是DRM设备中所有显示相关对象的“总目录”。它由drm_mode_config_init()初始化,并在驱动加载时填充。该结构体包含以下关键成员:

成员类型作用
min_width/max_widthint支持的最小/最大水平分辨率
min_height/max_heightint支持的最小/最大垂直分辨率
funcsdrm_mode_config_funcs*回调函数集(如fb_create、output_poll_changed)
fb_lockstruct mutex保护framebuffer列表的锁
connector_listlist_head所有connector对象的链表头
encoder_listlist_head所有encoder对象的链表头
crtc_listlist_head所有crtc对象的链表头
plane_listlist_head所有plane对象的链表头
num_crtc/num_encoder等int各类对象的数量统计

驱动初始化时,需要依次创建各个对象并调用drm_mode_config_register系列函数将其注册到对应的链表中。例如,在xxx_drm_load()函数中,典型流程为:

// 1. 初始化mode_config
drm_mode_config_init(dev);

// 2. 设置分辨率范围
dev->mode_config.min_width = 320;
dev->mode_config.max_width = 4096;
dev->mode_config.min_height = 240;
dev->mode_config.max_height = 2160;

// 3. 创建并注册Plane
drm_universal_plane_init(dev, plane, possible_crtcs, &plane_funcs, formats, num_formats, NULL, type, NULL);

// 4. 创建并注册CRTC
drm_crtc_init_with_planes(dev, crtc, primary, cursor, &crtc_funcs, NULL);

// 5. 创建并注册Encoder
drm_encoder_init(dev, encoder, &encoder_funcs, DRM_MODE_ENCODER_LVDS, NULL);

// 6. 创建并注册Connector
drm_connector_init(dev, connector, &connector_funcs, DRM_MODE_CONNECTOR_LVDS);
drm_connector_register(connector);
注意:对象创建顺序很重要。必须先创建Plane和CRTC(因为Encoder需要引用CRTC的掩码),再创建Encoder,最后创建Connector(因为Connector需要引用Encoder)。同时,possible_crtcs掩码必须正确设置,否则运行时无法将CRTC与Encoder绑定。

4.3 drm_connector实现详解

struct drm_connector代表物理显示接口,其实现涉及设备检测、模式获取和状态管理。核心回调函数包括:

  • detect():检测显示器是否连接。对于支持热插拔的接口(HDMI/DP),通过读取HPD引脚状态或I2C访问EDID来判断;对于固定面板(eDP/LVDS),通常直接返回connector_status_connected。
  • get_modes():获取显示器支持的模式列表。从EDID中解析出详细时序(DTD)和标准时序,并调用drm_mode_probed_add()添加到connector的mode列表。对于固定面板,需要手动添加唯一的原生模式。
  • mode_valid():验证某个模式是否在当前连接器上有效。例如,检查分辨率是否超过面板物理分辨率,或像素时钟是否超出Encoder能力。
  • atomic_check():在原子提交时验证状态变更的合法性,如检查Plane与Connector的兼容性。

一个典型的LVDS连接器初始化示例:

static const struct drm_connector_funcs lvds_connector_funcs = {
    .detect = lvds_connector_detect,
    .fill_modes = drm_helper_probe_single_connector_modes,
    .atomic_destroy_state = drm_atomic_helper_connector_destroy_state,
    .atomic_duplicate_state = drm_atomic_helper_connector_duplicate_state,
};

static const struct drm_connector_helper_funcs lvds_connector_helper_funcs = {
    .get_modes = lvds_connector_get_modes,
    .mode_valid = lvds_connector_mode_valid,
};

static int lvds_connector_detect(struct drm_connector *connector, bool force)
{
    // 对于固定面板,始终返回已连接
    return connector_status_connected;
}

static int lvds_connector_get_modes(struct drm_connector *connector)
{
    // 添加面板原生模式(例如 1024x600@60Hz)
    struct drm_display_mode *mode = drm_mode_create(connector->dev);
    mode->hdisplay = 1024; mode->vdisplay = 600;
    mode->clock = 51200; // 像素时钟 51.2MHz
    drm_mode_set_name(mode);
    drm_mode_probed_add(connector, mode);
    return 1;
}
经验:对于eDP/LVDS固定面板,detect()应直接返回connector_status_connected,避免不必要的I2C访问。同时,get_modes()必须至少返回一个模式,否则用户空间无法设置显示。

4.4 drm_encoder实现详解

struct drm_encoder负责信号转换,其核心回调较少,但至关重要:

  • atomic_check():验证CRTC与Encoder的连接是否有效,以及模式是否在Encoder支持范围内。
  • atomic_mode_set():在模式切换时调用,用于配置Encoder的硬件寄存器(如设置LVDS输出格式、HDMI的TMDS时钟等)。
  • enable()/disable():在显示链路启用/禁用时调用,控制Encoder的电源和信号输出。

Encoder与CRTC的绑定关系通过possible_crtcs掩码定义。例如,如果Encoder只能连接到CRTC 0和1,则设置possible_crtcs = 0x3。在drm_encoder_init()时传入该掩码。

static const struct drm_encoder_funcs lvds_encoder_funcs = {
    .destroy = drm_encoder_cleanup,
};

static const struct drm_encoder_helper_funcs lvds_encoder_helper_funcs = {
    .atomic_mode_set = lvds_encoder_atomic_mode_set,
    .enable = lvds_encoder_enable,
    .disable = lvds_encoder_disable,
};

static void lvds_encoder_atomic_mode_set(struct drm_encoder *encoder,
                                         struct drm_crtc_state *crtc_state,
                                         struct drm_connector_state *conn_state)
{
    struct xxx_device *priv = to_xxx(encoder->dev);
    struct drm_display_mode *mode = &crtc_state->mode;
    
    // 配置LVDS控制寄存器
    u32 val = readl(priv->base + LVDS_CTRL);
    val |= LVDS_ENABLE;
    val |= (mode->hdisplay << 16) | mode->vdisplay;
    writel(val, priv->base + LVDS_CTRL);
    
    // 设置像素时钟分频
    clk_set_rate(priv->lvds_clk, mode->clock * 1000);
}
实战要点:在atomic_mode_set()中,务必从crtc_state->mode获取当前模式参数,而不是直接读取硬件寄存器。因为原子提交可能包含多个对象的状态变更,只有通过state传递才能保证一致性。

4.5 案例拆解:基于i.MX6ULL的LCD驱动实现

以NXP i.MX6ULL处理器为例,其内部集成了LCDIF控制器(作为CRTC)、一个并行RGB Encoder和一个LVDS Connector。我们来看如何组织这些对象。

硬件背景:i.MX6ULL的LCDIF支持RGB888输出,通过24位并行接口连接外部LVDS桥接芯片(如SN65LVDS83),再驱动1024x600的LVDS面板。系统约束:

  • LCDIF时钟源来自PLL5,最大像素时钟为120MHz
  • LVDS桥接芯片需要I2C配置,且需要额外的使能GPIO
  • 面板原生模式为1024x600@60Hz,像素时钟51.2MHz

初始现象:开发板启动后,LCD无显示,通过串口查看内核日志发现drm_connector_register失败,且get_modes返回0个模式。

分析过程:

  1. 检查drm_connector_init()的type参数,错误地使用了DRM_MODE_CONNECTOR_HDMIA,而实际应为DRM_MODE_CONNECTOR_LVDS。这导致内核在注册时尝试访问HDMI相关的I2C总线,而该总线未初始化。
  2. 检查get_modes()实现,发现未调用drm_mode_create()创建mode对象,而是直接操作了一个未初始化的指针。
  3. 检查possible_crtcs掩码,LCDIF对应CRTC 0,但Encoder初始化时错误地设置为0x0,导致无法绑定。

修复步骤:

// 1. 修正Connector类型
drm_connector_init(dev, connector, &lvds_connector_funcs, DRM_MODE_CONNECTOR_LVDS, NULL);

// 2. 修正get_modes实现
static int lvds_connector_get_modes(struct drm_connector *connector)
{
    struct drm_display_mode *mode;
    mode = drm_mode_create(connector->dev);
    if (!mode)
        return 0;
    mode->hdisplay = 1024;
    mode->vdisplay = 600;
    mode->clock = 51200;
    drm_mode_set_name(mode);
    drm_mode_probed_add(connector, mode);
    return 1;
}

// 3. 修正possible_crtcs
drm_encoder_init(dev, encoder, &lvds_encoder_funcs, DRM_MODE_ENCODER_LVDS, NULL);
encoder->possible_crtcs = 0x1; // 仅连接到CRTC 0

结果对比:修复后,modetest命令能正确列出1024x600模式,weston启动后LCD正常显示。整个调试过程耗时约2小时,主要时间花在排查Connector类型错误上。

风险提示:Connector类型错误不会导致编译失败,但会在运行时引发奇怪的I2C错误。务必根据实际物理接口选择正确的DRM_MODE_CONNECTOR_*类型。同时,possible_crtcs掩码错误会导致“No connectors found”或“failed to bind”错误,且难以从日志中直接定位。

4.6 常见误区与总结复盘

在KMS驱动开发中,以下误区最为常见:

误区后果正确做法
混淆Connector与Encoder的职责在Encoder中处理热插拔检测,导致中断风暴热插拔检测属于Connector,Encoder只负责信号转换
忽略possible_crtcs掩码运行时无法绑定CRTC,显示黑屏根据硬件连接关系正确设置掩码
在get_modes()中返回0用户空间无法获取模式,显示不可用至少返回一个有效模式(固定面板返回原生模式)
直接操作硬件寄存器而非通过state原子提交时状态不一致,出现撕裂始终从crtc_state/connector_state读取参数
忘记调用drm_connector_register()用户空间无法通过sysfs访问connector在初始化完成后显式调用注册函数

复盘总结:KMS四大对象的设计哲学是“关注点分离”——Plane管数据源,CRTC管时序,Encoder管信号格式,Connector管物理连接。驱动开发时,应严格按照这个分工实现回调函数。调试时,优先使用modetest和drm_info工具验证对象注册情况,再深入硬件寄存器调试。记住:一个正确的KMS驱动,其对象关系图应该与硬件数据流完全对应。