深入DRM CRTC:从drm_crtc.c看MTK平台KMS显示调度核心机制
2026/9/17 16:17:56 网站建设 项目流程

在内核DRM源码树里,drm_crtc.c算不上最显眼的文件——它没有drm_atomic.c那么宏大的事务机制,也没有drm_framebuffer.c那么直观的像素格式管理,但如果你想在MTK平台上真正搞懂KMS的工作方式,这个文件绕不开。这个系列从DRM整体框架开始,一路拆到plane、encoder、connector,今天我们终于要碰最核心的调度枢纽:CRTC本身。

先说个反直觉的结论:在MTK平台上,drm_crtc.c这个文件本身你几乎不会去改,但它定义的回调函数和执行顺序,直接决定了你后面在mtk_drm_crtc.c里写的每一行代码能不能按预期跑起来。换句话说,它是那个“大家都依赖它,但没人关心它怎么实现”的底层设施。CRTC的作用可以粗暴理解成“显示控制器”的软件抽象,它手里攥着帧缓冲(framebuffer)、时机(timing)、vblank中断这三样东西。没有它,用户态设了分辨率也不知道往哪儿输出;plane有数据也不知道什么时候扫描出去。理解它,才谈得上真正调通MTK的HDMI、DSI、DPI这些输出。

这篇文章我会从drm_crtc.c的接口和生命周期讲起,再深入到mode_set、vblank、event这几个最常见的代码路径,最后结合MTK平台上我们实际踩过的一些坑,讨论怎么基于这个文件去定位问题。内核版本以常见5.10/5.15为参考,MTK平台部分结合 mt8195/mt8188 的驱动结构来聊。

1. CRTC的角色定位:KMS流水线里谁才是真正的“总调度”

在KMS的抽象世界里,一套显示链路大概长这样:framebuffer(显存里的图像数据) ->plane(图层) ->CRTC(显示控制器) ->encoder(编码器) ->connector(物理接口) -> 屏幕。很多人一开始会把注意力放在plane和encoder上,因为plane决定你能叠加几个图层,encoder决定信号格式对不对。但真正卡住整个流水线的其实是CRTC——它决定了“什么时刻把哪个plane的图像扫描到哪个encoder上去”。

1.1 DRM显示流水线的任务分工

我用一个比较好理解的类比来帮忙捋顺:假设CRTC是一条汽车生产线上的总控台,plane是产线上的工位(每个工位负责把一种零件装上),framebuffer是零件仓库,encoder则是装好车之后把车开出去的物流通道。总控台不直接参与装配,但它决定产线什么时候启动、以什么节拍运转、零件从哪个仓取、装配完成的车辆什么时候交给物流通道。搬到DRM里,“什么时候启动”就是crtc_enable,“以什么节拍运转”就是mode timing(像素时钟、行场消隐参数),装配完成的车辆交给物流通道就是crtc->encoder的绑定关系。

在MTK的显示子系统里,这个“总控台”对应的是SoC内部的DISPLAY(比如mt8195里的DP_INTF、DSI0/1、DPI0/1)所连接的那个显示控制器。MTK的显示控制器本身是很典型的“内存读->图层合成->像素输出”硬件管线,而CRTC这个概念正好可以干净地包住这一整条硬件管线。你在代码里会看到mtk_drm_crtc这个结构体里挂着struct mtk_drm_crtc *mtk_crtc,它内部又包了struct drm_crtc basestruct mtk_drm_crtc_state *state。这里有个容易绕晕的点:drm_crtc.c里的struct drm_crtc是DRM core给所有驱动定义的公共结构,而mtk_drm_crtc是MTK驱动自己扩展的私有结构,两者通常靠container_of互相转换。

1.2 为什么MTK平台要比一般平台更重视CRTC

高通有msm_drm_crtc,NVIDIA有tegra_drm_crtc,每个厂商都有自己的一套CRTC实现,但MTK这边有个特点:它的显示链路天然是“多通路”的。一块SoC上可以同时挂eDP、HDMI、DSI、DPI多个输出,而且它们背后对应的是多个独立或半独立的显示控制器实例。这意味着MTK的DRM驱动里经常会出现“双CRTC”甚至“三CRTC”的拓扑。你在mtk_drm_drv.c里初始化时,如果crtc_num计算错了一位,或者某个输出意外绑到了另一个CRTC上,表现出来的就不是简单的黑屏,而是“主屏显示正常、副屏没有信号”或者“两块屏内容一模一样的克隆模式”,这类问题在代码审查阶段很难看出逻辑错误,只有对CRTC的边界职责有清晰认识才能快速定位。

