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核心代码,管理对象生命周期和状态机
- 硬件驱动层:具体芯片的驱动实现,操作真实寄存器
嗯,这里要注意:我们做驱动适配时,主要打交道的是最下面那层。但不懂上面两层,你连调试信息都看不懂。
四大核心对象: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 → 显示器
数据流向是单向的:
- CRTC从Framebuffer读取像素数据
- CRTC把数据送给Encoder
- 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节点,如果此时其他对象还没准备好,就会出问题。