4、DRM驱动注册与初始化:drm_driver结构体、module_init流程、设备树解析与匹配
好,我们进入第四章。这一章讲的是DRM驱动怎么“活”起来——从代码到硬件,从内核模块到显示设备。说白了,就是驱动怎么被加载、怎么找到对应的硬件、怎么完成初始化。
我当年刚接触DRM时,最困惑的就是:drm_driver 这个结构体到底要填什么?module_init 之后发生了什么?设备树又是怎么和驱动匹配上的?
今天咱们就把这三件事彻底讲透。
4.1 drm_driver结构体:驱动的“身份证”
每个DRM驱动,核心就是一个 struct drm_driver。它定义了驱动的能力、回调函数、以及一些关键参数。你可以把它理解成驱动的“自我介绍”。
我习惯把这个结构体分成三块来看:
| 分类 | 成员 | 说明 |
|---|---|---|
| 基本信息 | driver_features |
驱动支持哪些特性,比如是否支持ATOMIC、是否支持PRIME |
name, desc, date |
驱动名称、描述、日期,调试时很有用 | |
| 文件操作 | fops |
用户空间通过 /dev/dri/card0 访问时的操作函数集 |
open, postclose |
文件打开/关闭时的钩子,我常在这里做per-fd资源管理 | |
| GEM/内存管理 | gem_create_object |
创建GEM对象的回调,驱动可以重写以分配私有数据 |
prime_handle_to_fd 等 |
PRIME缓冲共享,跨进程显示的关键 |
这里有个重点:driver_features 的配置直接影响DRM核心的行为。比如你设了 DRIVER_ATOMIC,核心就会启用atomic modeset路径;不设的话,走的是legacy路径。
关键点: drm_driver 不是每个驱动都从头写。大部分驱动用 drm_gem_fops 作为默认fops,然后重写个别回调。我见过新手把整个fops自己实现一遍,结果漏了关键ioctl——嗯,画面直接黑屏。
来看一个实际例子,这是我参与的一个显示控制器的驱动片段:
static const struct drm_driver foo_drm_driver = {
.driver_features = DRIVER_GEM | DRIVER_MODESET | DRIVER_ATOMIC,
.name = "foo_drm",
.desc = "Foo Display Controller DRM Driver",
.date = "20240101",
.fops = &foo_drm_fops,
.open = foo_drm_open,
.postclose = foo_drm_close,
.gem_create_object = foo_gem_create_object,
.prime_handle_to_fd = drm_gem_prime_handle_to_fd,
.prime_fd_to_handle = drm_gem_prime_fd_to_handle,
};
注意看 prime_handle_to_fd 我直接用了DRM核心的通用实现。为什么?因为我们的GEM对象没有特殊的内存布局,通用实现就够了。如果硬件有特殊要求(比如连续物理内存),那就得自己写。
4.2 module_init流程:从内核模块到DRM设备
驱动写好了,怎么让它跑起来?答案是 module_init。但这里面的流程,比你想象的要复杂。
典型的调用链是这样的:
- module_init(foo_init) → 内核加载模块时调用
- platform_driver_register → 注册平台驱动,等待设备匹配
- probe 回调被触发 → 设备树匹配成功后调用
- devm_drm_dev_alloc → 分配并初始化DRM设备
- drm_dev_register → 注册DRM设备,创建 /dev/dri/card0
我当年踩过一个坑:devm_drm_dev_alloc 和 drm_dev_register 之间,一定要把所有的CRTC、PLANE、ENCODER、CONNECTOR都初始化好。否则注册后用户空间打开设备,看到的是一堆空指针。
注意: 不要在 drm_dev_register 之后再动态添加CRTC。DRM核心在注册时会枚举所有资源并建立索引,后续添加会导致索引混乱。我曾经因为这个bug,调试了整整两天。
来看一个标准的probe函数骨架:
static int foo_drm_probe(struct platform_device *pdev)
{
struct drm_device *drm;
int ret;
// 1. 分配DRM设备,附带驱动私有数据
drm = devm_drm_dev_alloc(&pdev->dev, &foo_drm_driver,
struct foo_drm_private, base);
if (IS_ERR(drm))
return PTR_ERR(drm);
// 2. 初始化硬件:时钟、复位、中断等
ret = foo_hw_init(drm);
if (ret)
return ret;
// 3. 创建CRTC、PLANE、ENCODER、CONNECTOR
ret = foo_modeset_init(drm);
if (ret)
return ret;
// 4. 注册DRM设备
ret = drm_dev_register(drm, 0);
if (ret)
return ret;
// 5. 启用热插拔检测(如果有)
drm_kms_helper_poll_init(drm);
return 0;
}
这里有个细节:devm_drm_dev_alloc 是带资源管理的分配函数。如果probe失败,它会自动释放。我个人习惯尽量用devm系列函数,省去手动清理的麻烦。
4.3 设备树解析与匹配:驱动怎么找到硬件
设备树(Device Tree)是Android/Linux嵌入式平台描述硬件的方式。驱动通过设备树知道:我在哪个地址?中断号是多少?GPIO怎么配?
匹配过程其实很简单:
- 驱动声明一个
of_device_id表,里面包含兼容字符串 - 设备树节点里的
compatible属性与之匹配 - 匹配成功后,内核调用驱动的probe函数
来看一个典型的匹配表:
static const struct of_device_id foo_of_match[] = {
{ .compatible = "foo,display-controller-v1", },
{ .compatible = "foo,display-controller-v2", .data = &v2_config },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, foo_of_match);
static struct platform_driver foo_drm_platform_driver = {
.probe = foo_drm_probe,
.remove = foo_drm_remove,
.driver = {
.name = "foo_drm",
.of_match_table = foo_of_match,
},
};
这里我用了 .data 字段来区分硬件版本。v1和v2的寄存器布局不同,但驱动代码大部分可以复用。在probe里通过 of_match_device 拿到对应的配置结构体,然后初始化不同的寄存器偏移表。
经验之谈: 设备树里的 reg 属性对应platform设备的IO资源。用 platform_get_resource(pdev, IORESOURCE_MEM, 0) 获取,然后 devm_ioremap_resource 映射。中断用 platform_get_irq(pdev, 0)。这些接口我几乎每个probe函数都会用到。
设备树解析还有一个常见场景:获取GPIO和时钟。比如:
// 获取复位GPIO
priv->reset_gpio = devm_gpiod_get_optional(dev, "reset", GPIOD_OUT_LOW);
// 获取像素时钟
priv->pixel_clk = devm_clk_get(dev, "pixel");
为什么用 _optional 版本?因为有些硬件版本可能没有复位引脚,驱动要能兼容。我习惯把所有可能缺失的资源都设为optional,然后在初始化时检查。
4.4 整体流程:一张图看懂
说了这么多,咱们用一张SVG图把整个流程串起来。从内核加载到用户空间看到 /dev/dri/card0,每一步都清晰可见。
这张图里,我特意把 设备树匹配 标成了红色。为什么?因为这是驱动和硬件“握手”的关键一步。匹配不上,后面的probe根本不会执行。我遇到过好几次,设备树里compatible写错了一个字母,驱动死活不加载——查了两天才发现是拼写问题。
4.5 避坑指南
最后,分享几个我实际项目中踩过的坑:
- 设备树reg地址要跟芯片手册一致。 有一次我把base address写错了,驱动映射后读写全是0xFFFFFFFF,还以为是硬件没上电。
- drm_dev_register 之前确保所有kms对象已创建。 否则用户空间打开设备后,枚举对象时可能崩溃。
- module_init 里不要做耗时操作。 内核模块加载是有超时限制的。我习惯在probe里做真正的初始化,module_init只注册驱动。
- of_match_table 最后一定要加 sentinel(空结构体)。 忘了的话,内核遍历匹配表时会越界,导致随机崩溃。
小技巧: 调试设备树匹配问题时,可以在内核启动参数加 of_devlink 或者查看 /sys/firmware/devicetree 下的内容。我经常用 dtc -I fs -O dts /sys/firmware/devicetree/base 反编译设备树,确认节点属性是否正确。
好了,这一章的内容就是这些。drm_driver结构体、module_init流程、设备树解析与匹配,这三块是DRM驱动的基石。理解透了,后面写具体的CRTC、PLANE驱动就会顺手很多。