drm_crtc.c的角色就在这:它是DRM core定义CRTC标准行为的地方。它不关心你这块SoC是MTK还是高通的,它只提供一套标准动作——创建CRTC、注册CRTC、清理CRTC、设置CRTC的模式、开关vblank、发送vblank事件。MTK驱动要做的,就是把自己的硬件行为嵌到这套标准动作里对应的回调函数中。

1.3 drm_crtc.c对外暴露的典型接口清单

我自己在分析内核源码时有个习惯:先不看实现细节,而是把文件对外“说了什么”捋一遍。drm_crtc.c暴露给外界的东西大概分四类:

  • CRTC对象的生命周期管理:drm_crtc_initdrm_crtc_init_with_planesdrm_crtc_cleanup
  • CRTC的模式设置:drm_mode_setcrtc(用户态ioctl入口)、drm_crtc_set_mode(内部核心函数)
  • vblank相关:drm_crtc_vblank_ondrm_crtc_vblank_offdrm_crtc_vblank_getdrm_crtc_arm_vblank_event
  • 属性与会话相关:drm_mode_crtc_set_gamma_sizedrm_crtc_force_disable_all

这四块几乎覆盖了你在调试一个KMS问题时需要关注的所有路径。下面我们逐个展开。

2. CRTC的出身:从drm_crtc_init到真正能用的完整生命周期

CRTC在代码里的“出生”其实非常朴素:drm_crtc_init或者它的升级版drm_crtc_init_with_planes。MTK平台一般会走后者,因为我们在创建CRTC时就能把plane的对应关系确定下来,省得后面再通过drm_mode_crtc_set_gamma_size这种间接手段去补。

2.1 核心结构体初始化与plane绑定

来看一段典型的MTK初始化代码片段,我在mt8195的驱动里把它简化了一下:

static int mtk_drm_crtc_init(struct drm_device *drm, struct mtk_drm_crtc *mtk_crtc, struct drm_plane *primary, struct drm_plane *cursor, unsigned int pipe) { int ret; ret = drm_crtc_init_with_planes(drm, &mtk_crtc->base, primary, cursor, &mtk_crtc_funcs, NULL); if (ret) return ret; /* 这里可以拿到drm_crtc_init_with_planes初始化的crtc->state */ mtk_crtc->mmsys_reg = mtk_get_mmsys_base(drm); ... }

drm_crtc_init_with_planes这个函数做了几件你看不到但很关键的事:

第一,它会把drm_crtc挂到drm_device->mode_config.crtc_list链表中,同时生成两个设备节点对应的对象ID。以后用户态通过DRM_IOCTL拿到crtc id,本质上拿到的就是这个对象。

第二,它会自动创建一个drm_crtc_state结构体并赋给crtc->state。这是给后续atomic机制用的。如果这一步没做好,后面所有基于state的检查(比如drm_atomic_get_crtc_state)都会直接崩溃,而且是在很奇怪的位置崩,很多新手在这里耗费大量时间。

第三,它会把传入的primary和cursor plane与CRTC关联起来。注意这里的“关联”只是双向指针记录,真正校验plane与crtc是否匹配是在atomic check时做的。在MTK平台上,primary plane通常对应OVL0,cursor plane可能不存在或者复用primary。如果传了NULL,也不是不行,但用户态在枚举资源时就会看到这个CRTC下面没有cursor,某些Android的硬件光标方案会出问题。

2.2 funcs回调表里藏着运行时行为

drm_crtc_init_with_planes的第四个参数是const struct drm_crtc_funcs *funcs,这是整个CRTC在运行时行为的关键。MTK的mtk_crtc_funcs大体长这样:

