深入DRM CRTC:从drm_crtc.c看MTK平台KMS显示调度核心机制
发布时间:2026/9/17 16:17:56 作者:尧图编辑部 阅读量:1,286

在内核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 base和struct 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_crtcNVIDIA有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_init、drm_crtc_init_with_planes、drm_crtc_cleanupCRTC的模式设置drm_mode_setcrtc用户态ioctl入口、drm_crtc_set_mode内部核心函数vblank相关drm_crtc_vblank_on、drm_crtc_vblank_off、drm_crtc_vblank_get、drm_crtc_arm_vblank_event属性与会话相关drm_mode_crtc_set_gamma_size、drm_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通常对应OVL0cursor 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 生命周期中的常见错误提前使用stateCRTC生命周期里最常见的错误我看过太多回了驱动在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_state的container_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比如1920x108060这之后会发生什么如果你能完整解释这条链路说明你对KMS的理解已经到了一定深度。这条链路的中间环节就在drm_crtc.c。3.1 从ioctl到drm_crtc_set_mode的完整调用链流程是这样的以非atomic legacy路径为例因为内部路径更直观atomic路径最终也会落到类似的动作用户态调用drmModeSetCrtc(fd, crtc_id, fb_id, x, y, connectors, num_connectors, mode)。内核侧进入drm_ioctl分发到drm_mode_setcrtc这个函数在drm_crtc.c里实现主要做了参数校验crtc_id是否合法、fb是否存在、connector是否绑定到该crtc允许的encoder上。然后调用drm_mode_set_config_internal它会准备一个drm_modeset_acquire_ctx其实就是在为后面的modeset锁做准备。接着核心动作在drm_crtc_set_mode里发生它会遍历该crtc下所有绑定的encoder对每个encoder依次调用encoder-crtc对应的mode_fixup、mode_valid确认这个mode在当前硬件上可用。再往后就是真正切硬件的时刻依次调用crtc-funcs-mode_set_nofb通知CRTC要切mode了和每个encoder的mode_set最后是crtc-funcs-commit或者通过helper框架走crtc-enable。这里就不放太长代码了但在drm_crtc.c里drm_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_num和bit_clk重新调整了pixel_clock如果调出来的结果超出DPHY范围mode_set就会失败。这类问题的定位方式很简单开启drm.debug0x1f在内核log里搜mode_valid、mode_fixup的输出基本一锤定音。3.2 MTK平台上mode_set必须多留意的时钟与通路绑定在MTK平台drm_crtc.c的mode_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平板上做高分屏适配面板本身支持2880x1800DSI也支持4lane但亮度调暗后偶尔闪屏。排查半天发现是CRTC在计算vrefresh时由于mode的htotal/vtotal设置得太贴近DSI的blanking极限导致实际帧率从60掉到58出头跟面板内部的动态刷新率调节打架。解决办法不是去改drm_crtc.c而是要在mtk_drm_crtc_mode_set里给htotal和vtotal加上合理的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_begin和mtk_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_on和drm_crtc_vblank_off是drm_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都开vblankdrm_crtc_vblank_get和drm_crtc_vblank_put必须一一对应。一旦某块屏的驱动在热插拔时少调了一次putvblank就永远关不掉整个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_event和drm_crtc_send_vblank_event。在atomic方式下当用户态发起page flip内核会把drm_pending_vblank_event挂到CRTC的队列里然后在下一个vblank中断到来时通过drm_crtc_send_vblank_event通知用户态“这一帧已经显示了”。MTK平台上常见的问题是“第一次page flip特别慢”或者“帧率只有预期的一半”。这时候优先检查vblank中断是否真的在预期频率触发。有一种低级错误在ISR里读取寄存器判断当前是否处于vblank区间由于时钟域不同步读到的值一直是SWstart 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.debug0x1f内核参数加这一条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.c的drm_encoder_init相关文档中有个很容易忽略的说明encoder的possible_crtcsmask决定它能挂在哪些CRTC下面。MTK Etrack上经常看到有人把HDMI和DSI都填了BIT(0)那自然永远是克隆。把DSI改成BIT(1)并确认第二个CRTC被正确初始化扩展模式就出来了。这个问题不是drm_crtc.c的执行逻辑有bug而是我们对CRTC资源位的使用规划没考虑清楚。最后就分享一个我个人觉得性价比最高的调试技巧在接入一个新panel或新平台时上来先别急着写驱动先把drm.debug0x1f打开手动跑一次modetest -M mtk如果用户态有libdrm-tools的话硬拉一个分辨率看log。重点看三行内容CRTC enable成功没有、vblank有没有稳定计数、encoder mode_set是否被正确调用了。这三行对上了后面性能调优才有基础。drm_crtc.c看着不过是个几千行的基础设施文件但它定义了这套“显示调度”的规矩吃透它MTK平台上的KMS问题基本就解决了一大半。