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 架构从传统模式到原子模式的演进路径,以及各阶段引入的关键能力:

DRM 架构演进路径 DRM 架构演进路径 传统模式设置 逐属性设置 全局锁 / 单缓冲 通用模式设置 (KMS) CRTC/Encoder/Connector Page Flip 支持 原子模式设置 事务化提交 VBlank 同步 Linux 2.6.29 Linux 3.17 Linux 4.6+ 原子模式设置是当前 DRM 驱动开发的标准,硬件光标与 VBlank 同步在此架构下获得最佳支持

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 核心组件架构图 drm_device drm_crtc drm_plane (主/光标) drm_encoder drm_connector 数据流向:drm_plane → drm_crtc → drm_encoder → drm_connector → 物理显示器 硬件光标位于 drm_crtc 内部,通过 drm_plane 类型 DRM_PLANE_TYPE_CURSOR 暴露给用户态
硬件光标定位: 在 DRM 框架中,硬件光标通过 drm_plane 的子类型 DRM_PLANE_TYPE_CURSOR 实现。每个 CRTC 最多关联一个光标 Plane,其更新时机必须与 VBlank 中断严格同步,否则会出现光标闪烁或撕裂。

1.3 驱动注册流程

DRM 驱动的注册遵循标准化的生命周期,从设备探测到用户态访问共经历六个关键步骤:

  1. 设备探测 (Probe) — 驱动入口函数被调用,分配 drm_device 实例并初始化硬件资源。
  2. 子对象初始化 — 依次创建 CRTC、Plane、Encoder、Connector 并建立连接拓扑。
  3. 模式配置注册 — 调用 drm_mode_config_init() 注册显示模式配置能力。
  4. 中断注册 — 注册 VBlank 中断处理函数和硬件光标中断(如适用)。
  5. 设备注册 — 调用 drm_dev_register() 将设备暴露给用户空间。
  6. 用户态访问 — 用户态通过 /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() 注册后用户态可立即访问,确保所有子对象已就绪