static const struct drm_crtc_funcs mtk_crtc_funcs = { .set_config = drm_atomic_helper_set_config, .destroy = mtk_drm_crtc_destroy, .page_flip = drm_atomic_helper_page_flip, .reset = mtk_drm_crtc_reset, .atomic_duplicate_state = mtk_drm_crtc_duplicate_state, .atomic_destroy_state = mtk_drm_crtc_destroy_state, .gamma_set = drm_atomic_helper_legacy_gamma_set, .enable_vblank = mtk_drm_crtc_enable_vblank, .disable_vblank = mtk_drm_crtc_disable_vblank, };

这里头的潜规则值得说两句。.enable_vblank.disable_vblank是在中断上下文被调用的,所以里面不能放太耗时的操作,更不能msleep。有些MTK工程师为了让vblank时间戳更精准,会在这里面直接操作寄存器开启/关闭对应显示模块的帧中断,这个思路是对的,但一定记得加锁保护。.reset回调则在每次初始化或drm_mode_config_reset时被调用,MTK平台一般在这里给mtk_drm_crtc_state做默认初始化,比如把所有plane的zpos按顺序排好。

从实际调试经验看,很多“开机后花屏”或“显示内容偏移”的问题,追到根因往往是mtk_drm_crtc_state里的某些字段在reset时没有清零,导致第二次打开显示时残留了上次的脏状态。这类问题非常隐蔽,因为第一次播放是正常的,关掉再打开就出问题,而且log毫无异常。建议每个字段都检查一遍默认值。

2.3 生命周期中的常见错误:提前使用state

CRTC生命周期里最常见的错误,我看过太多回了:驱动在probe阶段、crtc_init之前就去读crtc->state。严格来说,drm_crtc_init_with_planes返回之前,state可能还是NULL。很多厂商的私有状态结构体都依赖drm_crtc_state的子类,如果你在init之前就尝试访问子类字段,那内核直接给你一个漂亮的Oops。

我自己的习惯是:MTK驱动里所有需要访问私有state的地方,都先调用drm_atomic_get_crtc_state或者通过drm_crtc_statecontainer_of拿到子类指针,并且在访问前判空。虽然这会多写几行代码,但在内核态,多一道防御就能少一次panic。

另外,drm_crtc_cleanup的调用时机也很讲究。它必须在所有plane、encoder都解绑之后调用,否则链表遍历时会访问到已经释放的对象。在MTK平台,如果通过component框架做驱动解绑,需要确保unbind时先销毁CRTC再销毁plane,顺序反了你可能会看到从drm_mode_config_cleanup冒出来的use-after-free。内核的KASAN有时候能抓到,有时候抓不到,很折腾。

3. mode_set这条链路:分辨率切换是怎么在CRTC这儿落地的

用户态执行一个DRM_IOCTL_MODE_SETCRTC,传入想要的mode,比如1920x1080@60,这之后会发生什么?如果你能完整解释这条链路,说明你对KMS的理解已经到了一定深度。这条链路的中间环节就在drm_crtc.c

3.1 从ioctl到drm_crtc_set_mode的完整调用链

流程是这样的(以非atomic legacy路径为例,因为内部路径更直观,atomic路径最终也会落到类似的动作):

  1. 用户态调用drmModeSetCrtc(fd, crtc_id, fb_id, x, y, connectors, num_connectors, mode)
  2. 内核侧进入drm_ioctl,分发到drm_mode_setcrtc,这个函数在drm_crtc.c里实现,主要做了参数校验:crtc_id是否合法、fb是否存在、connector是否绑定到该crtc允许的encoder上。
  3. 然后调用drm_mode_set_config_internal,它会准备一个drm_modeset_acquire_ctx,其实就是在为后面的modeset锁做准备。
  4. 接着核心动作在drm_crtc_set_mode里发生:它会遍历该crtc下所有绑定的encoder,对每个encoder依次调用encoder->crtc对应的mode_fixupmode_valid,确认这个mode在当前硬件上可用。
  5. 再往后就是真正切硬件的时刻:依次调用crtc->funcs->mode_set_nofb(通知CRTC要切mode了)和每个encoder的mode_set,最后是crtc->funcs->commit或者通过helper框架走crtc->enable

这里就不放太长代码了,但在drm_crtc.cdrm_crtc_set_mode有一段非常关键的执行逻辑,伪代码如下:

