4. DRM 驱动模型:DRM 驱动注册流程与 struct drm_driver 结构体详解
DRM 驱动模型是 Linux 显示子系统从 Framebuffer 演进到现代图形栈的核心。与传统的 fbdev 驱动不同,DRM 驱动需要遵循一套标准化的注册流程,并通过 struct drm_driver 结构体向内核框架暴露驱动能力。本节将深入剖析 DRM 驱动的初始化路径与关键数据结构。
4.1 DRM 驱动注册全流程
一个典型的 DRM 设备驱动从硬件探测到用户空间可见,需要经历以下关键阶段:
- 平台设备匹配:驱动通过
platform_driver或pci_driver与硬件设备树/ACPI 表匹配,触发probe()回调。 - 核心数据结构分配:调用
drm_dev_alloc()分配struct drm_device,并初始化struct drm_driver回调表。 - 硬件资源初始化:映射 I/O 内存、注册中断、初始化 CRTC/Encoder/Connector/Plane 等显示管道组件。
- DRM 设备注册:调用
drm_dev_register()将设备注册到 DRM 核心,创建设备节点/dev/dri/cardX。 - fbdev 兼容层(可选):通过
drm_fbdev_generic_setup()或自定义 fbdev 模拟层,为遗留应用提供/dev/fbX接口。
移植经验: 从 fbdev 迁移到 DRM 时,
probe() 函数是重写工作量最大的部分。原 fbdev 驱动中的 xxxfb_probe() 需要拆解为 DRM 的 bind() 回调,并将显存管理从 fb_info 迁移到 drm_gem 对象。
4.2 struct drm_driver 结构体详解
struct drm_driver 是 DRM 驱动的“身份证”与“功能清单”,它定义了驱动名称、支持的 IOCTL、内存管理方式、GEM 操作、fops 文件操作等核心回调。该结构体在 include/drm/drm_drv.h 中定义,是每个 DRM 驱动必须填充的入口。
4.2.1 核心成员分类
| 分类 | 成员名称 | 说明 | 移植注意事项 |
|---|---|---|---|
| 基本信息 | driver_feature |
DRM 功能标志位掩码,如 DRIVER_GEM、DRIVER_MODESET |
必须包含 DRIVER_GEM 和 DRIVER_MODESET |
| 基本信息 | name / desc |
驱动名称与描述字符串 | 与设备树 compatible 属性保持一致 |
| 文件操作 | fops |
指向 struct file_operations,定义 open/release/ioctl/mmap |
通常使用 drm_driver_fops 模板 |
| GEM 操作 | gem_create_object |
创建 GEM 对象回调,用于分配显存 | fbdev 驱动需新增 GEM 分配逻辑 |
| GEM 操作 | gem_prime_import_sg_table |
DMA-BUF 导入回调,用于多设备共享显存 | 嵌入式平台需实现 dma_alloc_attrs |
| 显示管道 | irq_handler |
中断处理函数,处理 VBLANK 等事件 | 原 fbdev 中断逻辑可直接复用 |
| 显示管道 | get_vblank_timestamp |
获取 VBLANK 时间戳,用于页面翻转同步 | 硬件需支持精确时间戳寄存器 |
| 调试支持 | debugfs_init |
初始化 debugfs 调试接口 | 可选,但强烈建议实现 |
4.2.2 典型驱动实例代码
以下是一个嵌入式 DRM 驱动的 struct drm_driver 填充示例,展示了从 fbdev 移植时的关键适配点:
static const struct drm_driver xxx_drm_driver = {
.driver_features = DRIVER_GEM | DRIVER_MODESET | DRIVER_ATOMIC,
.name = "xxx_display",
.desc = "XXX SoC Display Controller DRM Driver",
.date = "20250101",
.major = 1,
.minor = 0,
/* GEM 相关回调 */
.gem_create_object = xxx_gem_create_object,
.gem_prime_import_sg_table = xxx_gem_prime_import_sg_table,
.dumb_create = xxx_dumb_create,
.dumb_destroy = xxx_dumb_destroy,
.dumb_map_offset = xxx_dumb_map_offset,
/* 文件操作 */
.fops = &xxx_drm_fops,
/* 中断与 VBLANK */
.irq_handler = xxx_irq_handler,
.get_vblank_timestamp = xxx_get_vblank_timestamp,
/* 调试 */
.debugfs_init = xxx_debugfs_init,
};
移植提示: 从 fbdev 迁移时,
dumb_create 回调对应原 fbdev 中的 fb_ops.fb_alloc_cmap 和显存分配逻辑。DRM 的 dumb buffer 是用户空间通过 IOCTL 分配的简单帧缓冲,必须保证物理连续(对于不支持 MMU 的嵌入式设备尤为重要)。
4.3 DRM 设备注册核心 API
驱动在 probe() 函数中需要按顺序调用以下 API 完成注册:
drm_dev_alloc(&xxx_drm_driver, &pdev->dev):分配并初始化struct drm_device,绑定驱动结构体。dmm_xxx_init(drm):自定义函数,初始化显示控制器硬件、创建 CRTC/Encoder/Connector/Plane。drm_dev_register(drm, 0):将设备注册到 DRM 子系统,生成/dev/dri/card0节点。drm_fbdev_generic_setup(drm, 32):创建 fbdev 兼容层,32 表示 bpp。
4.4 移植适配关键点
常见陷阱:
- driver_features 遗漏:未设置
DRIVER_MODESET会导致 modeset 功能不可用,用户空间无法设置显示模式。 - fops 未正确赋值:直接使用
drm_driver_fops模板即可,不要自定义unlocked_ioctl,否则会破坏 DRM 核心的 IOCTL 分发机制。 - GEM 对象未实现:即使只做简单 framebuffer,也必须实现
dumb_create,否则modetest等工具无法分配显存。 - 中断处理不完整:VBLANK 中断必须调用
drm_crtc_handle_vblank(),否则页面翻转会卡死。
4.4.1 从 fbdev 到 DRM 的成员映射
| fbdev 成员 | DRM 对应成员 | 迁移要点 |
|---|---|---|
fb_ops.fb_setcolreg |
drm_mode_config_funcs.fb_create |
颜色表管理由 DRM 核心接管,驱动只需提供 framebuffer 创建 |
fb_ops.fb_pan_display |
drm_crtc_funcs.page_flip |
页面翻转通过 atomic 或 legacy page_flip 实现 |
fb_var_screeninfo |
drm_display_mode |
显示时序参数结构体不同,需转换 |
fb_info.screen_base |
drm_gem_object.vaddr |
显存映射从固定地址改为 GEM 对象管理 |
fb_info.fix.smem_start |
drm_gem_object.dma_addr |
物理地址通过 GEM 的 dma_buf 获取 |
4.5 注册流程代码骨架
以下是一个完整的 probe() 函数骨架,展示了 DRM 驱动注册的标准写法:
static int xxx_drm_probe(struct platform_device *pdev)
{
struct drm_device *drm;
int ret;
/* 1. 分配 DRM 设备 */
drm = drm_dev_alloc(&xxx_drm_driver, &pdev->dev);
if (IS_ERR(drm))
return PTR_ERR(drm);
platform_set_drvdata(pdev, drm);
/* 2. 硬件初始化:CRTC、Encoder、Connector、Plane */
ret = xxx_display_init(drm);
if (ret)
goto err_free;
/* 3. 注册 DRM 设备 */
ret = drm_dev_register(drm, 0);
if (ret)
goto err_deinit;
/* 4. 创建 fbdev 兼容层 */
drm_fbdev_generic_setup(drm, 32);
dev_info(&pdev->dev, "XXX DRM driver loaded successfully\n");
return 0;
err_deinit:
xxx_display_deinit(drm);
err_free:
drm_dev_put(drm);
return ret;
}
经验总结: DRM 驱动的注册流程比 fbdev 更加规范化,但灵活性也更高。核心原则是“先分配、再初始化、后注册”。
drm_dev_alloc 与 drm_dev_register 必须配对使用,错误路径中调用 drm_dev_put 释放资源。fbdev 兼容层建议放在最后,即使失败也不应阻塞主驱动的注册。