4. DRM驱动初始化:platform driver注册、DRM device创建、mode_config初始化
好,咱们今天聊聊DRM驱动初始化的核心流程。说白了,就是让显示子系统在Linux内核里“活过来”的过程。我做了这么多年显示驱动,每次拿到一个新的SoC平台,第一步要搞定的就是这个初始化流程。
你想想看,一个显示设备要正常工作,得先让内核知道它的存在吧?得给它分配资源吧?得告诉它支持什么显示模式吧?嗯,这就是我们今天要讲的三板斧:platform driver注册、DRM device创建、mode_config初始化。
核心要点:DRM驱动的初始化,本质上是一个“注册-创建-配置”的三步走过程。每一步都有坑,我当年踩过的坑可不少。
4.1 platform driver注册:让内核找到你的设备
先说说platform driver。在嵌入式Linux里,显示控制器通常挂在系统总线上,用platform驱动模型来管理。我个人习惯先把driver定义好,再考虑怎么注册。
来看一个典型的platform driver结构:
static const struct of_device_id mipi_dsi_of_match[] = {
{ .compatible = "vendor,display-controller", },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, mipi_dsi_of_match);
static struct platform_driver display_driver = {
.probe = display_probe,
.remove = display_remove,
.driver = {
.name = "my_display",
.of_match_table = mipi_dsi_of_match,
.pm = &display_pm_ops,
},
};
module_platform_driver(display_driver);
这里有个细节要注意:of_match_table里的compatible字符串,必须和设备树里写的完全一致。我曾经因为少写了一个逗号,折腾了一整天——驱动死活不probe,最后发现是设备树里写的是"vendor,display-controller",而驱动里写的是"vendor,displaycontroller"。嗯,这种低级错误,犯过一次就再也不会忘了。
小技巧:注册platform driver时,建议用module_platform_driver()宏,它帮你处理了module_init和module_exit。省事,也不容易出错。
4.2 DRM device创建:从platform device到DRM世界
platform driver注册成功后,内核会调用probe函数。这时候,我们要在probe里创建DRM device。说白了,就是把一个普通的platform device,包装成一个DRM设备,让它进入DRM的框架里。
我一般这样写probe函数:
static int display_probe(struct platform_device *pdev)
{
struct drm_device *drm;
int ret;
// 分配DRM device结构体
drm = drm_dev_alloc(&display_driver_funcs, &pdev->dev);
if (IS_ERR(drm))
return PTR_ERR(drm);
// 设置driver data,方便后续使用
platform_set_drvdata(pdev, drm);
// 注册DRM device
ret = drm_dev_register(drm, 0);
if (ret)
goto err_free;
return 0;
err_free:
drm_dev_put(drm);
return ret;
}
这里有两个关键函数:drm_dev_alloc()和drm_dev_register()。前者是分配内存并初始化,后者是让DRM核心知道这个设备的存在。顺序不能乱,先分配再注册。
注意:drm_dev_alloc()分配的内存,要用drm_dev_put()释放。别搞混了,我见过有人用kfree去释放,结果内核直接panic。DRM框架有自己的内存管理方式,别自作聪明。
4.3 mode_config初始化:告诉DRM你的显示能力
DRM device创建好了,接下来要初始化mode_config。这一步是告诉DRM核心:我这个显示设备支持什么分辨率?支持什么显示接口?最大带宽是多少?
mode_config初始化通常在bind函数里做,或者放在probe的后面部分。我个人习惯单独写一个初始化函数:
static int display_mode_config_init(struct drm_device *drm)
{
// 设置最小和最大分辨率
drm->mode_config.min_width = 320;
drm->mode_config.min_height = 240;
drm->mode_config.max_width = 3840;
drm->mode_config.max_height = 2160;
// 设置显示接口类型
drm->mode_config.output_poll_changed = NULL;
drm->mode_config.funcs = &display_mode_config_funcs;
// 设置其他参数
drm->mode_config.preferred_depth = 32;
drm->mode_config.prefer_shadow = 0;
// 别忘了调用这个
drm_mode_config_init(drm);
return 0;
}
这里有个容易忽略的点:drm_mode_config_init()必须在设置完参数之后调用。为什么?因为这个函数会初始化一些内部链表和锁,如果参数还没设置好,后面可能会出问题。
我曾经在一个项目里,把drm_mode_config_init()放在了最前面,结果后面设置max_width的时候,发现内部校验通不过——因为初始化时默认值太小了。嗯,这种顺序问题,调试起来特别隐蔽。
4.4 完整的初始化流程
把上面三步串起来,完整的初始化流程是这样的:
static int display_probe(struct platform_device *pdev)
{
struct drm_device *drm;
int ret;
// 第一步:分配DRM device
drm = drm_dev_alloc(&display_driver_funcs, &pdev->dev);
if (IS_ERR(drm))
return PTR_ERR(drm);
platform_set_drvdata(pdev, drm);
// 第二步:初始化mode_config
ret = display_mode_config_init(drm);
if (ret)
goto err_free;
// 第三步:注册DRM device
ret = drm_dev_register(drm, 0);
if (ret)
goto err_config;
return 0;
err_config:
drm_mode_config_cleanup(drm);
err_free:
drm_dev_put(drm);
return ret;
}
你看,逻辑很清晰:先有设备,再配置能力,最后注册上线。这个顺序是DRM框架强制要求的,别想着偷懒。
4.5 常见问题与避坑指南
- probe不调用? 检查设备树compatible字符串是否匹配,检查驱动是否编译进内核。
- drm_dev_register失败? 看看是不是mode_config没初始化,或者min/max设置不合理。
- 内存泄漏? 检查错误路径是否调用了drm_dev_put()和drm_mode_config_cleanup()。
避坑指南:我曾经在一个项目里,probe成功了,但drm_dev_register一直返回-ENOMEM。查了两天,最后发现是drm_dev_alloc时传的driver_funcs里,有个回调函数没实现。DRM框架在register时会调用这些回调,如果没实现就直接报错。所以,driver_funcs里的每个回调都要认真实现,哪怕是个空函数。
4.6 知识体系图
下面这张图,把整个初始化流程串起来了。你可以看到,从platform driver注册开始,到DRM device创建,再到mode_config初始化,每一步都有明确的输入和输出。
这张图里,三个步骤是串行的。你可能会问:为什么不能先注册再配置?嗯,因为DRM框架的设计哲学是:先让设备存在,再告诉它你能做什么。就像一个人,得先出生,再学本事,对吧?
个人经验:调试初始化流程时,我建议在probe函数里多加几个dev_dbg()打印。这样能清楚地看到每一步的执行情况。我曾经靠这个方法,半小时就定位到了一个因为drm_dev_alloc失败导致的黑屏问题。
好了,DRM驱动初始化的核心内容就这些。记住三步走:注册platform driver、创建DRM device、初始化mode_config。每一步都有它的道理,也有它的坑。多写几次,自然就熟练了。
公众号:蓝海资料掘金营,微信deep3321