static int drm_crtc_set_mode(struct drm_device *dev, struct drm_crtc *crtc, struct drm_framebuffer *fb, struct drm_display_mode *mode, struct drm_connector_state *conn_state) { ... /* 1. 遍历encoder,先做mode_valid检查和fixup */ drm_for_each_encoder_mask(encoder, dev, crtc_state->encoder_mask) { if (connector->funcs->mode_valid) connector->funcs->mode_valid(connector, mode); encoder->bridge->funcs->mode_fixup(...); } /* 2. 关闭CRTC和encoder */ drm_helper_disable_unused_functions(dev); /* 3. 更新CRTC的显示模式 */ crtc->funcs->mode_set_nofb(crtc, mode); drm_for_each_encoder_mask(encoder, dev, crtc_state->encoder_mask) encoder->funcs->mode_set(encoder, mode, adjusted_mode); /* 4. 真正打开CRTC输出 */ crtc->funcs->commit(crtc); ... }

看懂这段逻辑,你就能明白为什么有时候一个mode明明在connector那边支持,但最终切不过去——因为mode_valid是一层层校验的,任何一个环节认为不行,整条链就断开。MTK这边比较常见的是DSI接口的时序参数问题,比如在mode_fixup阶段根据DSI的lane_numbit_clk重新调整了pixel_clock,如果调出来的结果超出DPHY范围,mode_set就会失败。这类问题的定位方式很简单:开启drm.debug=0x1f,在内核log里搜mode_validmode_fixup的输出,基本一锤定音。

3.2 MTK平台上mode_set必须多留意的时钟与通路绑定

在MTK平台,drm_crtc.cmode_set不只是一个“设置分辨率”的动作,它还会触发整个mmsys显示时钟域的重新配置。很多android项目在做“mipi dsi drm竖屏改横屏显示”这类需求时,改完fb的orientation和kernel dts之后发现画面变形或者只显示一部分,根因往往是CRTC的mode_timing没有跟着变。注意,这里不是说connector的timing不重要,而是说在MTK的显示管线里,CRTC层面会有一个pixel_clk的约束,它跟connector->display_info里的max_tmds_clock不一定一样,必须同时满足两侧约束,屏幕才正常。

举个具体的坑。之前在某款mt8195平板上做高分屏适配,面板本身支持2880x1800,DSI也支持4lane,但亮度调暗后偶尔闪屏。排查半天发现是CRTC在计算vrefresh时,由于mode的htotal/vtotal设置得太贴近DSI的blanking极限,导致实际帧率从60掉到58出头,跟面板内部的动态刷新率调节打架。解决办法不是去改drm_crtc.c,而是要在mtk_drm_crtc_mode_set里给htotalvtotal加上合理的min余量。这种经验在代码文档里很难找到,得靠实测。

3.3 atomic模式下的CRTC提交行为

现在新内核里用户态基本都走atomic接口了,但drm_crtc.c的legacy路径并没有消失,它只是被封装成了drm_atomic_helper_set_config。在atomic路径下,CRTC的行为挪到了drm_atomic_commit里统一调度。MTK的atomic commit有一个自己的mtk_drm_crtc_atomic_beginmtk_drm_crtc_atomic_flush,分别对应提交开始和提交结束。

这里有必要提醒一点:在把plane_state->fb切到新framebuffer时,要特别注意implicit fence的处理。MTK的DMA-BUF在有些场景下是需要等待GPU渲染完成的,如果CRTC在atomic_flush里没有正确调用drm_atomic_helper_commit_planes,就可能出现画面撕裂或者残留旧帧的情况。这类问题表面上像plane的配置错误,实际上还是CRTC的提交时序没处理好。

4. vblank机制:drm_crtc.c里最能影响帧率的中枢神经

vblank,即垂直消隐期,是所有CRTC机制里最“生理性”的一部分。它跟用户看到的一切是否流畅直接相关:vsync、frame pacing、双缓冲交换的时机、以及系统省电策略,全摄于vblank。在我调试MTK平台的KMS问题时,至少有一半的时间花在vblank相关的路径上。

4.1 vblank的打开与关闭:谁在调用,什么时候调用

drm_crtc_vblank_ondrm_crtc_vblank_offdrm_crtc.c里最关键的一对兄弟函数。它们的主要作用是维护一个引用计数dev->vblank[crtc_index].refcount,当计数从0变1时,驱动会实际使能硬件vblank中断;当计数从1变0时,关闭中断。

