MTK平台的显示驱动,说穿了就是一套Linux原生的DRM/KMS实现,但第一次打开mtk_drm_drv.c的时候,我确实懵了几天——正常的DRM驱动是platform_driver直接probe完set up,而MTK这边到处是component_add、component_match_add、component_bind_all,绕来绕去像在搭积木。这篇文章我打算按“从KMS模块到组件框架”的顺序,把MTK DRM的初始化流程完整拆一遍。不管你是刚接手MTK显示栈、碰到屏幕点不亮,还是想理解DRM组件框架的整合机制,这篇文章都能给你一个可以直接参考的索引。我会把代码逻辑、设备树、时钟计算、常见坑一起讲透,尽量少说废话。
1. 先建立整体认知:MTK DRM/KMS是什么
1.1 内核里的DRM和KMS到底管什么
DRM(Direct Rendering Manager)是Linux内核子系统,原来的职责是给显卡用户态驱动提供安全的GPU访问通道,后来逐渐演变成完整显示栈的管理框架。KMS(Kernel Mode Setting)是DRM框架里负责显示模式配置的部分,分辨率、刷新率、连接器、竖屏横屏这些都属于KMS管辖范围。另外提醒一下,看到KMS这个缩写,千万别和Windows跑的那个激活工具KMS搞混了,两者除了字母一样,没有任何关系。
在你的Android或嵌入式Linux设备上,用户态App画好一帧内容后,通过SurfaceFlinger/Wayland等合成,最后调用libdrm的接口把buffer交到KMS。KMS内部由CRTC、Plane、Encoder、Connector四个核心对象协作:
- CRTC:负责把内存里的图像帧扫描出来,生成对应的像素时钟和控制信号。
- Plane:对应显示硬件里的图层(如MTK的OVL层),一个CRTC可以挂多个Plane。
- Encoder:像素数据从SoC出来后,由Encoder变成具体的传输时序,比如DPI并行信号、DSI差分信号、HDMI编码信号。
- Connector:代表物理输出接口,负责检测有没有屏接在上面、屏支持哪些timing。
MTK DRM驱动做的事情,就是把这四个对象和MTK的具体硬件IP一一对应起来,并注册到Linux DRM核心里。
1.2 MTK DRM驱动的目录结构和模块划分
MTK的DRM驱动放在内核源码的drivers/gpu/drm/mediatek/目录,不同内核版本文件划分略有差异,但核心文件基本固定:
mtk_drm_drv.c:DRM主控,负责注册platform driver、管理组件框架、创建CRTC/Plane、注册DRM设备。mtk_drm_crtc.c:CRTC实现,包含图层调度、VBLANK、自刷新等功能。mtk_drm_plane.c:Plane实现,对应MTK的OVL硬件叠加层。mtk_drm_fb.c、mtk_drm_gem.c:framebuffer和DMA内存分配管理。mtk_dsi.c:DSI host控制器驱动,实现Encoder和Connector逻辑。mtk_dpi.c:DPI并行接口驱动,常用于低端屏或外接转换芯片。mtk_hdmi.c:HDMI驱动(不同平台差异较大)。mtk_mipi_tx.c:MIPI D-PHY物理层驱动,负责差分信号的电气参数和时钟。
MTK DRM驱动不是把所有功能塞在一个文件里的单体架构,而是每个硬件IP一个驱动、各自管理各自的资源。这个设计直接决定了后面要讲的组件框架。
1.3 为什么MTK用组件框架而不是直接probe
Linux里同一个SoC的不同外设驱动,各自的probe时机由设备树和总线匹配顺序决定,而MTK显示链路涉及的模块太多:DSI控制器要等MIPI_PHY准备好,MIPI_PHY又可能要等对应的电源域先起来,DSI所接的Panel又依赖I2C或者GPIO控制可用。如果让每个驱动自己probe完就对外提供接口,极容易出现“下游驱动想用上游模块,但上游模块还没初始化完”的尴尬局面。
组件框架(component framework)解决的就是这种多模块协同初始化问题。大体思路是把显示链路里的每个模块注册成一个Component,把DRM主控注册成Master,Master等待所有挂接的Component到齐之后,再统一调用bind回调完成真正的初始化。实际接入顺序由组件绑定的匹配关系决定,不会因为模块加载次序的差异而出错。
打个比方:组件框架就像装修,不用管瓦工、电工、木工谁先到场,只要工长拿到所有工人都签到的消息,才开始统一安排干活。MTK DRM主控就是这个工长,DSI、DPI、HDMI这些就是工人。
2. 初始化流程的整体设计思路拆解
2.1 从设备树到平台设备:DTS怎么描述显示链路
理解MTK DRM初始化,必须先看设备树里显示链路是怎么描述的。一个典型的MTK平台显示相关节点大致长这样:
display_mutex: displaymutex@14000000 { compatible = "mediatek,mt8183-display-mutex"; reg = <0 0x14000000 0 0x1000>; #clock-cells = <1>; /* ... */ }; ovl0: ovl@14008000 { compatible = "mediatek,mt8183-disp-ovl"; reg = <0 0x14008000 0 0x1000>; interrupts = <GIC_SPI 225 IRQ_TYPE_LEVEL_LOW>; clocks = <&mmsys CLK_MM_DISP_OVL0>; power-domains = <&spm MT8183_POWER_DOMAIN_DISP>; /* ... */ }; dsi0: dsi@1400f000 { compatible = "mediatek,mt8183-dsi", "mediatek,mt8186-dsi"; reg = <0 0x1400f000 0 0x1000>; interrupts = <GIC_SPI 223 IRQ_TYPE_LEVEL_LOW>; clocks = <&mmsys CLK_MM_DSI0_MM>, <&mmsys CLK_MM_DSI0_IF>; phys = <&mipi_tx0>; phy-names = "dphy"; /* ... */ }; mipi_tx0: mipi_tx@10215000 { compatible = "mediatek,mt8183-mipi-tx"; reg = <0 0x10215000 0 0x1000>; /* ... */ }; panel: panel@0 { compatible = "boe,tv110c9m-ll60"; reg = <0>; /* ... */ };这些设备树节点经过内核的of_platform_populate机制,会逐条生成对应的platform_device,然后开始匹配驱动。匹配的工作是相对独立的,没有强制先后顺序。比如mipi_tx0和dsi0谁先生效,内核并不保证,正是这种不确定性让组件框架变得必要。
2.2 Master和Component的双重角色
在MTK DRM的框架里:
- Master:
mtk_drm_drv.c对应的platform_device,即整个DRM显示实例。 - Component:参与显示链路的各个模块驱动,最典型的是
mtk_dsi、mtk_dpi、mtk_hdmi,部分平台还会把mtk_ovl、mtk_rdma这类内部IP也注册为Component。
Master和Component之间通过一个compare函数进行匹配。匹配的依据通常是设备树节点指针,也就是说,Master在添加匹配信息时会把“需要哪些节点”记录下来,当某个节点对应的驱动注册Component时,通过component_add通知框架,框架拿节点指针去比对,全部对上就触发bind。
MTK的compare逻辑在mtk_drm_drv.c里一般是这样的:
static int mtk_drm_component_compare(struct device *dev, void *data) { struct device_node *np = data; return dev->of_node == np; } static int mtk_drm_component_match(struct device *dev, struct component_match **match) { /* 遍历所有显示相关的输出接口节点,加入match */ component_match_add(dev, match, mtk_drm_component_compare, np); return 0; }开发者只需要在函数里把我们想要纳入管理的每一个显示输出节点都加进match列表,之后框架就会自动等待它们完成注册。
2.3 初始化顺序为什么要“可延展”
组件框架最大的价值不只是“等齐了再开工”,而是让平台扩展变得很干净。同一个DRM主控,不同产品可能接DSI屏、接DPI屏、接HDMI或者同时接多种输出,只需要在match列表里按需添加对应的设备树节点,新增的输出接口驱动通过component_add注册进来,DRM主控就能自动识别并初始化。不需要在master驱动里堆一堆#ifdef,也不需要关心这些输出接口驱动的probe顺序。
这个过程我实践下来,最直观的好处是在开发前期可以只使能一路输出,比如先点DSI屏,其他输出接口节点不添加到match列表或直接去使能设备树节点,系统启动依旧正常。等DSI稳定了再接上HDMI,不用大改主驱动代码,扩展性和可维护性都好了很多。
3. 从probe到DRM设备注册:核心流程逐步走
3.1 平台驱动的注册与probe入口
MTK DRM的入口在mtk_drm_drv.c的platform_driver定义:
static struct platform_driver mtk_drm_platform_driver = { .probe = mtk_drm_probe, .remove = mtk_drm_remove, .driver = { .name = "mediatek-drm", .pm = &mtk_drm_pm_ops, .of_match_table = mtk_drm_of_ids, }, }; module_platform_driver(mtk_drm_platform_driver);当设备树里的“mediatek,display-subsystem”或“mediatek,mt8183-drm”这类节点匹配上驱动后,内核调用mtk_drm_probe。mtk_drm_probe做的事情不多,主要是分配私有数据、注册Master:
static int mtk_drm_probe(struct platform_device *pdev) { struct mtk_drm_private *private; private = devm_kzalloc(&pdev->dev, sizeof(*private), GFP_KERNEL); platform_set_drvdata(pdev, private); /* 收集需要匹配的组件,把自己注册为Master */ ret = mtk_drm_component_match(&pdev->dev, &match); if (ret < 0) return ret; ret = component_master_add_with_match(&pdev->dev, &mtk_drm_master_ops, match); if (ret < 0) return ret; return 0; }注意:到这一步,真正的内容还没初始化,只是“报名”成了Master。如果match里声明的组件设备没有全部就位,mtk_drm_master_ops.bind不会被执行,DRM设备也不会注册。
3.2 component_match_add:DRM实例成为Master
component_master_add_with_match是组件框架的入口,它需要传入两个关键参数:Master设备(即DRM platform_device)和Master操作集。MTK Master操作集定义:
static const struct component_master_ops mtk_drm_master_ops = { .bind = mtk_drm_bind, .unbind = mtk_drm_unbind, };从这里的逻辑可以看出来,MTK DRM真正的所有初始化工作,都被放到了mtk_drm_bind里,而不是放在probe里。这也是MTK DRM和普通DRM驱动最明显的差别。后面排查问题时,如果你发现probe执行了但/dev/dri/card0没有生成,第一反应不要是“驱动没probe”,而要去看mtk_drm_bind有没有被调用、卡在哪一步。
3.3 mtk_drm_bind:所有初始化的真正入口
当match列表里的所有Component都注册完毕,组件框架回调mtk_drm_bind。它的核心流程大致如下:
static int mtk_drm_bind(struct device *dev) { struct mtk_drm_private *private = dev_get_drvdata(dev); struct drm_device *drm; int ret; drm = drm_dev_alloc(&mtk_drm_driver, dev); if (IS_ERR(drm)) return PTR_ERR(drm); private->drm = drm; /* 先绑定所有组件设备 */ ret = component_bind_all(dev, NULL); if (ret) goto err_component_bind_all; /* 初始化KMS模式配置 */ ret = mtk_drm_kms_init(drm); if (ret) goto err_kms_init; /* 注册DRM设备 */ ret = drm_dev_register(drm, 0); if (ret) goto err_dev_register; /* 初始化fbdev兼容层 */ drm_fbdev_generic_setup(drm, 32); return 0; }代码顺序很关键:先component_bind_all,把所有输出接口组件绑进来,再调用KMS初始化,创建CRTC和Plane,最后注册DRM设备。原因是CRTC和Plane创建完成后,需要关联已经存在的Encoder/Connector,如果Encoder和Connector都没创建好,后面的模式配置就无法建立连接关系。
3.4 CRTC/Plane/Encoder/Connector 的构建细节
在mtk_drm_kms_init里,MTK驱动会做几件事:
- 配置
drm_mode_config,比如设好min/max width/height,挂载funcs回调。 - 遍历每个显示通道,为每个通道创建Plane和CRTC。
- 每个Plane初始化使用
drm_universal_plane_init,并给Plane绑定MTK特有的更新和禁用回调。 - 每个CRTC通过
drm_crtc_init_with_planes挂上对应Plane,CRTC提供enable/disable/mode_valid/atomic_check等回调。
MTK的显示数据流典型路径是:OVL(图层叠加硬件) → RDMA(DMA读取并输出像素) → 输出接口(DSI/DPI/HDMI)。对应的,每个CRTC会挂载一个“主Plane”和一个“光标/辅助Plane”,分别映射OVL0和OVL1等硬件层。用户态合成好的一帧画面,通过drm_atomic提交到主Plane,CRTC不断扫描该Plane指向的内存地址,把画面输出到Encoder。
Encoder和Connector则是在mtk_dsi_bind、mtk_dpi_bind这些组件自己的bind回调中创建的。每个组件bind回调会用drm_encoder_init注册一个Encoder,同时用drm_connector_init注册一个Connector,并实现get_modes回调,用来从Panel驱动读timing列表。最后用drm_connector_attach_encoder把它们组成一条完整链路。
3.5 fbdev 兼容层的初始化
Android系统一般不依赖内核fbdev,但嵌入式Linux、Buildroot、以及部分调试场景仍然在使用/dev/fb0。内核里的drm_fbdev_generic_setup会在DRM设备注册后创建一个兼容的fbdev设备,用户态程序可以通过经典的fb_ioctl操作显示。这个代码在MTK DRM初始化里同样是最后一步,因为必须先有可用的CRTC和Connector,fbdev才能确定使用哪条显示路径。
我在调试早期曾碰到过fbdev显示雪花点的问题,后来发现是drm_fbdev_generic_setup传入的bpp与Panel实际位深不一致引起的,把32改成Panel的RGB888输出就能正常显示。如果你也在嵌入式Linux上调试MTK DRM,fbdev兼容层这种小坑值得留意。
4. DSI/DPI显示接口的接入过程
4.1 DSI 驱动如何被组件框架拉起
MTK DSI驱动对应mtk_dsi.c,它的platform_driver收到设备树节点后调用probe。DSI probe中除了常规的时钟、复位、中断、PHY获取之外,最后一步就是把自己注册成Component:
static int mtk_dsi_probe(struct platform_device *pdev) { /* 各种资源获取:clk、regmap、phy ... */ return component_add(&pdev->dev, &mtk_dsi_component_ops); }mtk_dsi_component_ops里bind回调是重点:
static const struct component_ops mtk_dsi_component_ops = { .bind = mtk_dsi_bind, .unbind = mtk_dsi_unbind, }; static int mtk_dsi_bind(struct device *dev, struct device *master, void *data) { return mtk_dsi_create_conn_enc(dev, master); }在mtk_dsi_create_conn_enc中,DSI驱动创建属于自己的drm_encoder和drm_connector,并把encoder的possible_crtcs设为对应CRTC掩码。这里的掩码要和大框架里CRTC的index对上,否则后续原子提交时会找不到可用的CRTC,屏幕自然点不亮。这是常见的配置错误点,后面排错部分我会再提。
4.2 mipi_tx 物理层与时钟初始化
DSI控制器只是逻辑层,真正的电气信号由MIPI D-PHY物理层产生,在MTK平台对应mtk_mipi_tx.c。设备树里DSI节点通过phys引用了mipi_tx0,所以驱动里可以调用phy_init、phy_power_on来初始化和供电。
某个Panel链路是否稳定,很大程度取决于通道数和时钟参数配置。MIPI DSI速率计算遵循的原理是:
- 先算像素时钟:pixel_clock = htotal × vtotal × fps
- 需要的DSI数据速率:data_rate = pixel_clock × bpp ÷ lane_num
- 实际PHY时钟:因为MIPI是DDR双沿采样,PHY输出频率 = data_rate ÷ 2
以1080x1920、60Hz、24bpp、4 lane为例,假设htotal=2244,vtotal=2244(包含了前后肩和porch),像素时钟大概1920×2244×60 ≈ 258.5MHz,然后258.5 × 24 ÷ 4 ≈ 1551Mbps,PHY时钟约775.5MHz。具体值还要根据Panel的数据手册微调,过高可能信号不稳、过低可能刷不满帧率。
实际调试中我经常通过查看cat /sys/kernel/debug/dri/0/state来确认当前开启的时钟和lane配置。MTK的MIPI TX驱动也会在probe或power_on阶段根据设备树里的lane数设定寄存器。之前碰到过“画面左边有一条绿边”的问题,最后查下来是lane映射配置有误,数据错位导致的,跟时钟无关。
4.3 Connector 与 Panel 的绑定链路
MTK平台通常通过panel设备树节点挂接具体的显示屏驱动,panel驱动实现drm_panel_funcs,包含get_modes、prepare、enable、disable、unprepare等回调。DSI的Connector在get_modes中会调用drm_panel_get_modes,把Panel支持的timing转换为drm_display_mode加入模式列表。
上电时序一般是这样:
drm_panel_prepare:拉reset引脚、打开电源、初始化寄存器。drm_panel_enable:发送退出睡眠命令、开启显示。- 关闭时顺序刚好相反:先关显示、再关背光、再拉reset。
TFT类LCD的模组对时序要求很严格:先说一句“本Panel需要reset拉低至少10ms再拉高”,但如果实际驱动里只等5ms,部分Panel会偶尔白屏。排查这种问题最有效的办法是点亮前后对比逻辑分析仪波形,或者反复开关屏测试。大部分市面上成熟的Panel datasheet都会明确写时序表,做显示驱动的人一开始看不见得,经历几次问题后就会特别重视这些参数。
5. 与实战相关的模式配置:竖屏改横屏的场景
5.1 DRM下横竖屏怎么切
“MIPI DSI DRM竖屏改横屏显示”这个话题其实包含两种完全不同的需求:
一是物理面板本身就竖着,但系统UI要以横屏方式运行。这种一般不改内核DRM驱动,而是在Android或应用层做旋转,底层只要保证输出的timing与面板物理方向一致。Android里通过PersistProperties设persist.sys.rotation.euler或修改SystemUI的ro.orientation实现。
二是产品设计需要面板横放,比如原本是手机竖屏屏,被翻转为横屏使用。这时候必须改面板驱动中的timing,把水平方向active区从1080改成1920,垂直从1920改成1080,同时调整HFP/HBP/VFP/VBP等porch参数。
真正的trick在于,改了timing不等于物理面板就能显示,很多竖屏Panel的源极/栅极驱动IC是按固定扫描方向设计的。把长宽对调之后,必要时还需要同时反转扫描方向、修改初始化序列里的entry mode寄存器,常见的Action有PASET、CASET、BURST_MODE等。这些通常要到Panel厂拿横屏使用的初始化代码,或者自行对照datasheet调整。
5.2 时序参数怎么算
无论竖屏还是横屏,最终都要落到一组精确的时序参数上。以1920x1080横屏60Hz、典型面板参数为例:
- H_ACTIVE = 1920
- H_FRONT_PORCH = 80
- H_BACK_PORCH = 64
- H_PULSE = 8
- H_TOTAL = 1920 + 80 + 64 + 8 = 2072
- V_ACTIVE = 1080
- V_FRONT_PORCH = 10
- V_BACK_PORCH = 30
- V_PULSE = 4
- V_TOTAL = 1080 + 10 + 30 + 4 = 1124
像素时钟 = 2072 × 1124 × 60 ≈ 139.8MHz。这个结果就是MIPI计算的数据源。经验法则是普通Panel的porch不建议太小,至少留够10像素以上的水平消隐,否则高频下可能出现水平噪声线。竖屏改成横屏后,porch值未必还是原来的值,需要同样按照新分辨率重新估算一版,最好找Panel厂商确认。
5.3 panel上电和backlight时序
改横屏显示后最容易被忽略的就是Panel初始化序列的上下电时序。很多Panel的初始化命令序列包含A0入口、Page切换、Gamma设置、MADCTL(Memory Data Access Control)、扫描方向设置。MADCTL寄存器的bit值决定了RGB的BGR顺序、行扫描方向、列扫描方向,横屏和竖屏的值通常是镜像或旋转180度的关系。只改timing不改MADCTL,轻则显示方向不对,重则内容错位。
另一个明显问题是背光启动时序。常见现象是:屏幕有图像,但一直黑着,看着就像没点亮。原因是backlight的enable没有放在drm_panel_enable之后,或者背光PWM对应的GPIO没有正确申请。排查时可以手动写GPIO值测试背光是否正常,背光正常再检查PWM配置。可调的PWM频率也需要注意,过低会看到闪烁,一般选择面板手册建议的20k~30kHz区间。
6. 常见问题与排查技巧实录
6.1 组件绑定失败的典型原因
如果不理解组件框架,看到这类日志会一头雾水。常见情况是DTS里某个显示输出节点已经添加到了match列表,但这个节点对应的驱动没有正常probe,组件框架一直等它,DRM master的bind迟迟不执行。日志表现就是/dev/dri/card0不出现,dmesg里没有DRM bind的打印。
解决办法首先是确认每个参与链路的驱动是否都执行了probe:
dmesg | grep -E "mtk_dsi|mtk_dpi|panel|mediatek-drm"如果能确认某个驱动没有probe,又能找到-EPROBE_DEFER提示,多半是它依赖的时钟、电源域、或PHY还没有就绪。这时候可以看完整启动日志里deferred probe的统计:
cat /sys/kernel/debug/devices_deferred它会直接告诉你哪个设备还在等什么资源。如果某个组件驱动确实不需要参与当前配置,从match列表和设备树里同时移除它,DRM就能正常起来。
6.2 屏幕不亮但内核无报错的排查
屏幕不亮分很多种:视频信号没出来、Panel没初始化、背光没亮、或者只有某路硬件错误。如果内核无任何明显报错,我建议按顺序查这四步:
- 确认drm设备状态:
cat /sys/kernel/debug/dri/0/state,看crtc/plane/connector是不是on。 - 查看当前mode:
cat /sys/class/drm/card0-DSI-1/status和cat /sys/class/drm/card0-DSI-1/modes,确认分辨率和对端设备状态是connected。 - 测量背光使能脚:用GPIO命令直接强制拉高,如果亮了,问题在PWM或背光驱动。
- 检查Panel上电和reset引脚顺序:必要时上逻辑分析仪抓时序。
这类问题最大的障碍是“静默失败”。如果你能看到显示内容只是黑屏,先排除背光问题;如果整机完全像没接屏,则优先怀疑DSI物理链路或Panel没退出睡眠。
6.3 日志工具与关键节点
调试MTK DRM,在内核启动参数里可以加drm.debug=0x1f或drm.debug=0xff,这会输出大量DRM核心的日志。配合fbcon调试时,还要确保内存足够,避免fbcon输出干扰。
常用关键节点:
/d/dri/0/state:当前各对象的状态和连接关系。/d/dri/0/gem_names:查看GEM内存对象。/sys/class/drm/card0-*/status:连接状态。/sys/class/drm/card0-*/modes:支持的显示模式列表。/sys/kernel/debug/devices_deferred:等待defer probe的设备。
在MTK平台上还经常要看/d/mali或者/d/disp(部分平台有显示硬件debug节点)。如果Debugfs里找不到显示调试入口,说明对应内核config可能没打开,需要打开CONFIG_DRM_MEDIATEK_DEBUG或CONFIG_DEBUG_FS。
6.4 排查速查表
我把日常遇到的高频问题整理成了一张表,方便直接对照:
| 现象 | 可能原因 | 快速排查手段 |
|---|---|---|
| 没有/dev/dri/card0 | 组件未全部bind、match不匹配 | 看devices_deferred,检查DTS节点 |
| 有card0但屏幕黑 | 背光未亮、Panel未初始化 | 先强制拉背光GPIO,再量reset时序 |
| 出图但颜色偏绿/偏紫 | lane映射、MIPI传输错位 | 检查DSI lane数和映射参数 |
| 出图但方向不对 | MADCTL扫描方向错误 | 对照Panel手册改MADCTL或init序列 |
| 闪屏 | 时钟不足或背光PWM频率过低 | 重算pixel clock,调高PWM频率 |
| 竖屏改横屏后切边 | porch或active区域配置错误 | 重新计算timing,与Panel确认 |
这张表基本覆盖了显示驱动开发前期的常见问题。实际操作中还要注意,不要把单个现象的排查思路锁死,显示链路是一整条,任何一个环节出问题都可能表现为“屏幕不亮”或“画面异常”。
根据我个人这几年的经验,MTK DRM的初始化流程并不复杂,复杂的是把组件框架、DRM核心对象、硬件IP三者之间那些隐式的依赖关系想明白。单看代码可能绕很久,但只要自己把设备树节点、probe顺序、bind顺序、以及CRTC/Encoder/Connector的创建流程完整走一遍,后面遇到任何点亮问题都能很快定位到具体环节。再分享一个小技巧:刚开始调试时,建议把drm_debug打开,并给mtk_drm_bind和各个组件的bind回调临时加上打印,这样框架一调你就知道跑到哪儿了,能省掉很多拿示波器盲猜的时间。