3、DRM核心框架:drm_driver注册流程、drm_dev_alloc/register/unregister、module_init/exit、设备树与platform驱动绑定
DRM(Direct Rendering Manager)子系统是Linux内核中负责图形显示与GPU管理的核心框架。对于驱动开发者而言,理解DRM驱动的生命周期管理——从设备探测、内存分配、注册到注销——是编写稳定、可维护显示驱动的基石。本章将深入剖析DRM驱动的注册流程,重点讲解drm_driver结构体的作用、drm_dev_alloc/register/unregister API的正确使用方式,以及如何通过设备树与platform驱动机制完成设备绑定。我们将从内核源码角度拆解每一个关键步骤,并结合实际项目中的常见问题给出工程实践建议。
上图展示了DRM驱动从设备树匹配到最终注销的完整生命周期。下面我们将逐一拆解每个环节的实现细节与工程要点。
3.1 核心原理:drm_driver结构体与设备生命周期
struct drm_driver是DRM框架中最核心的数据结构之一,它描述了驱动的能力、回调函数和私有数据。该结构体在include/drm/drm_drv.h中定义,包含数十个字段,但实际驱动通常只需关注其中一部分。关键字段包括:
- driver_features:位掩码,声明驱动支持的特性,如
DRIVER_MODESET(支持内核模式设置)、DRIVER_GEM(支持GEM内存管理)、DRIVER_RENDER(支持渲染节点)等。 - fops:文件操作结构体,定义用户空间通过
/dev/dri/cardX访问设备时的回调,如open、ioctl、mmap等。 - gem_funcs:GEM(Graphics Execution Manager)相关回调,用于对象创建、释放、mmap映射等。
- prime_funcs:PRIME缓冲共享回调,用于跨设备DMA-BUF导出/导入。
- irq_handler:中断处理函数指针,用于处理GPU或显示控制器产生的中断。
- num_ioctls与ioctls:驱动自定义的ioctl命令表。
设备生命周期管理遵循“先分配、后注册、再使用、最终注销”的原则。具体流程为:drm_dev_alloc()分配并初始化drm_device结构体,然后调用drm_dev_register()将设备发布到用户空间,驱动进入工作状态。卸载时,先调用drm_dev_unregister()移除用户空间可见性,再通过drm_dev_put()释放引用计数归零后的设备内存。
关键理解:DRM框架采用“引用计数 + 注册状态”双重管理机制。drm_dev_alloc()返回的设备初始引用计数为1,drm_dev_register()成功后引用计数增加,用户空间打开设备节点也会增加引用。只有所有引用释放且设备处于未注册状态时,内存才会被真正释放。
3.2 操作流程:从module_init到设备注册
一个典型的DRM platform驱动,其模块入口和探测流程如下:
// 模块入口
static int __init mygpu_init(void)
{
return platform_driver_register(&mygpu_platform_driver);
}
module_init(mygpu_init);
static void __exit mygpu_exit(void)
{
platform_driver_unregister(&mygpu_platform_driver);
}
module_exit(mygpu_exit);
// platform驱动定义
static const struct of_device_id mygpu_of_match[] = {
{ .compatible = "vendor,mygpu" },
{ }
};
MODULE_DEVICE_TABLE(of, mygpu_of_match);
static struct platform_driver mygpu_platform_driver = {
.probe = mygpu_probe,
.remove = mygpu_remove,
.driver = {
.name = "mygpu",
.of_match_table = mygpu_of_match,
},
};
当设备树中某个节点与of_match_table中的compatible属性匹配时,内核会调用probe回调。在probe函数中完成DRM设备的初始化:
static int mygpu_probe(struct platform_device *pdev)
{
struct drm_device *drm;
struct mygpu_private *priv;
int ret;
// 1. 分配私有数据结构
priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
// 2. 获取硬件资源(寄存器映射、中断、时钟等)
priv->regs = devm_platform_ioremap_resource(pdev, 0);
if (IS_ERR(priv->regs))
return PTR_ERR(priv->regs);
priv->irq = platform_get_irq(pdev, 0);
if (priv->irq < 0)
return priv->irq;
// 3. 分配DRM设备
drm = drm_dev_alloc(&mygpu_drm_driver, &pdev->dev);
if (IS_ERR(drm))
return PTR_ERR(drm);
drm_set_drvdata(drm, priv);
priv->drm = drm;
// 4. 初始化硬件(如设置显示控制器、GPU时钟等)
ret = mygpu_hw_init(priv);
if (ret)
goto err_free;
// 5. 注册DRM设备
ret = drm_dev_register(drm, 0);
if (ret)
goto err_hw_fini;
platform_set_drvdata(pdev, drm);
return 0;
err_hw_fini:
mygpu_hw_fini(priv);
err_free:
drm_dev_put(drm);
return ret;
}
在remove回调中执行相反操作:
static int mygpu_remove(struct platform_device *pdev)
{
struct drm_device *drm = platform_get_drvdata(pdev);
struct mygpu_private *priv = drm_get_drvdata(drm);
drm_dev_unregister(drm);
mygpu_hw_fini(priv);
drm_dev_put(drm);
return 0;
}
3.3 案例拆解:基于设备树的GPU驱动绑定实战
假设我们正在为一块嵌入式SoC编写GPU驱动,该SoC的显示控制器挂载在系统总线上。设备树节点定义如下:
gpu: gpu@1c00000 {
compatible = "vendor,maligpu";
reg = <0x01c00000 0x10000>;
interrupts = <GIC_SPI 42 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&ccu CLK_GPU>;
resets = <&ccu RST_GPU>;
};
驱动开发过程中,我们遇到一个典型问题:设备树匹配成功但probe函数未被调用。通过排查发现,of_match_table中的compatible字符串与设备树不一致——设备树中写的是"vendor,maligpu",而驱动中误写为"vendor,mali-gpu"。修正后probe正常执行。
另一个常见问题是drm_dev_register失败,返回-ENODEV。这通常是因为drm_driver中设置了DRIVER_MODESET特性,但系统没有可用的framebuffer控制台或VT支持。解决方案是检查内核配置中CONFIG_DRM_FBDEV_EMULATION和CONFIG_FB是否开启。
| 排查步骤 | 检查点 | 常用命令/方法 |
|---|---|---|
| 1. 设备树匹配 | compatible属性是否一致 | ls /sys/firmware/devicetree/base/.../compatible |
| 2. 驱动加载 | 模块是否成功插入 | dmesg | grep mygpu |
| 3. probe调用 | probe函数是否执行 | 在probe入口添加dev_info()打印 |
| 4. 资源获取 | reg/irq/clk是否有效 | /proc/iomem、/proc/interrupts |
| 5. DRM注册 | drm_dev_register返回值 | dmesg | grep drm |
3.4 常见误区与总结复盘
在DRM驱动开发中,以下误区需要特别注意:
误区一:忽略drm_dev_put的调用时机。许多初学者在probe失败路径上只调用drm_dev_unregister而忘记drm_dev_put,导致设备内存泄漏。正确做法是:drm_dev_register失败时,直接调用drm_dev_put即可,因为未注册的设备没有额外引用。
误区二:在remove中直接释放私有数据。如果使用devm_kzalloc分配私有数据,内核会在设备释放时自动回收,无需手动kfree。但若使用普通kzalloc,则必须在drm_dev_put之后释放,否则可能访问已释放的内存。
经验总结:建议在probe中使用devm_*系列API管理资源(如devm_platform_ioremap_resource、devm_clk_get),这样即使probe中途失败,内核也会自动清理已获取的资源,减少错误处理代码量。
此外,关于drm_dev_register的第二个参数flags,通常传入0即可。如果驱动需要创建多个DRM设备(例如同时管理GPU和显示控制器),可以传入DRM_DEV_MAKE_NODE_NAME等标志来区分设备节点名称。
最后,理解module_init与module_exit的对称性至关重要。在卸载模块时,必须确保所有打开的设备节点已关闭,否则drm_dev_unregister会等待引用计数归零,可能导致卸载阻塞。建议在remove回调中先注销DRM设备,再释放硬件资源,最后调用drm_dev_put完成内存回收。
通过本章的学习,读者应能够独立完成一个DRM platform驱动的注册流程搭建,并具备排查常见绑定与注册问题的能力。在实际项目中,建议结合drivers/gpu/drm/目录下的真实驱动(如pl111、mcde)进行对照学习,加深对框架的理解。