1. DRM 框架概述
DRM (Direct Rendering Manager) 是 Linux 内核中负责图形显示输出的核心子系统。自 Linux 2.4.xx 引入以来,DRM 经历了从传统模式设置到原子模式设置的架构演进,成为现代嵌入式图形、桌面 GPU 和移动 SoC 显示驱动的标准框架。本章从架构演进、核心组件和驱动注册流程三个维度,建立 DRM 显示驱动的全局视图。
1.1 DRM 架构演进
DRM 架构经历了三个主要阶段,每个阶段都解决了特定的显示管理痛点:
| 阶段 | 内核版本 | 核心特征 | 局限性 |
|---|---|---|---|
| 传统模式设置 (Legacy) | 2.6.29 ~ 3.16 | 逐属性设置、全局锁、单缓冲 | 闪烁、撕裂、无事务回滚 |
| 通用模式设置 (KMS) | 3.17 ~ 4.5 | 独立 CRTC/Encoder/Connector 对象、Page Flip | 非原子提交、状态管理分散 |
| 原子模式设置 (Atomic) | 4.6+ | 事务化提交、状态校验、VBlank 同步 | 驱动适配成本较高 |
课程聚焦: 本课程涉及的硬件光标与 VBlank 同步机制,在原子模式设置下可获得最精确的时序控制和状态一致性。建议新驱动直接基于 Atomic API 开发。
下图展示了 DRM 架构从传统模式到原子模式的演进路径,以及各阶段引入的关键能力:
1.2 核心组件
DRM 框架将显示输出链路抽象为五个核心对象,它们共同构成从帧缓冲区到物理显示器的完整数据管道:
- drm_device — 驱动顶层实例,管理所有子对象、文件操作和 ioctl 分发。
- drm_crtc — 显示控制器,负责扫描输出、生成时序信号(VBlank/HBlank)并驱动硬件光标。
- drm_plane — 图层对象,每个 CRTC 可关联多个 Plane(主平面、光标平面、叠加平面)。
- drm_encoder — 编码器,将 CRTC 输出的像素流转换为物理接口信号(如 HDMI、DSI、LVDS)。
- drm_connector — 连接器,代表物理显示接口,负责 EDID 读取、热插拔检测和模式列表管理。
下图展示了 DRM 核心组件之间的连接关系和数据流向:
硬件光标定位: 在 DRM 框架中,硬件光标通过
drm_plane 的子类型 DRM_PLANE_TYPE_CURSOR 实现。每个 CRTC 最多关联一个光标 Plane,其更新时机必须与 VBlank 中断严格同步,否则会出现光标闪烁或撕裂。
1.3 驱动注册流程
DRM 驱动的注册遵循标准化的生命周期,从设备探测到用户态访问共经历六个关键步骤:
- 设备探测 (Probe) — 驱动入口函数被调用,分配
drm_device实例并初始化硬件资源。 - 子对象初始化 — 依次创建 CRTC、Plane、Encoder、Connector 并建立连接拓扑。
- 模式配置注册 — 调用
drm_mode_config_init()注册显示模式配置能力。 - 中断注册 — 注册 VBlank 中断处理函数和硬件光标中断(如适用)。
- 设备注册 — 调用
drm_dev_register()将设备暴露给用户空间。 - 用户态访问 — 用户态通过
/dev/dri/card0打开设备,发起 ioctl 调用。
以下是一个简化的驱动注册代码示例,展示核心步骤:
static int my_driver_probe(struct platform_device *pdev)
{
struct drm_device *ddev;
int ret;
// 1. 分配 drm_device 实例
ddev = drm_dev_alloc(&my_driver, &pdev->dev);
if (IS_ERR(ddev))
return PTR_ERR(ddev);
// 2. 初始化模式配置
drm_mode_config_init(ddev);
ddev->mode_config.min_width = 640;
ddev->mode_config.max_width = 4096;
// 3. 创建 CRTC、Plane、Encoder、Connector
ret = my_crtc_init(ddev);
if (ret)
goto err_free;
ret = my_plane_init(ddev);
if (ret)
goto err_free;
ret = my_encoder_init(ddev);
if (ret)
goto err_free;
ret = my_connector_init(ddev);
if (ret)
goto err_free;
// 4. 注册 VBlank 中断
ret = drm_vblank_init(ddev, 1); // 1 个 CRTC
if (ret)
goto err_free;
// 5. 注册设备
ret = drm_dev_register(ddev, 0);
if (ret)
goto err_free;
platform_set_drvdata(pdev, ddev);
return 0;
err_free:
drm_dev_put(ddev);
return ret;
}
常见陷阱: 驱动注册顺序不可颠倒。特别是
drm_vblank_init() 必须在 drm_dev_register() 之前调用,否则 VBlank 中断无法正确关联到 CRTC。硬件光标 Plane 的初始化应在 CRTC 初始化之后、Encoder 初始化之前完成,以确保光标层与 CRTC 的时序同步。
下表总结了驱动注册各阶段的关键函数和注意事项:
| 步骤 | 关键函数 | 注意事项 |
|---|---|---|
| 设备探测 | drm_dev_alloc() |
确保私有数据结构在分配时正确初始化 |
| 模式配置 | drm_mode_config_init() |
设置最小/最大分辨率、FB 格式支持 |
| CRTC 初始化 | drm_crtc_init_with_planes() |
关联主 Plane 和光标 Plane |
| VBlank 注册 | drm_vblank_init() |
参数为 CRTC 数量,必须在 register 前调用 |
| 设备注册 | drm_dev_register() |
注册后用户态可立即访问,确保所有子对象已就绪 |