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的核心工作有两件:
- 扫描输出:按行按列从framebuffer里读像素,生成视频流
- 时序控制:生成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 → 显示器
数据流向是这样的:
- Plane从framebuffer里取出像素数据
- CRTC按时序扫描这些像素,生成视频流
- Encoder把视频流转换成接口信号(比如MIPI DSI差分信号)
- 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——是理解整个显示子系统的基石。你只要搞懂了它们各自干什么、怎么连在一起,剩下的就是填回调函数的事了。