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驱动的基础。本章将深入剖析这些对象的概念、内核实现方式以及它们如何协同工作完成一帧画面的输出。
上图展示了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_width | int | 支持的最小/最大水平分辨率 |
min_height/max_height | int | 支持的最小/最大垂直分辨率 |
funcs | drm_mode_config_funcs* | 回调函数集(如fb_create、output_poll_changed) |
fb_lock | struct mutex | 保护framebuffer列表的锁 |
connector_list | list_head | 所有connector对象的链表头 |
encoder_list | list_head | 所有encoder对象的链表头 |
crtc_list | list_head | 所有crtc对象的链表头 |
plane_list | list_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);
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;
}
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个模式。
分析过程:
- 检查
drm_connector_init()的type参数,错误地使用了DRM_MODE_CONNECTOR_HDMIA,而实际应为DRM_MODE_CONNECTOR_LVDS。这导致内核在注册时尝试访问HDMI相关的I2C总线,而该总线未初始化。 - 检查
get_modes()实现,发现未调用drm_mode_create()创建mode对象,而是直接操作了一个未初始化的指针。 - 检查
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类型错误上。
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驱动,其对象关系图应该与硬件数据流完全对应。