Linux DRM框架入门:DRM/KMS架构、drm_framebuffer、drm_crtc

好,咱们正式开始啃DRM这块硬骨头。说实话,我早年做嵌入式显示驱动时,被DRM框架折腾得够呛——文档少、概念绕、示例代码还经常跑不起来。但摸爬滚打几年后,我发现DRM/KMS的设计其实非常优雅。你只要抓住几个核心对象,整个框架就清晰了。

DRM/KMS架构概览

DRM全称是Direct Rendering Manager,直接渲染管理器。它最初是为了解决多个程序同时访问GPU时的冲突问题。后来大家发现显示控制这块也需要统一管理,于是就有了KMS(Kernel Mode Setting)。

说白了,DRM负责渲染,KMS负责显示。两者合在一起,就是Linux图形栈的基石。

核心思想:DRM把显示输出抽象成几个硬件对象,应用程序通过操作这些对象来控制屏幕。你不需要关心底层寄存器怎么配,只需要告诉DRM:我要用哪个buffer、在哪个显示器上、以什么分辨率显示。

我习惯把DRM/KMS架构分成三层:

  • 用户空间层:应用程序或图形库(如Wayland、X11)通过ioctl调用DRM接口
  • 内核框架层:DRM核心代码,管理对象生命周期和状态机
  • 硬件驱动层:具体芯片的驱动实现,操作真实寄存器

嗯,这里要注意:我们做驱动适配时,主要打交道的是最下面那层。但不懂上面两层,你连调试信息都看不懂。

DRM/KMS 三层架构 用户空间层 应用程序 / Wayland / X11 / libdrm 内核框架层 DRM Core / KMS 对象管理 / ioctl 分发 硬件驱动层 具体芯片驱动 / 寄存器操作 / 中断处理 驱动适配主要工作在硬件驱动层,但必须理解框架层的对象模型

四大核心对象:CRTC、Encoder、Connector、Plane

KMS把显示硬件抽象成四个对象。我第一次接触时觉得这玩意儿太绕了,后来画了个实物对照图才明白——其实就是把真实硬件拆开了。

KMS对象 对应硬件 主要职责
CRTC 显示控制器 生成时序信号,从framebuffer读取数据
Encoder 编码器(如HDMI、LVDS发送器) 将CRTC输出的数据编码为特定接口信号
Connector 物理接口(HDMI口、VGA口) 检测连接状态,获取EDID信息
Plane 硬件图层 叠加多个framebuffer,实现合成

举个例子你就明白了。想象一台投影仪:

  • CRTC 就是投影仪的光源和扫描机构——它决定什么时候亮、怎么扫
  • Encoder 是信号转换器——把内部信号转成HDMI格式
  • Connector 就是那个HDMI插座——你插线的地方
  • Plane 相当于画中画功能——可以在主画面上叠加一个小画面

我的经验:调试显示问题时,先确认Connector检测到了显示器,再看Encoder和CRTC是否绑定成功。我曾经花了两天查黑屏问题,最后发现是Connector的hotplug检测没做对。

drm_framebuffer:显示缓冲区

drm_framebuffer是DRM框架中最基础的数据对象。它描述了一块内存区域,里面存放着要显示的画面数据。

我习惯把它理解成「画布」。你往画布上画什么,屏幕就显示什么。

struct drm_framebuffer {
    struct drm_device *dev;
    struct drm_mode_object base;
    const struct drm_framebuffer_funcs *funcs;
    unsigned int width;        // 宽度(像素)
    unsigned int height;       // 高度(像素)
    unsigned int depth;        // 颜色深度
    unsigned int bits_per_pixel;
    unsigned int pitches[4];   // 每行字节数
    unsigned int offsets[4];   // 各平面的偏移
    uint64_t modifier;         // 内存布局修饰符
    struct drm_gem_object *obj[4];
};

