4. MTK DRM驱动架构:mtk_drm_drv.c分析、平台驱动注册、组件化框架与bind/unbind机制
好,咱们今天来啃一块硬骨头——mtk_drm_drv.c。这个文件是整个MTK DRM驱动的入口,说白了就是所有显示操作的“总指挥部”。我当年第一次看这个文件时,说实话有点懵,因为里面涉及了平台驱动注册、组件化框架、还有bind/unbind这些概念。但等你理清了脉络,会发现它其实非常优雅。
4.1 平台驱动注册——一切从这里开始
先看最基础的部分。MTK的DRM驱动是通过Linux平台驱动框架注册到内核里的。你想想看,系统怎么知道该调用哪个驱动来处理显示?靠的就是这个平台驱动匹配机制。
static const struct of_device_id mtk_drm_of_ids[] = {
{ .compatible = "mediatek,mt8678-drm", },
{ /* sentinel */ },
};
MODULE_DEVICE_TABLE(of, mtk_drm_of_ids);
static struct platform_driver mtk_drm_platform_driver = {
.probe = mtk_drm_probe,
.remove = mtk_drm_remove,
.driver = {
.name = "mtk-drm",
.of_match_table = mtk_drm_of_ids,
.pm = &mtk_drm_pm_ops,
},
};
module_platform_driver(mtk_drm_platform_driver);
这里有个细节我提醒你注意:module_platform_driver这个宏,它其实展开后就是module_init和module_exit。我个人习惯在写新驱动时,如果不需要特殊初始化顺序,直接用这个宏最省事。
我在项目中遇到过一个问题:设备树里compatible写成了"mediatek,mt8678-drm",但驱动里写的是"mediatek,mt8678-drm-v1",结果驱动死活不加载。嗯,这种低级错误排查起来最浪费时间。
4.2 probe函数——驱动的心脏
当内核匹配到设备后,就会调用mtk_drm_probe。这个函数干了三件大事:
- 分配并初始化DRM设备结构体
- 注册各个显示子组件(如OVL、DSI、HDMI等)
- 调用
component_master_add_with_match启动组件化绑定流程
static int mtk_drm_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct mtk_drm_private *private;
struct component_match *match = NULL;
int ret;
private = devm_kzalloc(dev, sizeof(*private), GFP_KERNEL);
if (!private)
return -ENOMEM;
private->drm = drm_dev_alloc(&mtk_drm_driver, dev);
if (IS_ERR(private->drm))
return PTR_ERR(private->drm);
/* 收集所有子组件 */
ret = mtk_drm_component_match_add(dev, &match);
if (ret)
goto err_free;
/* 注册为组件化master */
component_master_add_with_match(dev, &mtk_drm_component_ops, match);
dev_set_drvdata(dev, private);
return 0;
err_free:
drm_dev_put(private->drm);
return ret;
}
你可能会问:为什么要搞这么复杂?直接一次性初始化不行吗?
答案在于MTK的显示架构是高度模块化的。OVL(图层叠加)、DSI(显示串行接口)、HDMI、DP等模块,它们可能在不同的时间点才准备好。组件化框架就是为了解决这种“异步依赖”问题。
核心要点:probe函数只负责“搭架子”,真正的硬件初始化要等到所有组件都bind完成后才进行。
4.3 组件化框架——分而治之的艺术
组件化框架是Linux内核里一套很巧妙的设计。它把一个大驱动拆成多个小组件,每个组件可以独立probe,等所有组件都ready了,再统一进行bind操作。
在MTK DRM里,组件分为两类:
| 组件类型 | 代表模块 | 职责 |
|---|---|---|
| Master(主控) | mtk_drm_drv | 协调所有组件,管理DRM设备 |
| Component(子组件) | ovl, dsi, hdmi, dp | 各自管理硬件模块,提供接口 |
每个子组件需要实现两个关键回调:bind和unbind。我刚开始接触时觉得这两个名字起得真好——bind就是“绑到一起”,unbind就是“解绑”。
static const struct component_ops mtk_ovl_component_ops = {
.bind = mtk_ovl_bind,
.unbind = mtk_ovl_unbind,
};
static int mtk_ovl_probe(struct platform_device *pdev)
{
/* 初始化硬件资源 */
/* ... */
/* 把自己注册为组件 */
return component_add(dev, &mtk_ovl_component_ops);
}
避坑指南:我曾经在调试时发现某个子组件的probe成功了,但bind没被调用。排查了半天,原来是设备树里该节点的status写成了"disabled"。记住,组件化框架只对status为"okay"的设备进行bind。
4.4 bind/unbind机制——从零到一的组装过程
当所有子组件都完成probe后,内核会调用master的bind函数。这个函数才是真正的“开工信号”。
static int mtk_drm_bind(struct device *dev)
{
struct mtk_drm_private *private = dev_get_drvdata(dev);
struct drm_device *drm = private->drm;
int ret;
/* 1. 绑定所有子组件 */
ret = component_bind_all(dev, drm);
if (ret)
return ret;
/* 2. 初始化DRM框架 */
ret = drm_dev_register(drm, 0);
if (ret)
goto err_unbind;
/* 3. 创建CRTC、Encoder、Connector等 */
ret = mtk_drm_kms_init(drm);
if (ret)
goto err_unregister;
/* 4. 创建Framebuffer和显示平面 */
ret = mtk_drm_fbdev_init(drm);
if (ret)
goto err_kms_deinit;
return 0;
err_kms_deinit:
mtk_drm_kms_deinit(drm);
err_unregister:
drm_dev_unregister(drm);
err_unbind:
component_unbind_all(dev, drm);
return ret;
}
这里有个顺序问题值得注意:component_bind_all必须在drm_dev_register之前调用。为什么?因为子组件在bind时可能会注册自己的encoder或connector,如果DRM设备还没注册,这些资源就没地方挂载。
unbind函数是bind的逆过程,顺序要反过来:
static void mtk_drm_unbind(struct device *dev)
{
struct mtk_drm_private *private = dev_get_drvdata(dev);
struct drm_device *drm = private->drm;
mtk_drm_fbdev_fini(drm);
mtk_drm_kms_deinit(drm);
drm_dev_unregister(drm);
component_unbind_all(dev, drm);
}
重要提醒:unbind函数在系统休眠、驱动卸载或热插拔时都会被调用。我曾经见过一个bug,unbind里没有正确释放中断,导致下次bind时申请中断失败。所以写unbind时一定要“打扫干净屋子再请客”。
4.5 多屏显示的关键——组件化如何支持多路输出
MTK8678支持多屏显示,说白了就是可以有多个CRTC和多个Encoder。组件化框架怎么支持这个?
答案在于:每个显示通道(如主屏、副屏、HDMI)都可以看作一个独立的组件组。设备树里会定义多个节点:
display-subsystem {
compatible = "mediatek,mt8678-display-subsystem";
ovl@14000000 {
compatible = "mediatek,mt8678-ovl";
/* 主屏图层 */
};
ovl@14001000 {
compatible = "mediatek,mt8678-ovl";
/* 副屏图层 */
};
dsi@14010000 {
compatible = "mediatek,mt8678-dsi";
/* 主屏DSI */
};
hdmi@14020000 {
compatible = "mediatek,mt8678-hdmi";
/* HDMI输出 */
};
};
在mtk_drm_component_match_add函数里,驱动会遍历这些节点,为每个节点创建一个组件匹配项。当所有节点都probe完成后,bind函数就会一次性把它们全部初始化。
我个人觉得这种设计最妙的地方在于:添加一个新的显示接口(比如DP),只需要在设备树里加一个节点,然后实现对应的component_ops,完全不需要修改核心的bind/unbind逻辑。这就是组件化框架的魅力——高内聚、低耦合。
4.6 调试技巧与常见问题
最后分享几个我在调试MTK DRM驱动时积累的经验:
- 查看组件绑定状态:
cat /sys/kernel/debug/component可以列出所有已注册的组件和它们的绑定状态。如果某个组件显示"not bound",说明它还没准备好。 - 模拟unbind/bind:通过
echo 0 > /sys/bus/platform/drivers/mtk-drm/unbind可以手动解绑,再echo 1 > .../bind重新绑定。这在调试热插拔场景时特别有用。 - 日志过滤:在
mtk_drm_bind和各个子组件的bind函数里加dev_dbg,然后通过echo 'file mtk_drm_drv.c +p' > /sys/kernel/debug/dynamic_debug/control动态开启日志。
嗯,关于mtk_drm_drv.c的分析就到这里。这个文件虽然只有几百行,但它承载了整个多屏显示架构的骨架。理解了它,你就掌握了MTK DRM驱动的“总开关”。下一章我们会深入分析各个子组件的具体实现,看看OVL、DSI这些模块是怎么在bind/unbind框架下工作的。