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驱动注册与设备生命周期 设备树节点 compatible = "vendor,gpu" reg = <0x...>, irq = <...> platform_driver probe() / remove() of_match_table 匹配 struct drm_driver driver_features fops / gem_funcs prime_funcs / irq_handler 初始化 drm_dev_alloc() 分配drm_device + 初始化 drm_dev_register() 发布设备节点 /dev/dri/card0 drm_dev_unregister() 注销设备节点 生命周期:probe → alloc → register → 运行 → unregister → remove module_init/exit platform_driver_register()

上图展示了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)进行对照学习,加深对框架的理解。