MTK平台的习惯是:驱动在crtc->enable里调用drm_crtc_vblank_on,在crtc->disable里调用drm_crtc_vblank_off。这个顺序非常讲究,我曾经见过有工程师把vblank_on放在了mode_set之后、真正的硬件使能之前,结果就是中断来得比寄存器使能早,导致isr里读到的line count不准确,出现偶发的时间戳跳跃。

更隐蔽的一个坑:如果系统里同时有两个CRTC都开vblank,drm_crtc_vblank_getdrm_crtc_vblank_put必须一一对应。一旦某块屏的驱动在热插拔时少调了一次put,vblank就永远关不掉,整个DRM子系统一直处于高频中断状态,CPU占用直接飙升。这个问题在log里表现为vblank wait timed out后又恢复正常,非常迷惑。

我在代码审查时发现过一个很典型的MTK问题:mtk_drm_crtc_disable里没有先调用drm_crtc_vblank_off,而是直接关了显示模块的时钟。这样会导致vblank计数不归零,后续再打开时硬件中断使能状态错乱。正确做法是参照helper框架的规范:先vblank_off,再关时钟和通路。

4.2 vblank事件与page flip完成通知

再往深看,drm_crtc.c里还有一条日常中用得尤其多的路径:drm_crtc_arm_vblank_eventdrm_crtc_send_vblank_event。在atomic方式下,当用户态发起page flip,内核会把drm_pending_vblank_event挂到CRTC的队列里,然后在下一个vblank中断到来时,通过drm_crtc_send_vblank_event通知用户态“这一帧已经显示了”。

MTK平台上常见的问题是“第一次page flip特别慢”或者“帧率只有预期的一半”。这时候优先检查vblank中断是否真的在预期频率触发。有一种低级错误:在ISR里读取寄存器判断当前是否处于vblank区间,由于时钟域不同步,读到的值一直是SW(start of vblank)之前的值,导致drm_crtc_handle_vblank被延后到帧末才调用,表现为整体帧率掉一半。把ISR里的寄存器获取时机改成从“帧同步中断”驱动,问题就消失了。

另外,drm_crtc_send_vblank_event本身不是原子的,它需要持有dev->event_lock。在驱动自己的中断处理里调用的时候,要确保没有跟其他线程的vblank操作形成锁竞争,否则偶尔会出现event丢失。这种问题很讨厌,因为不是每次都复现,一旦应用层对丢帧敏感,就会看到画面卡顿但内核log干净得可怕。

4.3 MTK特有的vblank与硬件状态同步

在MTK的DSI场景下,vblank还有一个特殊用途:命令行模式(command mode)和视频模式(video mode)的触发方式不一样。视频模式下每个帧都有完整的blanking周期,vblank好理解;但command模式下,面板控制器自己维护刷新,主控只有在写命令时才介入,这时候vblank一般仍然会从显示控制器里生成“帧同步信号”。有些MTK的driver会借助vblank来做DSI的burst写命令的节流,这是比较深的tuning层面了,一般不带Q才接触不到。不过理解这一点能帮你解释:为什么改DSI panel的line/blank参数会影响整体功耗和帧率——因为它在影响着vblank的窗口宽度和command mode的写入时间。

5. 基于drm_crtc.c的实战排查记录:黑屏、无信号、帧率不稳三板斧

最后这部分,我把它当作一份“现场调试笔记”来写。在MTK平台上,我们最常碰到的KMS问题就是三类:黑屏、无信号、帧率不稳。看起来八竿子打不着,但很多时候根因都在CRTC的状态和调用时机上。

5.1 如何快速摸清一个CRTC当前的内部状态

在动手查问题之前,先要学会看状态。DRM子系统提供了一组debugfs节点,在MTK设备上通常是:

cat /sys/kernel/debug/dri/0/state

这个输出里会把整个atomic状态树打印出来,包括每个CRTC当前是否enable、使用的mode是什么、绑定了哪些plane和encoder。我最常用的排查组合是drm.debug=0x1f内核参数加这一条cat命令,先确认CRTC层面的状态是否符合预期,再去追硬件寄存器。

