第一次碰MTK平台的DRM显示驱动,大多数人都是在probe流程里迷路的。顶层明明挂着标准drm_driver的皮,真正干活的却是一套叫component的机制——匹配、绑定、拆解绕一大圈,最后才回到KMS对象的创建。再加上MTK显示管线本身又是OVL、RDMA、COLOR、DSI这一堆DDP组件拼起来的,三层结构叠在一起,初期排查问题基本靠猜。
这篇文章把我自己梳理这套初始化过程的思路完整记录下来:从drm设备对象的创建、component框架的bind顺序,到CRTC、PLANE、ENCODER、CONNECTOR逐个挂载,再到竖屏改横屏这种看起来和初始化无关、实际上和mode配置强相关的场景。适合在MTK平台调显示、移植新SoC、或者被公司老代码里各种comp回调折磨的驱动工程师,希望能帮你少走几趟弯路。
1. 从传统FB到DRM/KMS:MTK显示子系统的架构变迁
1.1 为什么MTK要把显示路径全部纳入KMS
老平台的MTK显示方案基本都是fbdev那套思路,一个/dev/fb0节点直通底层LCD控制器,想多图层就得自己开一堆FB或者通过私有ioctl操作ovl硬件,多屏显示基本靠补丁叠补丁。后来DRM/KMS成了Linux桌面和Android的主流显示框架,MTK也顺势把整个显示路径迁了过去。
KMS带来的核心优势是统一抽象:crtc代表显示控制器输出的完整流水线,plane代表图层,encoder代表信号编码器,connector代表物理输出口。用户空间只需要通过drm ioctl操作这些对象,不再需要关心具体是OVL还是RDMA在HW层面怎么配。MTK的显示管线里恰好有一堆自研模块,比如layer mixer(OVL)、读数据模块(RDMA)、色彩调校模块(COLOR/CCORR/GAMMA/AAL),它们天然适合被折叠成KMS内部逻辑。
还有一个现实原因:Android的HWC(Hardware Composer)走的就是DRM。MTK如果不想在HWC层做大量vendor hack,就必须把显示驱动的对外接口做得足够标准。所以你会在新版内核的drivers/gpu/drm/mediatek/目录下看到越来越多的标准drm_bridge、drm_panel、drm_atomic_helper代码,这些都是被HWC和应用生态倒逼出来的。
1.2 驱动文件地图与设备树对应关系
先给一张“地图”,方便后续对号入座。以Linux 5.15附近的内核为例,MTK DRM驱动主要文件如下:
| 文件 | 职责 | 对应的设备树节点 |
|---|---|---|
| mtk_drm_drv.c | 全局drm_driver、master绑定、component match | display-subsystem |
| mtk_drm_crtc.c | CRTC创建、显示流水线配置、vblank管理 | 由comp列表动态组合 |
| mtk_drm_plane.c | plane创建与状态更新 | OVL/OVL0/OVL1等 |
| mtk_dsi.c | DSI编码器/connector驱动 | dsi0、dsi1 |
| mtk_dpi.c | DPI并行接口驱动 | dpi0 |
| mtk_dp.c | eDP/DisplayPort驱动 | dp_tx等 |
| mtk_drm_gem.c | GEM对象、dma-buf管理 | 无需单独节点 |
设备树侧,关键不是某个节点的compatible,而是各显示组件节点之间的连接关系。一个比较典型的MTK显示配置片段长这样:
&ovl0 { compatible = "mediatek,mt8183-ovl"; reg = <0 0x14000000 0 0x1000>; interrupts = <GIC_SPI 224 IRQ_TYPE_LEVEL_LOW>; clocks = <&mmsys CLK_MM_OVL0>; power-domains = <&spm MT8183_POWER_DOMAIN_DISP>; }; &dsi0 { compatible = "mediatek,mt8183-dsi"; reg = <0 0x14010000 0 0x1000>; clocks = <&mmsys CLK_MM_DSI0>; phys = <&mipi_tx0>; }; &display_subsystem { compatible = "mediatek,display-subsystem"; mediatek,dpi = <&dpi0>; mediatek,dsi = <&dsi0>; };注意display_subsystem节点里的mediatek,dsi、mediatek,dpi这些属性,新老内核解析方式略有不同,但思路一致:它列出当前平台所有可用的输出接口,驱动拿到这些phandle后,再去逐个匹配对应的组件driver。
1.3 显示管线与组件模型的基本概念
MTK显示路径在硬件上是串联的。比如一个常见的手机内屏链路是:
OVL0 -> RDMA0 -> COLOR0 -> DSI0 -> Panel这条链路上每个硬件模块在驱动里都对应一个“组件”(component)。为什么叫组件而不叫普通platform设备?因为这条链路是一个整体,任何一个环节缺失,显示都无法工作。单独让OVL先probe没有任何意义,必须等整条链路上所有模块都准备好,再一次性完成初始化。
组件模型在MTK DRM里还有第二层作用:一条SoC常有多个显示输出,比如DSI0接内屏、DP接外接显示器。每个输出对应一条完整的流水线,但OVL/RDMA这些资源又可能在不同流水线之间复用或独立配置。组件框架允许驱动先收集所有硬件块,再通过代码逻辑把它们分成不同的CRTC。
从实现上讲,初始化做的最核心一件事,就是把设备树里那些散落的节点、加上各组件driver注册时暴露的能力,汇聚成一份KMS对象清单。后面章节的代码,本质上都在做这件事。
2. 初始化入口:从module_init到KMS设备注册
2.1 platform driver注册与drm设备对象创建
MTK DRM驱动的入口在mtk_drm_drv.c,通过module_platform_driver注册一个名为mtk_drm的platform驱动。它绑定的设备就是设备树里的display-subsystem节点。
probe函数的关键逻辑,我用一个简化版本展示(以5.15附近内核为参考,新内核API略有调整):
static int mtk_drm_probe(struct platform_device *pdev) { struct mtk_drm_private *private; struct drm_device *drm; int ret; private = devm_kzalloc(&pdev->dev, sizeof(*private), GFP_KERNEL); if (!private) return -ENOMEM; private->dev = &pdev->dev; dev_set_drvdata(&pdev->dev, private); drm = drm_dev_alloc(&mtk_drm_driver, &pdev->dev); if (IS_ERR(drm)) return PTR_ERR(drm); drm->dev_private = private; private->drm = drm; ret = mtk_drm_collect_components(private); if (ret) goto err_put_drm; ret = component_master_add_with_match(&pdev->dev, &mtk_drm_ops, private->match); if (ret) goto err_put_drm; return 0; err_put_drm: drm_dev_put(drm); return ret; }有几个细节值得注意。drm_dev_alloc只是创建一个drm_device对象,此时还没有注册到系统中,真正的对外暴露是后面drm_dev_register的事。mtk_drm_collect_components会遍历设备树,把在display_subsystem节点里声明的DSI、DPI、DP等phandle全部解析出来,记录到private->comp_node[]数组里,同时构造一个component_match。
component_master_add_with_match这一步是整条初始化链路的“发令枪”。它不会阻塞等待,而是告诉内核:这个master的条件是我刚才设置的match,当match里所有组件都注册到位,就调用mtk_drm_ops.bind。这就是标题里“组件框架”真正发挥作用的起点。
2.2 component框架:为什么MTK不能直接按probe顺序初始化
很多人在新手阶段会问一个问题:既然设备树已经把节点都列出来了,为什么不按顺序逐个probe、逐个初始化?
原因在于MTK显示硬件模块之间有很强的资源耦合。OVL、RDMA、COLOR这些模块通常挂在同一个电源域下,共享MMSYS的时钟控制。如果OVL先probe,它想申请时钟却发现MMSYS时钟还没准备好;如果DSI先probe,它想拉mipi_tx的电,结果phy的电源域还没上。单靠platform驱动的probe顺序无法可靠解决这种跨模块依赖。
component框架解决的是“多个独立驱动注册,但需要同时就绪后再统一初始化”的问题。机制本身很简单:
- 从设备驱动的probe里调用
component_add(dev, &ops),把自己注册进component系统。 - master驱动构造一个
component_match,声明自己需要哪些从设备。 - 当match中所有从设备都注册完成后,系统回调master的
bind函数。
MTK的每个显示组件driver,比如mtk_dsi、mtk_dpi,在probe的最后都会调用component_add(dev, &mtk_dsi_component_ops)。单独看这些probe,它们各自做的事情并不多:分配私有结构体、解析寄存器地址、处理时钟,然后就把自己“挂起”,等待master召唤。
这种设计的另一个好处是容错。某个组件probe失败或者返回-EPROBE_DEFER,不会影响其他组件继续注册。master的bind只会在“所有人凑齐”时触发,否则整个显示驱动的初始化就停留在等待状态。开发时遇到“drm device没起来”,很多时候就是某个组件没probe成功,导致bind从未被调用。
2.3 mtk_drm_bind:master绑定的总控逻辑
当所有component都ready后,mtk_drm_ops.bind被调用。这是一个承上启下的函数,承上是所有组件已经注册,启下是该创建真正的KMS对象了。
static int mtk_drm_bind(struct device *dev) { struct mtk_drm_private *private = dev_get_drvdata(dev); int ret; ret = mtk_drm_kms_init(private->drm); if (ret < 0) return ret; ret = drm_dev_register(private->drm, 0); if (ret < 0) goto err_deinit; mtk_drm_fbdev_init(private->drm); return 0; err_deinit: mtk_drm_kms_deinit(private->drm); return ret; }mtk_drm_kms_init是整个初始化的核心,逻辑大致如下:
static int mtk_drm_kms_init(struct drm_device *drm) { struct mtk_drm_private *private = drm->dev_private; int ret; ret = component_bind_all(drm->dev, drm); if (ret) return ret; ret = mtk_drm_crtc_create(drm); if (ret) goto err_component_unbind_all; ret = drm_vblank_init(drm, MAX_CRTC); if (ret) goto err_crtc_cleanup; drm_mode_config_init(drm); drm->mode_config.funcs = &mtk_drm_mode_config_funcs; ... }这里有个容易混淆的顺序问题:先调component_bind_all,再创建crtc。为什么?因为crtc创建时需要知道某一条显示流水线上具体有哪些comp。DSI、DPI这些component的bind函数会初始化对应的encoder和connector,但这些对象最终要挂到crtc的pipeline上。必须先让所有组件把自有资源准备好,crtc才能把它们组织成完整的KMS对象。
component_bind_all之前,所有组件driver的bind函数会被逐个调用。MTK各comp的bind函数通常做两件事:一是初始化自己的DRM子对象,比如DSI的bind里会drm_encoder_init和drm_connector_init;二是把自己注册到master的mtk_drm_private中,比如list_add到某个comp链表。
如果中途某个bind返回错误,component_bind_all会触发回滚,已经bind的组件会被unbind。这种错误回滚机制在实际开发中非常重要——之前我遇到过DSI的bind里申请dma_buf失败,结果整个KMS初始化退到只剩一个空drm_device,连卸载模块都做不干净。后续排查才知道,必须遵循组件框架的约定,任何失败都要让上层能走unbind路径。
3. KMS核心对象初始化:CRTC、PLANE、ENCODER、CONNECTOR
3.1 CRTC:显示流水线的KMS化身
CRTC在KMS里代表一条完整的显示输出流水线。对MTK来说,一个CRTC通常对应一条物理流水线,比如“OVL0 + RDMA0 + COLOR0 + DSI0”整体构造成一个CRTC。
mtk_drm_crtc_create的主要工作分三步。第一步,遍历private->comp,按每组流水线里包含的comp节点,确定每个crtc的成员列表。怎么判断哪些comp属于哪个crtc?老版本MTK有一个全局路径表mtk_mmsys_routes或者mtk_ddp_main_path,新版本则通过dts里mediatek,pipe之类的属性或者comp的type字段来分组。不同平台差异很大,但本质都是描述“这条链路从哪个模块开始、经过哪些中间模块、最后从哪个接口出去”。
第二步,为每个crtc申请struct mtk_drm_crtc,初始化互斥锁、事件对象,注册vblank相关的回调,并请求中断。MTK显示控制器通常有frame done中断或者underflow中断,这些中断在crtc注册时一并申请。
第三步,调用drm_crtc_init_with_planes或者类似接口,把crtc注册进drm core。有些平台还会为crtc创建对应的plane。
这里要特别提醒:MTK的crtc不是简单把KMS的crtc和硬件寄存器一一对应,它更像“软件虚拟显示控制器”。一个crtc内部管理着整条流水线的ddp模块。所以你会看到crtc的atomic_enable、atomic_flush回调里,会依次对路径上的每个comp做配置。初始化顺序决定了这个“路径”概念在各回调里如何被遍历。
3.2 Plane:OVL图层如何进入KMS
plane在MTK里最常见的对应物是OVL模块。OVL硬件本身支持多层叠加,但不同平台支持的layer数不同,有的OVL0支持4层、OVL1支持2层,有的还把OVL的某层专门给硬件cursor用。
plane初始化在mtk_drm_plane_create中完成。这个函数会调用drm_universal_plane_init,并指定plane类型(primary/overlay/cursor),同时注册mtk_plane_funcs、mtk_plane_helper_funcs等回调集合。
关键点在于plane的state管理。MTK给plane定义了专门的mtk_plane_state,里面除了标准的drm_plane_state,还会缓存当前fb的地址、颜色格式、alpha、zpos、rotation等信息。atomic_check阶段会检查这些参数是否满足OVL硬件的能力,比如是否支持该pixel format、旋转90度后width/height是否超过硬件上限。
底层配置的入口在mtk_plane_atomic_update。它会把plane state里的crop信息、fmt信息翻译成OVL寄存器能吃的格式,然后写到mtk_crtc的pending层列表里。后续mtk_crtc_atomic_flush会真正把这些layer配置下发到硬件。
对开发来说,plane相关的问题大多是格式不匹配或者尺寸超限。以前排查过一个花屏问题,最后发现是scaling的时候crop没有按16字节对齐,OVL硬件读数据就错位了。这类问题在plane初始化和state校验阶段如果能写清楚约束,可以避免一大半。
3.3 Encoder与Connector:各接口组件的最后一块拼图
encoder和connector在MTK DRM里通常不是由master直接创建的,而是由具体接口组件在bind阶段创建的。以DSI为例,mtk_dsi_bind会:
static int mtk_dsi_bind(struct device *dev, struct device *master, void *data) { struct drm_device *drm = data; struct mtk_dsi *dsi = dev_get_drvdata(dev); ret = mtk_dsi_create_connector(drm, dsi); if (ret) return ret; ret = drm_encoder_init(drm, &dsi->encoder, &mtk_dsi_encoder_funcs, DRM_MODE_ENCODER_DSI, NULL); if (ret) return ret; drm_encoder_helper_add(&dsi->encoder, &mtk_dsi_encoder_helper_funcs); dsi->encoder.possible_crtcs = mtk_drm_find_possible_crtcs(drm, dev); ... if (dsi->panel) { ret = drm_panel_attach(dsi->panel, &dsi->connector); if (ret) return ret; } return 0; }这段代码有两个核心点。第一,connector是在component bind阶段创建的,而不是在DSI的platform probe阶段。所以你在DSI probe时哪怕用drm_get_device也找不到drm_device,必须等master的bind流程真正跑起来。很多初学者在写调试代码时,习惯在probe里打日志看connector是否创建,结果发现永远是为空——正确的位置应该在bind里。
第二个核心点是possible_crtcs。一个encoder可能连接到多个crtc,比如某些平台DP可以同时接内屏crtc和外接crtc。mtk_drm_find_possible_crtcs遍历已创建的crtc,检查它是否包含该encoder所对应的输出端口,由此建立encoder到crtc的关联。这个关联如果配错,用户空间设置mode时就会出现“cannot find any crtc”的错误。
DSI的connector通常会进一步设置polled属性。如果面板支持热插拔,就设DRM_CONNECTOR_POLL_HPD;如果不支持,就设成DRM_CONNECTOR_POLL_CONNECT或者直接0。MTK的eDP/DP接口一般有HPD,DSI内屏则通常没有。
3.4 mode_config与atomic提交框架
当crtc和plane创建完成,mtk_drm_kms_init会调用drm_mode_config_init,然后设置MTK自己的mode_config.funcs。这个funcs结构体决定了整个atomic提交的入口:
static const struct drm_mode_config_funcs mtk_drm_mode_config_funcs = { .fb_create = mtk_drm_fb_create, .atomic_check = drm_atomic_helper_check, .atomic_commit = mtk_drm_atomic_commit, };MTK没有完全使用drm_atomic_helper_commit,而是自定义了mtk_drm_atomic_commit。原因在于MTK显示硬件commit时可能涉及多个crtc同时刷新,而不同crtc之间的power domain和clk关系比较复杂。自定义commit函数可以控制提交时机,保证在drm_atomic_helper_commit内部执行到flush时,所有crtc的layer状态已经准备好。
对初始化流程来说,mode_config注册完成意味着KMS已经被drm core认为是“可用了”。此时drm_dev_register会创建/dev/dri/card0节点,用户空间的modetest、weston、HWC才能真正开始访问设备。
整个初始化从drm_dev_alloc到drm_dev_register,中间的component匹配和bind其实就是“把底层硬件能力翻译成KMS对象清单”的过程。后面调试中发现某个对象没有创建,基本都能沿这个链路顺藤摸瓜。
4. DSI接口初始化与竖屏改横屏实战
4.1 mtk_dsi_bind:从组件到KMS对象的完整路径
DSI是手机内屏最常见的输出接口,值得单独展开。mtk_dsi_bind里除了创建encoder和connector,还会做很多和MIPI协议相关的初始化准备。
比如DSI的connector创建函数mtk_dsi_create_connector,会先初始化drm_connector,再把mtk_dsi用wrapper结构包起来:
static int mtk_dsi_create_connector(struct drm_device *drm, struct mtk_dsi *dsi) { int ret; dsi->connector.polled = DRM_CONNECTOR_POLL_CONNECT; ret = drm_connector_init(drm, &dsi->connector, &mtk_dsi_connector_funcs, DRM_MODE_CONNECTOR_DSI); if (ret) return ret; drm_connector_helper_add(&dsi->connector, &mtk_dsi_connector_helper_funcs); return 0; }DSI驱动真正的“电源启动”通常在encoder的atomic_mode_set或者enable阶段完成。它会按顺序打开clk、拉起mipi_tx的phy、执行panel的prepare、发送init code、最后drm_panel_enable。
这里有个很常见的坑:panel的probe顺序和dsi的probe顺序不一定一致。DSI在probe阶段会通过drm_of_find_panel_or_bridge尝试找panel,如果panel还没probe完,返回的是-EPROBE_DEFER,DSI驱动要做defer处理。但这是platform probe层面的defer,和component框架是两回事。两者叠加后,新手容易把-EPROBE_DEFER和component匹配失败搞混。区别在于:panel的defer会导致DSI这个component根本不会add,master拿不到全部组件,bind永远不会被调用;而component匹配失败则是bind被调用后某个组件在bind里出错了。
4.2 panel的接入与MIPI初始化时序
panel的接入,现代内核里用的是drm_panel框架。DSI在probe阶段拿到的是struct drm_panel *panel,而这个panel对象由单独的panel驱动创建。常见的MTK DSI panel驱动会定义一组初始化序列,比如mtk_drm_panel_init、prepare、enable。
这些函数不是简单地在初始化时跑一遍就结束。prepare阶段通常要保证MIPI时钟和Data Lane处于正确的状态,然后发sleep_out、显示初始化命令;enable阶段才真正开显示。KMS的atomic递交过程中,encoder的enable回调会调用mtk_dsi_poweron、mtk_dsi_start来启动DSI控制器向外发送像素数据。
一些DSI的初始化参数,如mipi_tx的驱动电流、阻抗、指的是连续时钟还是DDR模式,都来自设备树或panel驱动里的描述。如果初始化时序不对,最常见的现象是:背光亮了但画面是黑的,或者出现雪花噪点。
这里分享一个排查经验:遇到DSI黑屏但背光亮,先用示波器量MIPI的Data Lane有没有波形,区分是“根本没有发数据”还是“发了数据但panel不认”。前者查DSI控制器clk和电源门控,后者查init sequence和timing参数。
4.3 竖屏改横屏到底改哪里
热搜里经常出现“mipi dsi drm竖屏改横屏显示”这个需求,很多人在panel驱动里硬改timing,效果往往很差。我说说这个问题的正确打开方式。
第一种做法:直接改panel的mode,把hdisplay和vdisplay对调。以一块1080x1920的竖屏为例,竖屏timing可能是:
| 参数 | 值 |
|---|---|
| hdisplay | 1080 |
| hfp / hbp / hsa | 40 / 60 / 60 |
| vdisplay | 1920 |
| vfp / vbp / vsa | 8 / 10 / 4 |
如果简单对调成1920x1080,hdisplay翻倍,vdisplay减半,但porch必须重新设计,否则总像素数和刷新率对不上。非要改的话,要保持pixel clock基本不变,同时保证hsync/vsync的极性Correct。这个计算看起来简单,实际调起来要兼顾panel的规格书,很多panel对porch范围有严格限制,硬填会导致显示偏移甚至不亮屏。所以我不建议用这种硬改方式做横屏。
第二种做法,也是我推荐的做法:显示方向旋转交给plane的rotation属性处理。MTK的OVL硬件本身支持ROT_90/180/270,只需要在plane的atomic_check里判断drm_plane_state.rotation,然后翻译成OVL寄存器配置即可。用户空间只需要通过drmModeSetPlane带上rotation flag,或者在系统层面设置orientation,驱动就能把物理竖屏的内容旋转后横屏输出。这种方式不改变timing,不碰DSI速率,兼容性最好。
第三种做法:如果一定要在内核侧改,应该在connector的get_modes回调里处理,而不是让panel驱动把原生的横向mode写死。可以在get_modes里对drm_mode_duplicate出来的mode做宽高交换,同时保留原有pixel clock和porch比例,再通过drm_mode_set_crtcinfo重新计算。但这样做需要考虑DSI时钟是否还有余量,毕竟横屏单行像素数变多后,blank time可能会被压缩。
关于这套流程,我实际踩坑最多的是“rotation flag传到底层但OVL配置没跟上导致花屏”。OVL旋转不仅影响显示方向,还会影响crop计算和带宽,尤其是双层plane都旋转时,带宽很容易飙超。这类问题必须在atomic_check里加防护,否则运行时只能靠抓hdmi波形来猜,非常痛苦。
5. 初始化异常与调试技巧实录
5.1 component匹配失败:最典型的“绑定未完成”
MTK DRM初始化最常见的失败,就是整个绑定流程根本没有执行。现象是/dev/dri/card0不存在,dmesg里也看不到mtk_drm_bind相关的成功日志,只有一句类似“cannot find component”或者干脆静默。
这种问题本质是:master的match里至少有一个component没注册。排查思路按三个方向走。
方向一:看设备树。dsi0、dpi0、dp_tx这些节点的status是不是okay?某个节点如果被写成disabled,component_add就不会执行,master自然等不到它。很多从别的平台copy过来改的设备树,最容易漏掉这一点。
方向二:看有没有-EPROBE_DEFER。组件driver如果在probe阶段依赖的某个资源(比如panel)还没就绪,会返回-EPROBE_DEFER,内核会把它放到defer链表中稍后重试。如果panel一直不probe成功,DSI组件就永远注册不了。用dmesg | grep -i defer可以快速看到是哪个设备在等待。
方向三:看各组件driver的probe是否执行到最后一行。我习惯在各组件probe的component_add前加一句dev_info,确认每个组件都走到了“准备挂载”这一步。如果某个组件连probe都没执行完,去查它的时钟、中断或reset GPIO。
还有一些情况是match函数写得太严格,比如mtk_drm_component_match里拿dev->of_node和private->comp_node[type]比较时节点地址不相等。这种情况多发生在设备树被UE或其他机制reparent后,用of_parse_phandle得到的node和组件driver实际持有的node不是同一个。对策是改用of_node_name_eq或者直接比较compatible属性。
5.2 CRTC创建失败与第一帧黑屏
如果component匹配成功、bind也执行了,但仍然黑屏或卡在初始化,问题往往在crtc创建阶段。mtk_drm_crtc_create返回错误,常见的直接原因是drm_crtc_init_with_planes失败或者中断申请失败。
中断申请失败多见于IRQ号重复或共享IRQ配置错误。MTK的OVL、RDMA、DSI各自有独立中断,但有时设备树中两个节点误配了同一个IRQ,crtc建立时会request_irq失败。遇到这种情况查设备树节点的interrupts配置即可。
还有一种“软失败”:crtc创建成功了,drm_dev_register也成功了,但用户空间打开/dev/dri/card0后拿不到mode。这种情况多半是connector没有被正确关联到crtc,具体表现是modetest输出列表里encoder或connector为空。排查时看mtk_dsi_bind里的possible_crtcs是否计算正确,这个值如果为0,drm core会把encoder视为不可用。
第一帧黑屏则是另一个故事。初始化完成后,如果atomic commit执行有报错,先看/sys/kernel/debug/dri/0/state,这个文件会dump当前所有objects的状态,包括plane是否active、crtc是否mode_changed。它比任何printk都直观。
5.3 调试工具与内核开关速查
我整理了一份常用调试工具表,按使用频率排序:
| 工具/开关 | 作用 | 常用命令 |
|---|---|---|
| drm.debug | 打开DRM core日志 | echo 0x1f > /sys/module/drm/parameters/debug |
| modetest | 查看KMS对象和mode | modetest -M mediatek |
| /sys/kernel/debug/dri/0/state | atomic状态dump | cat /sys/kernel/debug/dri/0/state |
| dmesg+component | 检查组件绑定日志 | dmesg | grep -i component |
| dmesg+defer | 检查deferred probe | dmesg | grep -i defer |
| devmem/io | 读取硬件寄存器 | devmem 0x14000000 |
drm.debug=0x1f是最常用的开关,它会输出包括DRM_UT_CORE、DRM_UT_DRIVER、DRM_UT_KMS在内的日志。打开后,modetest或者HWC的每次调用都会在串口留下大量KMS栈打印,filter“drm:[”可以只看drm core的调用流程。
如果板子上没有modetest,可以临时用v4l2-compliance或者Android的hwclock来验证显示通路,但最轻量的还是交叉编译libdrm自带的modetest,文件很小,推到板上就能跑。
5.4 几个我踩过的坑
写几个平时文档里不太会提的经验。
第一个坑:设备树节点的compatible写错,但dmesg没有明显报错。组件driver的of_match_table没匹配到节点时,probe根本不会被调用,component_add自然也没有。看起来像“所有组件都没准备好”,实际上只是compatible拼写错误。排查办法是cat /sys/firmware/devicetree/base/.../compatible看一眼。
第二个坑:panel的prepared标志位没有正确初始化,导致第一次enable时直接把drm_panel_prepare跳过,panel根本没有进入工作状态。这类问题通常发生在panel驱动被多个DSI实例共用时,panel_device结构体里的mutex和标志位状态残留。每次disable之后要把标志位清干净。
第三个坑:drm_dev_register成功之后,HWC立刻开始访问设备,而driver内部有些初始化还没完成,比如fbdev还没有创建。如果有竞态窗口,用户空间可能读到不完整的能力列表。解决方案是确保drm_mode_config_init里所有min_width、max_width等能力字段在register前都赋值,不要让drm core用默认值去收fb_create请求。
第四个坑最隐蔽:MTK的MMSYS时钟属于clk_rate_exclusive,初始化阶段某个组件driver如果先申请了时钟,后面clk_set_rate会失败。曾经遇到DSI mode设置时pixel clock始终提不上去,查了半天是MMSYS里一个enable计数为0但rate被某段代码意外锁定。处理方式是所有clk操作都走clk_set_rate加返回值检查,同时在crtc的atomic_enable里统一设置,而不是分散到各组件bind中。
6. 关于这套流程,我沉淀下来的一些习惯
写到这里,想分享几个自己长期调显示驱动形成的习惯,算是一个从业者的一点总结。
第一,初始化代码里日志宁可多不要少。MTK DRM链路长,component bind失败的错误信息经常晚于问题节点好几层才暴露。所以我在每一个可能返回错误的分支上都会打印dev_err,并带上设备节点的全名。比如在DSI的bind里,如果有drm_encoder_init失败,直接打:
dev_err(dev, "failed to init encoder, ret=%d, node=%s\n", ret, dev->of_node ? dev->of_node->full_name : "unknown");这样一条日志就能定位到具体节点,不需要再去做二分排查。
第二,版本差异要心里有数。MTK各平台的内核版本跨度非常大,有的还在4.19上用老式路径表,有的已经切到6.1的drm_pipeline。看代码时先确认mtk_drm_kms_init所在的文件版本,不要拿着6.1的代码去套4.19的结构体。我自己的习惯是把某个SoC的显示驱动整个目录打包存成tar,每次review代码时对照着看,比记在脑子里可靠得多。
第三,遇到“玄学”问题先怀疑电源和时钟,再怀疑寄存器配置。显示驱动里90%的花屏、黑屏、闪烁,最终都能回溯到某个clk没开、某个power domain没上,或者某个timing计算里除数被截断了。DRM初始化流程本身给了一个很好的排查路径:从顶层drm object逐个往下追,总能找到是哪个组件没准备好。
MTK这套组件框架确实比标准DRM驱动多了不少门槛,但理清楚probe、component_match、bind、KMS对象创建这几层关系之后,再去看其他厂商的显示驱动也会顺手很多,因为底层的核心仍然是那几个KMS对象和它们的创建时序。希望这篇文章能把你在初始化的“迷宫”里带出来。