3. Framebuffer驱动实战

好,咱们进入DRM框架的核心地带。说实话,很多从FBDEV转过来的朋友,第一次看到DRM的对象模型都会有点懵——怎么这么多结构体?又是CRTC又是Encoder的。别急,我当初刚接触时也花了整整一周才理清楚。今天咱们就把这些概念掰开揉碎,讲明白。

3.1 DRM对象模型:四驾马车

DRM的核心对象模型,说白了就是四个角色:CRTC、Encoder、Connector、Plane。它们共同完成一件事——把内存里的像素数据,变成显示器上你能看到的画面。

我习惯用一个比喻来理解:

  • CRTC 就像投影仪的光机,负责从内存读取数据,生成视频时序信号
  • Encoder 就像信号转换器,把CRTC输出的信号转成显示器能接受的格式(比如HDMI、DP、LVDS)
  • Connector 就是物理接口,你插显示器的那地方
  • Plane 是图层,每个平面可以独立叠加、缩放、旋转

你想想看,这四者之间是怎么连接的?一条典型的显示链路是这样的:

Plane → CRTC → Encoder → Connector → 显示器

数据从Plane(内存中的帧缓冲)流向CRTC,CRTC生成时序后送给Encoder,Encoder转换信号格式,最后通过Connector物理接口输出到显示器。

关键点:一个CRTC可以连接多个Encoder,但同一时刻只能激活一条链路。我曾经在调试双屏扩展时,就是因为没搞清楚CRTC和Encoder的绑定关系,折腾了两天才发现是Encoder的possible_crtc掩码没配对。

3.2 各对象的职责与关键字段

咱们逐个来看,每个对象在代码里长什么样。

3.2.1 CRTC(显示控制器)

CRTC是显示链路的核心。它负责:

  • 从帧缓冲读取像素数据
  • 生成行场同步信号
  • 控制刷新率、分辨率、色彩格式

在代码中,每个CRTC对应一个 drm_crtc 结构体。我建议你重点关注这几个字段:

字段 说明 我的经验
port 设备树端口,用于匹配硬件 配错这个,CRTC根本不会使能
enabled 使能状态 调试时先看这个位
mode 当前显示模式 包含分辨率、刷新率等
funcs 操作函数集 必须实现 set_config 和 gamma_set

3.2.2 Encoder(编码器)

Encoder负责信号格式转换。比如你的CRTC输出的是RGB并行信号,但显示器要的是HDMI信号,中间就需要Encoder来转。

关键字段:

  • possible_crtcs:掩码,表示这个Encoder可以连接到哪些CRTC
  • possible_clones:克隆模式支持
  • encoder_type:类型,如 DRM_MODE_ENCODER_TMDS、DRM_MODE_ENCODER_LVDS

避坑指南:我曾经在移植一个HDMI驱动时,发现Encoder始终无法使能。查了两天,最后发现是 possible_crtcs 掩码写错了——我用的位偏移和硬件实际连接的CRTC索引对不上。记住,这个掩码是按bit位算的,不是按数组下标。

3.2.3 Connector(连接器)

Connector代表物理接口。它负责:

  • 检测显示器是否插入(热插拔检测)
  • 读取显示器的EDID信息
  • 提供可用的显示模式列表

常见的Connector类型:

类型 说明 典型应用
DRM_MODE_CONNECTOR_HDMIA HDMI Type A 电视、显示器
DRM_MODE_CONNECTOR_DisplayPort DisplayPort 笔记本、高端显示器
DRM_MODE_CONNECTOR_LVDS LVDS 嵌入式屏幕
DRM_MODE_CONNECTOR_eDP 嵌入式DP 笔记本内置屏

3.2.4 Plane(平面)

Plane是DRM框架中比较晚引入的概念(从KMS开始)。每个Plane对应一个硬件图层,你可以把不同的内容叠加显示。

Plane有三种类型:

  • Primary Plane:主平面,每个CRTC必须有一个
  • Cursor Plane:光标平面,专门用于鼠标指针
  • Overlay Plane:叠加平面,用于视频播放、UI叠加等

