1. 课程导入

在嵌入式图形与桌面显示系统中,DRM(Direct Rendering Manager)是 Linux 内核负责显示管线管理的核心子系统。它统一管理显示控制器、CRTC、编码器、连接器、帧缓冲与平面(Plane)等对象,并向用户态提供 KMS(Kernel Mode Setting)接口。对于显示驱动开发者而言,真正决定“画面是否流畅、光标是否跟手、撕裂是否出现”的,往往不是复杂的 3D 渲染,而是光标合成与垂直消隐(VBlank)同步这两件看似基础、却极其关键的事情。

学习目标

  • 理解 DRM/KMS 显示管线的整体框架,明确 CRTC、Plane、Framebuffer、Encoder、Connector 的职责边界。
  • 掌握硬件光标(Hardware Cursor)的寄存器模型、格式约束、位置更新路径与常见限制。
  • 理解 VBlank 中断的产生机制、时间基准意义,以及它与页面翻转(Page Flip)的配合方式。
  • 能够分析光标闪烁、拖影、撕裂、延迟等问题的根因,并给出基于 VBlank 的同步修复方案。
  • 具备阅读与修改 DRM 驱动中光标与 VBlank 相关代码的能力。

DRM 显示驱动开发整体框架

DRM 驱动从上到下大致分为用户态接口层、核心对象层、硬件抽象层与寄存器操作层。用户态通过 libdrm 或 DRM ioctl 提交模式设置与帧缓冲更新请求;内核 DRM 核心维护对象状态并做原子校验;驱动实现层(如 drm_crtc_helper_funcs、drm_plane_helper_funcs)负责把抽象状态翻译为硬件寄存器写入;最终由显示控制器在扫描输出时逐行读取像素。

在这条链路中,光标通常被抽象为一个特殊的 Plane(CURSOR Plane),它拥有独立的格式、尺寸与位置属性;而 VBlank 则是显示控制器每完成一帧扫描后发出的硬件中断,是驱动与用户态进行“帧节拍”对齐的时间锚点。二者结合,才能实现“光标移动不撕裂、页面翻转不闪烁”的显示体验。

DRM 显示管线:硬件光标与 VBlank 同步 用户态应用 libdrm / ioctl DRM 核心 原子提交 / 校验 驱动实现层 CRTC / Plane 回调 显示控制器 寄存器 / 扫描输出 硬件光标 Plane 格式:ARGB8888 / 2bpp 尺寸:64×64 / 128×128 位置:CURSOR_X / Y 寄存器 更新:原子提交或 legacy ioctl VBlank 中断 每帧扫描结束触发 时间基准:帧节拍 驱动回调:drm_crtc_handle_vblank 用户态:drmWaitVBlank 同步策略 光标位置在 VBlank 前提交 Page Flip 绑定 VBlank 事件 避免撕裂与光标拖影 降低输入到显示的延迟 典型时序: 帧 N 扫描 → VBlank 中断 → 驱动处理光标位置更新 → 帧 N+1 扫描时新光标位置生效 若在扫描过程中直接改光标寄存器,会出现光标撕裂或旧位置残留

核心原理:光标与 VBlank 为何必须协同

显示控制器以固定像素时钟逐行扫描输出。假设当前正在扫描第 300 行,此时若驱动直接修改光标位置寄存器,硬件可能在本帧剩余行中使用新位置,导致光标上半部分在旧位置、下半部分在新位置,形成“光标撕裂”。同理,若在扫描过程中切换光标图像缓冲区,也可能出现新旧图像混合的拖影。

VBlank 是扫描完成一帧、即将开始下一帧之间的空闲窗口。在这个窗口内更新光标位置或图像,硬件会在下一帧完整生效,从而保证视觉一致性。因此,驱动通常采用两种策略:一是将光标更新放入原子提交,由 DRM 核心在 VBlank 附近安排生效;二是使用 drm_crtc_arm_vblank_event 将页面翻转与 VBlank 事件绑定,用户态通过 drmWaitVBlank 获得节拍反馈。

关键结论:光标位置更新不是“越快越好”,而是“越准越好”。准,指与 VBlank 对齐;快,指在 VBlank 窗口内尽早完成寄存器写入。

方法流程:一次安全的光标更新

  1. 用户态计算新的光标坐标,准备新的光标图像(若需要)。
  2. 通过 DRM 原子接口提交 Plane 更新,设置 DRM_MODE_ATOMIC_NONBLOCK 或等待 VBlank。
  3. DRM 核心校验格式、尺寸、位置是否越界,并检查 CRTC 是否处于激活状态。
  4. 驱动在 atomic_update 或 update_plane 中写入光标寄存器。
  5. 若使用非阻塞提交,驱动在 VBlank 中断中完成事件通知,用户态收到 flip 完成。
  6. 若光标移动频繁,可启用硬件光标异步更新,但仍需保证不跨越 VBlank 边界。
/* 简化示意:在 VBlank 回调中处理光标事件 */
static void my_crtc_handle_vblank(struct drm_crtc *crtc)
{
    struct my_crtc *my = to_my_crtc(crtc);

    if (my->cursor_pending) {
        my_cursor_program(my, &my->cursor_state);
        my->cursor_pending = false;
    }

    drm_crtc_handle_vblank(crtc);
}

案例与场景

在车载仪表盘中,指针光标需要以 60fps 平滑旋转。若驱动在扫描过程中直接写光标坐标,指针边缘会出现明显锯齿与撕裂。将更新放入 VBlank 后,指针运动连续且无撕裂。在远程桌面场景中,鼠标移动事件频率高但显示刷新率固定,若每次事件都立即提交,会造成大量无效寄存器写入;通过 VBlank 合并更新,可显著降低功耗与总线占用。

场景 常见问题 同步策略 关键参数
桌面鼠标 光标拖影、延迟 VBlank 对齐更新 刷新率 60Hz
游戏全屏 画面撕裂 Page Flip + VBlank 事件 双缓冲 / 三缓冲
车载仪表 指针抖动 硬件光标 + 原子提交 ARGB8888 64×64
远程桌面 更新过于频繁 VBlank 合并更新 脏矩形 + 节流

误区与复盘

误区一:认为“硬件光标一定比软件光标快”。实际上,若硬件光标格式不支持或尺寸受限,驱动可能回退到软件合成,反而增加 GPU 负担。

误区二:认为“VBlank 中断越早处理越好”。中断处理应尽量短小,复杂逻辑应放到工作队列或原子提交的后续阶段,避免阻塞其他中断。

误区三:忽略多 CRTC 场景。多屏输出时,每个 CRTC 有独立 VBlank,光标更新必须绑定到对应 CRTC,否则会出现跨屏不同步。