MCU跑视频显示,说难不难,但真要在量产阶段遇到“画面突然停了”这种问题,血压直接拉满。这次我在STM32U5G9ZJT6Q平台上就撞上了这样一个“Video Stop”的案子——视频播放到一半画面定格,屏幕不刷新,系统任务还活着,但整个显示链路就像被按了暂停键。这篇就把完整的定位过程和修复方案整理出来,给同样在U5系列上做显示应用的朋友一个参考。
先交代一下背景。这个项目用的是STM32U5G9ZJT6Q,属于STM32U5系列里的高配型号,Cortex-M33内核,带TrustZone,160MHz主频,2MB Flash加786KB SRAM,内置LTDC(LCD-TFT显示控制器)、DMA2D(2D图形加速器)和GFXMMU(图形存储管理单元),跑RGB接口屏幕非常顺。正因为这套显示外设配置完整,我们直接用它做视频播放和人机交互界面。出问题的场景是:设备在播放视频过程中触发了一次低功耗唤醒流程,之后画面就停止更新了,音频还在走,串口日志也正常,唯独显示链路一动不动。
排查这类问题,最忌讳上来就改代码乱试。显示链路涉及的模块太多了:CPU侧的数据准备、DMA2D搬运、LTDC时序刷新、外部存储带宽、GPIO复用、时钟树,每一环断了都会表现为“视频停止”。我按“先硬件后软件、先时钟后传输、先同步后刷新”的顺序,最终把问题收敛到了两个点:一个是低功耗唤醒后LTDC时钟源恢复时序不完整,另一个是DMA2D的启动时机和LTDC撕裂同步没对齐。下面把整套思路拆开讲。
1. 项目背景与视频停止问题定位
1.1 显示链路的核心角色:LTDC、DMA2D、GFXMMU
在STM32U5G9ZJT6Q上做视频播放,说白了就是一条数据流水线:视频解码后数据写入内存,DMA2D按帧搬运到显存,LTDC按行同步信号把显存数据扫到屏幕。任何一环卡住,画面就会停止。
先理解LTDC,它不参与图像处理,只负责产生像素时钟、行同步、帧同步这些时序信号,然后按地址从显存中读像素数据输出到屏。LTDC是一个“始终在跑”的外设,只要它的时钟存在、控制寄存器配置有效,它就会不停地从刷新地址读取并输出,产生TE信号。若TE信号消失或者刷新地址错误,屏幕直接定格。
DMA2D是2D DMA加速器,负责图像拷贝、像素格式转换、混合等操作。视频播放场景里,通常把解码后的YUV帧转换成RGB,再通过DMA2D搬到LTDC的帧缓冲区。它的启动方式有寄存器手动触发和硬件同步触发两种,后者依赖LTDC的TE信号。
GFXMMU则是可选的地址映射层,它能把一个虚拟的连续显示缓冲映射到物理上不连续的多块内存中,主要用在外部PSRAM/SDRAM的碎片化管理。它的问题通常表现为地址解析错误,也会导致花屏或画面停止。
这条链路上,我排查时最先怀疑的是DMA2D。因为视频播放要频繁触发DMA2D搬运,一旦它的传输状态机卡住,前台画面自然就停了。但实际上,DMA2D在多数情况下是“背锅侠”,真正的问题常常出在时钟和同步信号上。
1.2 出问题时的现场表现与初步判断
故障复现路径是:运行一个长视频循环播放任务,播放超过30分钟后主动压低功耗进入STOP模式(此时LTDC时钟被关闭,屏幕仍有残余显示或关闭背光),再通过外部按键或RTC唤醒。恢复后不久,画面要么全黑,要么定格在唤醒前的最后一帧,不再刷新。
我把问题描述拆成三个关键词:时钟、DMA、中断。
- 时钟:LTDC的像素时钟来源于PLL2,从STOP模式唤醒后,PLL2可能还在重新锁定,如果软件在PLL2未稳定时就重新配置LTDC或者DMA2D,外设拿到的是一个错误或不稳定的时钟。
- DMA:DMA2D如果在唤醒过程中刚好在传送一帧数据,唤醒流程把时钟关了又打开,DMA的状态可能已经错乱。
- 中断:LTDC的Line Interrupt或TE事件中断如果丢失,软件层的帧同步机制就会失步,后续每一帧都停在错误时机。
这三点互相纠缠,必须逐个验证,不能靠猜。我先把启动流程和中断日志全部打开,确认到底卡在哪一步。
2. 前置排查:硬件、初始化代码与系统日志
2.1 硬件层面快速验证
看到视频停止,第一步先量硬件信号,不然后面全是白忙活。
用示波器/逻辑分析仪量三个点:
- 屏幕的DCLK:看像素时钟是否连续。
- LCD的DE信号:数据使能是否还存在。
- TE信号(如果屏支持撕裂同步):是否还在周期性产生。
实测下来,DCLK在唤醒后直接消失,这意味着LTDC没有工作在有效时钟下。这个结果很有价值,把排查方向直接推向了时钟链路,而不是数据搬运。再量VDD和复位脚,没有明显跌落,排除供电瞬时拉垮的问题。
注意:在嵌入式开发中,遇到显示异常,不要一上来就查软件缓冲区。示波器量DCLK和DE,10分钟能排除一半问题。尤其是DCLK消失这种硬性指标,比看寄存器省事得多。
2.2 软件的断点与日志策略
在确认了硬件层面DCLK消失后,我把排查重心转移到软件初始化代码。
STM32U5系列的时钟树里,LTDC时钟源通常可配置为PLL2或PLL3 P。我们的设计里用的是PLL2,输出一个满足屏幕要求的像素时钟。从STOP模式唤醒后,系统时钟先回到默认的HSI16,应用代码再通过SystemClock_Config()恢复PLL配置。问题就藏在这个“恢复”过程中。
我加了一组关键寄存器快照日志:
RCC->CR:PLL2使能位与就绪标志。RCC->PLL2CFGR:PLL2倍频配置是否仍保持。LTDC->GCR:LTDC全局配置是否被意外清掉。DMA2D->CR:DMA2D控制寄存器当前状态。
日志输出后发现一个规律:唤醒的时候,PLL2的RDY标志还没置位,代码就把LTDC使能打开了。LCD控制器在非稳定时钟下工作,轻则DCLK抖一下,重则直接停振。这个问题之所以隐蔽,在于它不是每次都复现,因为PLL2锁定时间受温度和电压影响,第一次唤醒可能刚好稳定,第二次就差几微秒,于是概率性黑屏。
2.3 Fault与分析工具配合
为了排除硬Fault导致系统跑飞的可能,我打开了Cortex-M33的硬Fault中断和UsageFault,配合栈回溯函数,在故障时打出一串调用栈和PC值。实测中没有触发Fault,说明问题不是CPU执行流错乱,而是外设状态异常。
同时,我让RTC每秒打印一个时间戳,确认系统整体调度没有停。结论更清楚了:MCU活着,视频链路死了,问题收窄到外设初始化和同步机制上。
3. 根因分析:唤醒时钟恢复不完整与DMA2D启动失步
3.1 PLL2锁定顺序导致LTDC时钟悬空
在STM32U5系列参考手册里,从低功耗模式恢复,PLL2不会自动恢复到停止前的状态。它需要软件重新把置位位写一遍,然后等待硬件把RDY位置1。很多人会在CubeMX生成的代码里看到类似操作,但往往忽略“等待就绪”这一点。
我复现问题时的初始化片段(精简版)是这样:
// 从STOP唤醒后恢复显示时钟 void SystemClock_Config(void) { // ... 其他时钟恢复代码 ... HAL_RCCEx_EnablePLL2(&PLL2Config); // 问题在这里:没有等待 PLL2RDY 就继续往下走 __HAL_RCC_LTDC_CLK_ENABLE(); LTDC_Init(); DMA2D_Init(); }这段代码在多数情况下能跑,因为PLL2配置在芯片内部,稳定时间通常很短,但一旦温度偏低,或电源毛刺导致第一次锁定失败,LTDC就会在一个残废时钟下被使能,界面直接拒收后续数据。
再往深处想,这里还隐藏着一个更大的坑:PLL2在唤醒后被意外关闭。我们用的是HAL_PWR_EnterSTOPMode(),如果模式参数里把PWR_SLEEPENTRY_WFI混用,或者代码路径里有某些外设通过__HAL_RCC_PLL2_CLK_DISABLE()做了关闭,唤醒后的恢复顺序就会错乱。所以我把唤醒流程检查了一遍,确认每次进入STOP之前,都做好了完整的时钟域记录。
3.2 TE同步标记丢失,DMA2D反复等待
第二个问题是我在修复时钟之后才暴露出来的。时钟恢复正常后,画面能出,但偶尔在唤醒后第一帧出现“上半屏正常、下半屏花屏然后停止更新”的现象。这个表现很典型,是DMA2D搬运和LTDC的撕裂同步(Tearing Effect)失步导致的。
传统实现里,我用LTDC的TE事件中断(或Line中断)来触发DMA2D搬运下一帧:
void LTDC_IRQHandler(void) { if (__HAL_LTDC_GET_FLAG(&hltdc, LTDC_FLAG_TE) != RESET) { // 清标志,再启动DMA2D传送下一帧 __HAL_LTDC_CLEAR_FLAG(&hltdc, LTDC_FLAG_TE); DMA2D_StartTransfer(); } }这个方案在正常运行的时候没问题,但唤醒后第一帧,LTDC寄存器里的状态标志可能是脏的(例如休眠前已经置位,唤醒后没有清干净),中断处理程序会认为“有一帧需要搬运”,于是立刻启动DMA2D。然而此时LTDC的扫描位置可能已经过了安全起始点,搬完的数据一半没被扫到,下一帧时序就乱了,随后LTDC的一个内部标志也可能错位,导致后续中断不再产生。
解决办法是,在唤醒后对LTDC和DMA2D做一次完整的“软复位+同步握手”,而不是让旧的状态残留着继续跑。
// 显示子系统安全重初始化 void DisplaySubsystem_Reinit(void) { // 停掉DMA2D DMA2D->CR &= ~DMA2D_CR_START; while (DMA2D->CR & DMA2D_CR_START); // 停掉LTDC LTDC->GCR &= ~LTDC_GCR_LTDCEN; // 清掉所有LTDC中断标志(按参考手册逐位处理) LTDC->ISR = LTDC_ISR_TERRIF | LTDC_ISR_RRIF | LTDC_ISR_LIF | LTDC_ISR_FUIF; // 重新使能LTDC LTDC->GCR |= LTDC_GCR_LTDCEN; // 只有LTDC已产生正常运行状态后,才启动DMA2D while (!(LTDC->ISR & LTDC_ISR_LIF)); // 等待第一个Line中断 DMA2D_StartTransfer(); }这样处理后,DMA2D永远不会抢跑,因为LTDC必须先输出一帧正常的扫描信号,软件再顺着它的节奏搬运。
3.3 还排查过内存带宽和外部存储
除了上面两个主因,我还排查了外部PSRAM的访问冲突。STM32U5G9ZJT6Q的内部SRAM很大,但视频帧缓冲如果放在外部PSRAM,LTDC刷新和DMA2D写入会争抢总线。U5系列内部有总线矩阵和缓存,可以在一定程度上缓解,但外部存储总线带宽还是有限制。
这个问题在唤醒后更容易出现,因为内部缓存可能处于dirty状态,需要写回。我通过将帧缓冲放到内部SRAM后,现象明显减少,但为了让画面流畅度更好,最后还是保留了I-Cache和D-Cache策略,并给外部存储控制器配置了合理的中断等待周期。这一项对我定位问题没有直接影响,但它是“视频停止”类问题必须排查的一环。
4. 修复方案落地与验证
4.1 时钟恢复顺序的规范化实现
修第一个问题的方案很直接:在恢复显示时钟时,强制等待PLL2就绪。拿标准库或HAL库都行,重点是不要跳过状态检查。
// 恢复PLL2并等待就绪 void DisplayClock_Recovery(void) { // 使能PLL2 __HAL_RCC_PLL2_ENABLE(); // 必须等待硬件确认锁定 while (!__HAL_RCC_GET_FLAG(RCC_FLAG_PLL2RDY)) { // 加入超时保护,避免死等 if (timeout-- == 0) { Error_Handler(); } } // 再使能LTCCLK和DMA2D时钟 __HAL_RCC_LTDC_CLK_ENABLE(); __HAL_RCC_DMA2D_CLK_ENABLE(); }这里有一个细节:PLL2重启后,像素时钟的相位和频率需要满足LCD模组的上电时序,否则即便时钟稳定,屏也可能不亮。所以我在恢复时钟后增加了至少5个DCLK周期的延时,再走LTDC重新初始化流程。
4.2 LTDC+DMA2D软件同步的完整代码
下面这段是最终验证过的显示子系统重初始化函数,结合了时钟恢复、LTDC软复位、中断标志清理和DMA2D同步启动。
void DisplayVideo_ResumeFromStop(void) { // 1. 恢复PLL2并等待稳定 DisplayClock_Recovery(); // 2. 禁止LTDC LL_LTDC_Disable(LTDC); // 3. 清空状态寄存器 LL_LTDC_ClearFlag_TE(LTDC); LL_LTDC_ClearFlag_LI(LTDC); LL_LTDC_ClearFlag_FU(LTDC); LL_LTDC_ClearFlag_TERR(LTDC); // 4. 重新配置LTDC时序参数(HBP、HFP、VBP、VFP等从配置结构体恢复) LL_LTDC_InitTiming(LTDC, &LtdcTimingConfig); // 5. 重新设置图层地址 LL_LTDC_ConfigLayerAddress(LTDC, 1, (uint32_t)frameBufferAddr); // 6. 使能LTDC LL_LTDC_Enable(LTDC); // 7. 等待LTDC产生第一个line中断,确保扫描正常 while (!LL_LTDC_IsActiveFlag_LI(LTDC)) { // 严禁加无限等待,务必带超时 } // 8. 此时DMA2D再开始搬帧 LL_DMA2D_StartTransfer(DMA2D); }这段代码里比较关键的是第7步,它相当于软件层面的“帧同步握手”。有了这个等待,DMA2D启动时机就完全对齐了LTDC的扫描节奏。有人可能觉得多等了一个Line中断会浪费一点时间,但相对于视频停止的风险,这点等待完全值得。
4.3 长时间压力测试与功耗切换测试
修复完成后,做了两轮验证:
第一轮是长跑。连续播放1080P级别的视频素材(YUV转RGB),正常运行模式下跑12小时,没有出现一次画面停止。这里的素材是循环播放的,每一帧都带时间戳校验,通过GPIO翻转+逻辑分析仪观察DMA2D每帧输出间隔,确认帧率在长时间运行中稳定。
第二轮是功耗切换。每10分钟切一次STOP模式,然后看门狗+RTC唤醒,连续切了500次。重点观察:唤醒后能否在200ms内恢复画面,TE中断是否连续,DMA2D是否会卡住。实测在修复后,唤醒恢复时间稳定在120ms左右,没有一次异常。
注意:长时间测试最好做成自动化。我是用一个外部信号源周期性触发唤醒引脚,配合串口输出状态码,晚上睡觉前挂上跑,第二天早上看日志。手动按按键测几百次,效率太低。
5. 常见问题速查表和避坑经验
5.1 视频停止类问题排查速查
| 现象 | 优先级 | 排查目标 | 常用手段 |
|---|---|---|---|
| 画面全黑,MCU正常 | 1 | PLL2时钟是否锁定 | 量DCLK,查PLL2RDY |
| 画面定格在最后一帧 | 1 | LTDC时钟和使能状态 | 检查GCR.LTDCEN,清状态寄存器 |
| 唤醒后第一帧花屏 | 2 | DMA2D启动是否超前 | 等待LINE中断后启动DMA2D |
| 偶尔停帧但音频继续 | 2 | TE同步信号/中断丢失 | 检查NVIC优先级,清标志顺序 |
| 视频画面撕裂 | 3 | 帧缓冲地址对齐 | 确保DMA2D访问地址4字节对齐 |
| 帧率低/缓慢 | 3 | 外部存储带宽 | 改用内部SRAM,优化总线优先级 |
每一类现象的排查手段,我在前面的章节里基本都覆盖到了。遇到问题别急着改代码,先对照表确认现象归属,能少走很多弯路。
5.2 我在这个项目里踩过的三个坑
第一个坑:LTDC中断标志的清除顺序。在STM32U5系列里,LTDC的中断标志清除方式和F4/F7系列不完全一样,特别是TE标志,如果直接在IRQHandler里LTDC->SRCR = LTDC_SRCR_IMR去触发布刷新,可能会覆盖当前帧的状态。更稳妥的做法是只在IRQHandler里清标志和置位软件状态,真正的图层地址更新和DMA2D启动放到主循环或RTOS任务里做,避免在中断上下文搞复杂同步。
第二个坑:DMA2D的FIFO阈值设置。DMA2D搬运大帧时,如果FIFO阈值设置得太小,频繁触发总线访问,反而会拖慢整体速度,极端情况下会造成总线拥塞,LTDC会拿不到数据,画面出现停顿。我在这个项目里把DMA2D的FIFO阈值调到一个合适的水平(具体值要看芯片参考手册里的FIFO寄存器映射),帧率就上来了。
第三个坑:别迷信CubeMX生成的默认时钟树。CubeMX生成的时钟恢复代码在有些版本里只恢复系统时钟,不会主动恢复PLL2/3的RDY等待。视频显示这类场合,一定要人工检查一遍SystemClock_Config里的PLL2初始化路径。我的做法是封装一个独立模块,只负责显示相关时钟,和系统启动代码解耦,这样后期调试也方便,直接在模块里打断点。
5.3 关于“视频停止”的认知层面建议
最后说点认知层面的东西。这类显示问题,真正的难点不是修复本身,而是如何快速缩小范围。我第一次遇到的时候,把LTDC初始化的每个寄存器都打出来对比,疯狂怀疑缓存一致性问题,实际上第一个嫌疑就能量DCLK直接拍死。硬件手段的优先级一定要高于软件调试。
另外,凡是涉及低功耗唤醒的视频系统,设计阶段就要规划好“显示子系统冷启动路径”。不要在唤醒之后直接恢复寄存器,别嫌麻烦,照着上电初始化的完整流程走一遍,最多多花几十毫秒,但稳定性能换来几个数量级的提升。
我在实际项目里深有体会:显示类Bug往往是时序Bug,而时序Bug靠静态看代码很难发现,必须靠仪器和日志一层层剥。这套“量DCLK确认时钟、看TE确认同步、查DMA2D状态确认搬运”三板斧下来,绝大多数视频停止问题都能半小时内锁到根因,剩下的就是按流程修。