我个人的经验是:如果你的硬件支持多个图层,一定要用Overlay Plane来做视频播放。这样视频数据不用经过CPU合成,直接由硬件叠加,性能提升非常明显。

3.3 drm_device结构体:一切从这里开始

drm_device 是整个DRM驱动的根结构体。你可以把它理解成驱动的大管家,所有核心对象都挂在这个结构体下面。

关键字段一览:

字段 类型 作用
dev struct device * 指向内核设备模型中的device
driver struct drm_driver * 驱动操作函数集
mode_config struct drm_mode_config 模式配置管理,包含所有CRTC/Encoder/Connector/Plane的链表
filelist 链表头 管理所有打开DRM设备的文件句柄
irq_enabled bool 中断是否已使能

初始化 drm_device 的典型代码:

struct drm_device *drm;
int ret;

// 1. 分配drm_device
drm = drm_dev_alloc(&my_driver, &pdev->dev);
if (IS_ERR(drm))
    return PTR_ERR(drm);

// 2. 设置驱动私有数据
drm->dev_private = my_priv;

// 3. 注册设备
ret = drm_dev_register(drm, 0);
if (ret)
    goto err_free;

注意:在调用 drm_dev_register 之前,你必须已经完成了所有CRTC、Encoder、Connector、Plane的初始化。否则注册后用户空间访问时,会发现对象列表不完整,导致崩溃。

3.4 驱动初始化流程:一步一步来

DRM驱动的初始化流程,我总结为五个步骤。每一步都有坑,咱们一个一个说。

  1. 分配并初始化drm_device
    • 调用 drm_dev_alloc 分配结构体
    • 设置 drm_driver 函数集
  2. 初始化模式配置
    • 调用 drm_mode_config_init
    • 设置 min_width、max_width 等限制
  3. 创建核心对象
    • 初始化CRTC:drm_crtc_init
    • 初始化Encoder:drm_encoder_init
    • 初始化Connector:drm_connector_init
    • 初始化Plane:drm_universal_plane_init
  4. 建立对象连接关系
    • 设置Encoder的 possible_crtcs
    • 调用 drm_connector_attach_encoder
    • 设置Connector的 polled 属性(热插拔检测)
  5. 注册设备
    • 调用 drm_dev_register
    • 使能中断

嗯,这里要注意第三步的顺序。我见过不少新手把CRTC和Encoder的初始化顺序搞反,结果在 drm_encoder_init 时因为CRTC还没注册,导致possible_crtcs无法正确设置。记住:先CRTC,后Encoder,再Connector,最后Plane。

3.5 知识结构总览

下面这张图,把DRM对象模型和初始化流程串起来了。我建议你保存下来,写驱动时对照着看。

DRM对象模型与初始化流程 DRM对象模型与驱动初始化流程 drm_device mode_config CRTC Encoder Connector Plane 初始化流程(5步) ① drm_dev_alloc → 分配drm_device ② drm_mode_config_init → 初始化模式配置 ③ 创建核心对象:CRTC → Encoder → Connector → Plane ④ 建立连接:possible_crtcs → attach_encoder ⑤ drm_dev_register → 注册设备,使能中断 执行顺序 数据流向:Plane → CRTC → Encoder → Connector → 显示器

3.6 实战中的几个关键点

最后,分享几个我在实际项目中踩过的坑:

关于中断:DRM驱动通常需要两个中断——VBlank中断和热插拔中断。VBlank中断用于垂直同步,热插拔中断用于检测显示器插拔。我建议你在 drm_dev_register 之后才使能中断,否则设备还没注册,中断来了也没地方处理。

关于内存分配:CRTC和Plane都需要分配帧缓冲。如果你的硬件支持DMA-BUF,尽量用 drm_gem_cma_create 或 drm_gem_shmem_create 来分配。我曾经图省事直接用 kmalloc,结果在4K分辨率下频繁出现画面撕裂——因为内存没有连续物理地址,DMA传输会断断续续。