举个例子:如果用户报障“副屏没信号”,先去看state,发现副屏的CRTC是自己disable的,但预期它应该enable——那就排除CRTC层问题,去看是不是用户态的hotplug事件没触发,或者connector那边的detect没有识别到外接显示设备。反过来,如果state里已经是enable,但实际物理接口没波形,那问题就在CRTC到encoder之间的晶体管级别,需要查时钟或reset。

5.2 黑屏问题:先从CRTC的enable和mode_set路径查起

黑屏可能是百种原因,但DRM层面最好定位的是“整个CRTC都没开”。如果state里CRTC是enable的,但屏幕仍然不亮,重点看两处:

一处是crtc->funcs->mode_set_nofb是否被调用。很多显示控制器需要在mode_set里配置总线的时序参数(HFP、HBP、VFP、VBP),这个函数没被调用的话,硬件会沿用上一次的时序,导致信号完全不同步。

另一处是crtc->enable是否真的执行到了最后一步。MTK平台上,enable回调里经常有一串依赖mmsys的寄存器配置,中途任何一步devm_regmap_read失败返回都会导致后续没执行。我在某次支持中遇到过因为mmsys的clock没有提前enable,导致CRTC enable时读寄存器超时,整个调用栈在log里像是在正常返回,但实际后半段全被goto err跳过了。排查手法是在enable里临时加DRM_DEV_INFO(drm->dev, "xxx\n"),一步步确认走到哪。这招虽然土,但效率奇高。

5.3 帧率不稳:vblank相关寄存器读到的是不是“真·vblank”

如果是帧率不稳,优先确认vblank路径。在MTK上,我常用的验证方法是测量实际的帧同步波形,同时与内核里的drm_crtc_vblank_count对比。如果内核计数没问题,但画面还是卡,那就不是KMS的问题,更可能在GPU合成或buffer分配上;如果内核计数本身就慢一半,那一定是在drm_crtc_handle_vblank调用时读到的寄存器状态不正确,需要检查ISR的触发源和寄存器读取位。

有一个我踩过两次的坑:MTK的显示控制器里,当正在跑的是command mode DSI时,主控制器的帧同步中断可能不会像video mode那样每一个用户可见帧都触发一次。某些硬件版本上,为了省电,两帧才产生一个中断,而vblank计数还是按中断来算的。最后在软件层用hsync的周期做校正,才算基本解决问题。这种问题你只看drm_crtc.c是看不出来的,必须结合MTK的硬件行为单独适配。

5.4 双屏扩展变克隆:十有八九是crtc_mask配错

再说一个在MTK平台上非常常见的“伪故障”。用户定义了两个connector,比如HDMI和DSI,期望扩展模式,结果却是两个显示内容一摸一样的克隆模式。大多数情况下,问题不在connector,而在CRTC。DRM的克隆规则是:如果两个连接器最终路由到同一个CRTC,那它们共享同一个显示内容。MTK驱动在mtk_drm_encoder_early_finish或绑定encoder到CRTC的那段逻辑里,如果用同一位bit去配置两个encoder的possible_crtc,那它们就是克隆关系。

drm_crtc.cdrm_encoder_init相关文档中,有个很容易忽略的说明:encoder的possible_crtcsmask决定它能挂在哪些CRTC下面。MTK Etrack上经常看到有人把HDMI和DSI都填了BIT(0),那自然永远是克隆。把DSI改成BIT(1),并确认第二个CRTC被正确初始化,扩展模式就出来了。这个问题不是drm_crtc.c的执行逻辑有bug,而是我们对CRTC资源位的使用规划没考虑清楚。

最后就分享一个我个人觉得性价比最高的调试技巧:在接入一个新panel或新平台时,上来先别急着写驱动,先把drm.debug=0x1f打开,手动跑一次modetest -M mtk(如果用户态有libdrm-tools的话),硬拉一个分辨率看log。重点看三行内容:CRTC enable成功没有、vblank有没有稳定计数、encoder mode_set是否被正确调用了。这三行对上了,后面性能调优才有基础。drm_crtc.c看着不过是个几千行的基础设施文件,但它定义了这套“显示调度”的规矩,吃透它,MTK平台上的KMS问题基本就解决了一大半。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询