4. DRM驱动架构

从 fbdev 迁移到 DRM KMS,最核心的变化不是“换了一组 API”,而是驱动被拆成了职责清晰的三层:drm_driver 描述“这个驱动能做什么”,drm_device 表示“这个显示控制器实例当前是什么状态”,而 GEM/TTM 负责“显存和缓冲区怎么分配、怎么搬移”。理解这三者的分工,是后续写 modeset、写 plane、写 atomic 提交的基础。

一、核心原理:drm_driver 与 drm_device 的分工

struct drm_driver 是驱动能力的静态描述,通常在模块加载时注册,包含 name、driver_features、major/minor、fops,以及 GEM 相关的 dumb_create、gem_create_object、prime_handle_to_fd 等回调。它更像一张“能力清单”,生命周期贯穿整个模块。

struct drm_device 则是运行期实例,由 drm_dev_alloc() 创建,内部挂载了 mode_config、GEM 私有对象、IRQ、文件句柄列表等。一个 drm_driver 可以对应多个 drm_device(多显示控制器场景),但每个 drm_device 只属于一个 drm_driver。迁移时最常见的错误,就是把本该放在 drm_device 私有结构里的状态(如寄存器基址、时钟、电源域)错误地塞进全局变量,导致多实例互相污染。

迁移要点:fbdev 的 fb_info 同时承担“能力描述”和“运行状态”两种角色;DRM 把它们拆成 drm_driver 与 drm_device,因此驱动私有数据应通过 drm_dev_priv() 或自定义结构体从 drm_device 派生。

二、GEM 与 TTM:内存管理的两条路线

GEM(Graphics Execution Manager)是 DRM 的通用缓冲区对象模型,核心对象是 struct drm_gem_object,它描述一块可被 GPU/显示控制器引用的内存,并带有 handle、size、refcount 等属性。GEM 本身不规定内存从哪里来,只规定“如何被引用和共享”。

TTM(Translation Table Manager)则进一步解决“内存在哪里、如何迁移”的问题,适合有独立显存、需要 VRAM/GTT 分域管理的 GPU。对于大多数嵌入式显示控制器,往往采用 GEM + DMA-BUF + CMA 的轻量方案,而不必引入完整 TTM。选择哪条路线,取决于硬件是否有独立显存、是否需要动态迁移。

  • GEM + CMA:适合集成显示控制器,缓冲区来自 CMA,物理连续,实现简单。
  • GEM + shmem:适合无独立显存、以系统内存为主的场景,配合 DMA-BUF 导出。
  • TTM:适合独立显存 GPU,支持 VRAM/GTT 迁移、放置策略和 eviction。

无论哪条路线,迁移时都要实现 dumb_create,因为它是 DRM 通用用户空间(如 modetest、Xorg)创建缓冲区的入口。下面是一个典型的 GEM CMA 实现骨架:

static int mydev_gem_create(struct drm_device *dev,
                            struct drm_file *file,
                            struct drm_mode_create_dumb *args)
{
    struct drm_gem_object *obj;
    struct mydev_bo *bo;
    int ret;

    args->pitch = args->width * 4;
    args->size  = args->pitch * args->height;

    obj = drm_gem_cma_create(dev, args->size);
    if (IS_ERR(obj))
        return PTR_ERR(obj);

    ret = drm_gem_handle_create(file, obj, &args->handle);
    drm_gem_object_put(obj);
    return ret;
}

三、模式设置流程与原子提交机制

传统 fbdev 的 modeset 是“直接改寄存器”,而 DRM KMS 把它抽象成对象模型:CRTC、Encoder、Connector、Plane、Framebuffer。模式设置的本质,是把这些对象按一条链路连接起来,并让 CRTC 按指定 mode 扫描出 Plane 上的 Framebuffer。

非原子路径使用 drm_crtc_helper_funcs 的 mode_set、commit 等回调,逐步设置,容易产生中间闪烁和竞态。原子提交(Atomic Modeset)则把一次显示更新打包成 drm_atomic_state,先 check 再 commit,保证要么全部生效,要么全部不生效。

原子提交的关键回调位于 struct drm_crtc_helper_funcs 与 struct drm_plane_helper_funcs:

  • atomic_check:校验资源、带宽、格式是否可满足,不写寄存器。
  • atomic_begin:提交前准备,如关闭中断、锁定寄存器。
  • atomic_update:真正写寄存器,更新 Plane 地址、格式、位置。
  • atomic_flush:提交后收尾,如重新使能中断、触发扫描。

用户空间通过 DRM_IOCTL_MODE_ATOMIC 一次性提交多个对象的属性变更,内核用 drm_atomic_commit() 完成状态交换。迁移时若只实现非原子路径,虽然能点亮屏幕,但无法支持多 Plane 叠加、页面翻转和原子测试,后续扩展会非常困难。

DRM 驱动架构:静态能力、运行实例与内存管理 drm_driver(静态能力) name / driver_features fops / major / minor dumb_create / gem_create_object prime_handle_to_fd drm_device(运行实例) mode_config / crtc / plane connector / encoder GEM 私有对象 / IRQ file_priv 列表 GEM / TTM(内存管理) drm_gem_object handle / size / refcount CMA / shmem / VRAM DMA-BUF 共享 原子提交(Atomic Modeset)流程 用户空间 MODE_ATOMIC atomic_state 收集属性变更 atomic_check 校验资源与格式 atomic_commit 交换状态并生效 CRTC / Plane 回调执行顺序 atomic_begin 准备寄存器 atomic_update 写地址/格式 atomic_flush 触发扫描 vblank 事件通知

四、案例与误区复盘

某 SoC 显示驱动从 fbdev 迁移时,工程师先实现了 drm_simple_display_pipe,屏幕能亮,但 modetest 报 “atomic check failed”。排查发现 atomic_check 中未校验 fb->format 与硬件支持的格式是否匹配,导致不支持的格式被放行到 atomic_update,寄存器写入非法值。修复方式是在 check 阶段返回 -EINVAL,并补充格式白名单。

另一个常见误区是把 GEM 缓冲区当成“永远物理连续”。在 CMA 方案下确实连续,但一旦引入 IOMMU 或 TTM,缓冲区可能被迁移,驱动必须使用 dma_addr_t 并正确处理 dma_buf_map,不能假设 virt_to_phys() 的结果可直接写寄存器。

对象职责迁移时常见错误
drm_driver静态能力与回调注册把运行状态写进全局变量
drm_device运行实例与对象树未用 drm_dev_priv 派生私有数据
GEM缓冲区对象与 handle假设缓冲区永远物理连续
TTM显存迁移与放置策略在无独立显存平台上强行引入
Atomic打包校验与提交只实现非原子路径,无法扩展
调试建议:先用 modetest -M <driver> 验证基本 modeset,再用 modetest -M <driver> -a 查看 atomic 能力;若 check 失败,打开 drm.debug=0x1e 可看到每个对象的校验细节。
注意:原子提交的 atomic_check 必须是无副作用的纯校验函数,任何寄存器写入都应放在 atomic_update 中,否则一旦 check 失败回滚,硬件状态会与软件状态不一致。

总结来看,drm_driver 定义能力,drm_device 承载实例,GEM/TTM 管理内存,而原子提交把模式设置从“逐步写寄存器”升级为“先校验后提交”的事务模型。迁移时只要抓住这四条主线,就能把 fbdev 的线性思维平滑地转换为 DRM KMS 的对象化思维。