这里有几个关键字段要注意:

  • pitches:一行像素占多少字节。不是简单的width * bpp,因为硬件可能有对齐要求
  • modifier:描述内存的tiling布局。不同GPU的tiling方式不同,这个字段就是用来区分它们的
  • obj:指向GEM对象,GEM才是真正管理物理内存的

避坑指南:我曾经在适配一款GPU时,直接用了默认的线性布局。结果显示出来的画面是斜的——每行数据错位了。后来才发现硬件要求64字节对齐,pitches没设对。记住:pitches不一定是width * bpp / 8,一定要查硬件手册的对齐要求。

drm_crtc:显示控制核心

CRTC是KMS里最复杂的对象。它负责从framebuffer读取数据,生成正确的时序信号,然后把数据送给Encoder。

你可以把CRTC想象成一个「数据搬运工」——它按照固定的节奏,一帧一帧地把画面数据从内存搬到显示接口。

struct drm_crtc {
    struct drm_device *dev;
    struct drm_mode_object base;
    struct drm_crtc_funcs *funcs;
    struct drm_framebuffer *primary_fb;  // 当前绑定的framebuffer
    struct drm_display_mode mode;        // 当前显示模式
    bool enabled;                        // 是否启用
    // ... 省略其他字段
};

CRTC的核心操作都在drm_crtc_funcs里:

回调函数 触发时机 典型实现
set_config 应用新的显示配置 配置PLL、时钟、时序寄存器
page_flip 切换framebuffer 更新显示地址寄存器,触发VBlank中断
set_crc_source 调试用 开启CRC校验,用于验证画面完整性
enable_vblank 开启/关闭VBlank中断 操作中断使能寄存器

嗯,这里要特别提一下page_flip。这是双缓冲切换的关键操作。你想想看,如果正在显示第1帧时,你突然把framebuffer地址改了,画面就会出现撕裂。所以page_flip必须等到VBlank期间才能执行。

核心要点:CRTC的page_flip是实现双缓冲无撕裂切换的基石。驱动里必须正确实现VBlank中断处理,并在中断服务函数中更新硬件显示地址。

对象之间的绑定关系

这四个对象不是孤立的。它们通过「管道」的概念连接在一起:

Framebuffer → Plane → CRTC → Encoder → Connector → 显示器

数据流向是单向的:

  1. CRTC从Framebuffer读取像素数据
  2. CRTC把数据送给Encoder
  3. Encoder编码后通过Connector输出到显示器

Plane可以插在Framebuffer和CRTC之间,实现图层叠加。比如视频播放时,视频层和UI层可以分别用不同的Plane,硬件自动合成。

我的建议:刚开始做驱动适配时,先别碰Plane。先把CRTC+Encoder+Connector这条主链路调通,能正常显示画面了,再考虑叠加和合成。一口吃不成胖子。

驱动注册流程

最后,我简单说一下驱动里怎么注册这些对象。这是你写驱动时必经的步骤:

// 1. 注册DRM设备
drm_dev_alloc()
drm_dev_register()

// 2. 创建CRTC
drm_crtc_init_with_planes(dev, crtc, primary, cursor, &funcs, NULL)

// 3. 创建Encoder
drm_encoder_init(dev, encoder, &funcs, DRM_MODE_ENCODER_DPI, NULL)

// 4. 创建Connector
drm_connector_init(dev, connector, &funcs, DRM_MODE_CONNECTOR_HDMIA)
drm_connector_register(connector)

// 5. 绑定关系
drm_mode_connector_attach_encoder(connector, encoder)
// CRTC和Encoder的绑定通过drm_mode_config_reset或set_config完成

这个顺序我建议你背下来。我当年就是搞反了初始化和注册的顺序,导致内核崩溃,查了半天才发现是对象还没初始化就注册了。

注意:Connector的注册必须在所有初始化完成之后。因为注册Connector时,内核会尝试读取EDID、创建sysfs节点,如果此时其他对象还没准备好,就会出问题。