news 2026/9/13 9:11:34

STM32U5G9ZJT6Q Video Stop停止顺序详解与低功耗实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32U5G9ZJT6Q Video Stop停止顺序详解与低功耗实现

做车载显示项目这几年,我有个习惯:一听到需求方提“Video Stop”,先追问一句“停到什么程度”。大多数时候,对方说的“停止播放视频”,心里真正想的其实只是“屏幕别再动画了”,但在 STM32U5G9ZJT6Q 这类集成了 LTDC、DMA2D、甚至 MIPI DSI 外设的高性能 MCU 上,真正干净地停住一路视频画面,牵扯到的是一条完整的硬件链路,而不是某个 GPIO 拉一下就能完事。

我在这个项目里就吃过亏:视频停住以后,屏幕偶尔闪彩色横条,背光明明关了却仍能隐约看到残留画面,系统从低功耗模式唤醒后还会花屏。排查到最后,所有现象的矛头都指向同一个根源——停止顺序错了。这篇文章把我在 STM32U5G9ZJT6Q 上做 Video Stop 的完整过程、关键代码和踩坑记录整理出来,适合正在用 STM32U5 系列做显示门禁、HMI 仪表、工业手持设备,尤其是需要视频播放/暂停/关闭功能的开发者参考。

1. 先说清楚:STM32U5G9ZJT6Q 上的 Video Stop,到底要停什么

1.1 一次“黑屏但没断电”的故障,让我重新理解了停止这个词

项目初期,我接到一个仪表显示需求:开机播放一段演示视频,用户按下按键后视频停止,画面切回主菜单,同时整机进入低功耗待机。听起来很常规,对吧?我当时的第一版实现非常简单:收到停止指令后,先关背光,再调 HAL_LTDC_Stop(),最后让 MCU 进入 STOP2 模式。结果实测时发现,屏幕虽然变黑了,但用示波器量 LVDS 输出端的差分信号,仍然有持续的波形输出;用手电筒侧着照屏幕,甚至能看到上一帧画面的残影。也就是说,MCU 已经以为自己把视频“停干净了”,实际上显示链路还在偷偷工作。

后来我翻了 STM32U5 系列的参考手册,才意识到 LTDC 停止函数只是把 LCD_TFT 控制器的主使能位清掉了,但它不会替你去管 DMA2D 是否还在搬运、帧缓冲里是否残留旧数据、DSI 主机的时钟域是否还在跑、以及用于生成像素时钟的 PLL 是否还挂在系统时钟树上。你如果只是调一个函数就进入低功耗,等于把一堆还在运转的外设扔在原地不管,功耗自然降不下去,复位的瞬间也可能出各种随机问题。

1.2 视频链路的三个层面:数据源、搬运链路、显示输出

想做好 Video Stop,第一步是把视频数据的流动路径拆开看。在 STM32U5G9ZJT6Q 上,一路典型视频显示通常由三个层面组成。

第一层是数据源。视频帧可能来自片内 Flash 里存的一段 MJPEG/JPEG 流,也可能来自外部 OctoSPI NOR Flash、SD 卡,或者通过 UART/USB 接收。这一层负责“生产”原始帧数据,如果不停掉,它会持续产生新的画面帧,后面的链路永远不知道什么时候才算完。

第二层是搬运链路。解码后的 RGB 原始数据不一定直接进 LTDC,中间往往要经过 DMA2D 做格式转换(比如 YUV 转 RGB565、ARGB8888 转 RGB888)、图像缩放、alpha 混合,或者经过 GFXMMU 做地址映射。DMA2D 是异步的,启动一次传输后由硬件独立搬运,CPU 只会在传输完成时收到中断,如果你不等它停稳就执行下一步操作,很容易出现“传输到一半数据断流”或者“下一次启动时 DMA2D 还在忙”的状态。

第三层是显示输出。由 LTDC 控制器负责从帧缓冲里按行列读取像素,生成 HSync/VSync/DE/CLK 等时序信号,送给面板接口。如果面板走的是 MIPI DSI,中间还要经过 DSI 主机控制器做协议转换。这一层一旦开始刷屏,就会按照设定的像素时钟持续不停地读内存,直到你把 LTDC 的层使能位和主使能位按正确顺序关掉。

这三个层面必须按“数据源 → 搬运链路 → 显示输出”的方向依次停止。反过来操作,相当于你在电影放映途中先把投影仪的灯泡拔了,但胶片还在继续走,等你下一次开机时,放映机里的胶片已经卡成一团了。

1.3 不同业务场景下的 Video Stop 定义差别很大

调这个功能之前,我建议先明确你属于哪一种需求,因为不同需求对应的停止策略完全不一样。

第一种是“视频暂停”:画面冻结在某一帧,但屏幕保持显示。这种场景虽然也调 LTDC 停止流程,但停止的位置是数据源和 DMA2D,帧缓冲里的最后一帧要保留,LTDC 可以继续工作,也可以把层输入切换到另一块静态缓冲区。这样做的关键是保证“冻结帧”和“界面帧”之间平滑切换,别让用户看到撕裂或者闪屏。

第二种是“视频关闭但界面保留”:视频区域不再显示,但屏幕上还有其他 UI 元素。这种相当于把视频层从 LTDC 里禁用,保留 UI 层,UI 层继续刷新。实现上要小心 LTDC 图层之间的混合顺序,以及旧视频帧是否还残留在图层缓冲区里。

第三种是“整机进入低功耗待机”:这需要把整条显示链路全部停干净,包括时钟、PLL、DSI PHY、外部存储器,最后才能让 MCU 进入 STOP2/STANDBY。

我这次项目踩坑,就是第三种需求里“停不干净”造成的。下面全部围绕“完全关闭视频链路的 Video Stop”来讲,但前两种场景的很多思路是通用的。

2. 停止之前,先把 STM32U5G9ZJT6Q 显示外设之间的依赖关系理清

2.1 LTDC、DMA2D、GFXMMU 和帧缓冲在一次画面刷新中的接力

在没有接触过 STM32 图形外设的人看来,显示就是一帧数据送到屏幕而已。实际上,在 STM32U5G9JT6Q 这类芯片上,一帧 800x480 或者 1280x720 的画面从内存到屏幕,要经过好几级接力。

最简单的一种路径是:CPU 或硬件解码器把视频帧解码到帧缓冲 A,DMA2D 把帧缓冲 A 的格式转换成 LTDC 需要的格式写入帧缓冲 B,LTDC 再按照 HSync/VSync 时序逐行从帧缓冲 B 读取,通过 RGB 并口或者 DSI 送出去。如果启用了 GFXMMU,DMA2D 和 LTDC 看到的还不是实际内存地址,而是一个虚拟连续地址,由 GFXMMU 把分散在多个内存块里的数据映射成连续缓冲区。

这相当于一条流水线:前一个环节产出的是后一个环节的原料。任何时候只要有一个环节还在跑,下一个环节甚至下下个环节就会被“带着走”。比如 DMA2D 已经把一帧新数据写到帧缓冲 B 了,但你还没关 LTDC,LTDC 就会继续把新数据刷到屏幕上,而你的“停止视频”逻辑其实是把数据源停了,屏幕上却显示出了最后一帧新数据——这在很多场景下是无所谓的,但如果你的本意是“屏幕保持菜单不变”,就会看到菜单被新视频帧盖掉一瞬。

2.2 真正的停止难点:异步外设之间的同步等待

视频链路里最坑人的地方在于,三个层面各自是异步的,它们的停止并不是一个“按下开关,全线停转”的过程,而是需要互相等待。LTDC 正在刷新一行画面时,你直接关掉 DMA2D,很有可能会导致帧缓冲只写了一半;DMA2D 正在搬运数据时,你直接关掉 LTDC,屏幕上可能出现撕裂。反过来,DMA2D 正在搬运时你把它的时钟关了,那连“搬运完成中断”都收不到,状态机可能永远卡在错误状态。

所以停止流程里必须显式地做“同步等待”。等待什么?等待 DMA2D 的当前传输完成,等待 LTDC 的 VSYNC/行同步信号,让所有外设都停在一个安全的边界上。这个道理跟多线程编程里的 join 一样——你要停一个线程,就得先等它执行完当前的任务,而不是直接把它杀了。

我这里说的“安全边界”,具体到显示场景,通常是以下三种:

  • DMA2D 的传输完成中断:表示一次搬运已经完整写入目标帧缓冲。
  • LTDC 的垂直消隐区间(VSYNC 之后):表示屏幕已经完成一帧刷新,可以安全切换帧缓冲或关闭图层。
  • DSI 命令链路的发送完成:表示主机发给面板的 shutdown 命令已经下发,而不是还堵在 FIFO 里。

在真正的代码里,你不需要每个环节都等,但至少要在关键切换点等。我第一版代码就是所有等待都省了,结果出问题的时间点非常随机——有时候是开机后第 3 次操作出问题,有时候第 20 次才出,极难复现,这类“异步边界没等”的 bug 比显性的错误更让人抓狂。

2.3 外部存储器在停止流程里扮演的角色

如果你的视频帧数据存在外部 PSRAM、SDRAM 或者 OctoSPI NOR Flash 里,那么停止视频链路时还得把外部存储器的状态考虑进来,因为外部存储器的时钟一旦关闭,正在执行的读操作就会立即失败。

我当时用的是外部 Quad-SPI PSRAM,帧缓冲放在里面。DMA2D 从 PSRAM 读数据转换成 RGB 格式。一开始我先把 DMA2D 的时钟关了,导致 DMA2D 还在读的途中直接被掐断,PSRAM 控制器那边就挂起了一个未完成的事务,后续再访问 PSRAM 时一直报超时。

正确的做法是:先让 DMA2D 停稳,确定没有针对 PSRAM 的在途访问了,再关闭外部存储器的时钟,或者将 SDRAM / PSRAM 切换到自刷新模式,把它 “冻结” 在当前状态。这个顺序一旦搞反,后面想恢复运行时遇到的问题会变得非常复杂,因为你需要先重置外部存储控制器才能恢复访问,可能导致整段显示数据都不可信。

3. Video Stop 的标准操作流程:五个关键步骤与代码

3.1 第一步:掐断视频数据源,让“生产端”先停下来

停止流程的第一个动作,永远是从最上游的数据源开始。我这里的视频数据源是一段 MJPEG 流,由一个定时器驱动的软件解码任务逐帧解码。停止时,我先设置一个全局标志位,通知解码任务处理完当前帧后立即退出循环,而不是马上打断它。

这一步有两个好处。第一,解码任务可以安全释放它占用的内存缓冲和 DMA 描述符,避免资源泄漏;第二,避免出现“帧解码到一半被强停,下一帧进来时数据不完整”的脏状态。

代码大致是这样:

volatile uint8_t video_source_stop_flag = 0; void video_source_abort(void) { video_source_stop_flag = 1; /* 请求解码任务退出 */ while (!video_source_stopped_flag) /* 等待解码任务确认退出 */ { /* 这里可以加超时保护,比如等待 100ms */ if (timeout_elapsed()) break; } }

使用标志位而不是直接挂起任务,可以让停止流程变得更加可控,不会在解码任务正在写帧缓冲时强行打断它。最终的效果是:解码器不会再向 DMA2D 或者下一级缓冲区提交新的数据帧,链路的最上游被掐断,后续步骤才能在不慌不忙的状态下进行。

3.2 第二步:安全停止 DMA2D,别一上来就 Abort

DMA2D 的停止要分两种情况处理。第一种,DMA2D 正在做长时间的大块传输,你希望尽快停住;第二种,DMA2D 刚好处于空闲或传输即将完成的状态,你只需要等待它结束。

如果直接调用 HAL_DMA2D_Abort() 把当前传输强制终止,可能造成源地址或者目的地址所在的帧缓冲只有一部分被更新,留下半个新帧和半个旧帧拼接的缓存状态。所以我采用的是“先查询状态,再决定是等待完成还是中止”。

void dma2d_safe_stop(void) { if (HAL_DMA2D_GetState(&hdma2d) == HAL_DMA2D_STATE_BUSY) { /* 如果当前是内存到内存的普通搬运,让它跑完比打断更安全。 这里等待完成,但要加超时。 */ HAL_DMA2D_PollForTransfer(&hdma2d, 1000); } else if (HAL_DMA2D_GetState(&hdma2d) == HAL_DMA2D_STATE_ERROR) { HAL_DMA2D_Abort(&hdma2d); __HAL_DMA2D_CLEAR_FLAG(&hdma2d, DMA2D_FLAG_TC | DMA2D_FLAG_TE); } /* 等待 DMA2D 状态机回到 READY */ while (HAL_DMA2D_GetState(&hdma2d) != HAL_DMA2D_STATE_READY) { /* 超时保护 */ } }

为什么等待比中止更好?因为 DMA2D 在传输过程中,操作的是目标帧缓冲的连续地址,如果传输被中断,下一帧的数据会从断点继续写入,目标缓冲区的后半段残留的是旧数据。这种数据不一致一旦发生,即使你后面把 LTDC 停了,下一次重新启动视频时也会从脏缓冲里读到错误内容。而等待传输完成,最多多花几十毫秒,换来的是链路上每一级缓冲都是干净的。

3.3 第三步:在 VSYNC 边界关闭 LTDC 图层和控制器

LTDC 的停止是整个 Video Stop 流程里最讲究时序的一步,因为 LTDC 是严格按照 HSync/VSync 时序刷新屏幕的。如果你在一个行中断的中间位置改它的层使能寄存器,硬件可能会按一个不一致的配置完成当前行的读取,严重时会在屏幕上出现一条固定位置的横向亮线,这就是非常典型的“闪横线”故障。

所以我建议的步骤是:先等一个垂直消隐信号(VSYNC)到达,然后再在中断服务程序里关闭图层,最后关闭 LTDC 控制器。

一个可行的做法是配置 LTDC 的行中断事件,在指定行号产生一个中断。把中断触发行设为帧的最后一行的前一两条线,这样中断触发时基本处于垂直消隐区域,执行关闭操作不会干扰到正在进行的行扫描。

void LTDC_IRQHandler(void) { if (__HAL_LTDC_GET_FLAG(&hltdc, LTDC_FLAG_LI)) { __HAL_LTDC_CLEAR_FLAG(&hltdc, LTDC_FLAG_LI); ltdc_vsync_marker = 1; /* 设置一个帧边界标记 */ } } void ltdc_safe_stop(void) { /* 等待一个帧边界靠近 */ ltdc_vsync_marker = 0; while (!ltdc_vsync_marker) { /* 等待,可加超时 */ } /* 关闭所有正在使用的图层 */ HAL_LTDC_DisableLayer(&hltdc, LTDC_LAYER_1); HAL_LTDC_DisableLayer(&hltdc, LTDC_LAYER_2); /* 关闭 LTDC 控制器本身 */ HAL_LTDC_Stop(&hltdc); /* 再强制清一次寄存器位,确保上一帧的状态不会残留 */ LTDC->GCR &= ~LTDC_GCR_LTDCEN; }

这里有一个容易被忽略的细节:HAL_LTDC_DisableLayer() 只是把 LTDC_LxCR 的 LEN 位清零,但 LTDC 控制器本身如果还在使能状态,它仍然会以透明背景继续刷新屏幕。所以图层全部关闭之后,要立刻调用 HAL_LTDC_Stop(),把 GCR 里的 LTDCEN 位也清掉,否则你会看到屏幕变黑,但行场时钟还在向外输出。

3.4 第四步:关闭 DSI/并口输出与背光

如果你的面板是 MIPI DSI 接口,LTDC 停止之后,DSI 主机控制器可能还维持着高速时钟向面板发送 LP/HS 信号。这时你不能直接关 DSI 的时钟,要先通过 DSI 主机发送一条“进入低功耗模式”或者“关闭显示”的命令给面板,等命令发送完成,再将 DSI 的 PHY 置于 shutdown 状态。

这一步很关键,因为有些 DSI 面板在收到关屏命令后,内部的行场扫描逻辑才会停止,你不发命令直接断电,面板内部状态可能停留在某个异常位置,下次上电时会出现显示偏移或者花屏。我的面板支持 DCS 标准的 0x28(Display Off)和 0x10(Sleep In)命令,时序上先发 0x28,延时 50ms,再发 0x10,延时 100ms,最后才把 DSI 时钟关掉。如果你用的是 RGB 并口屏,那就跳过 DSI 主机,直接把背光控制引脚拉低,再关闭 LTDC 的像素时钟。

背光的关闭顺序也有讲究。我一般是在 LTDC 图层关闭前先把背光亮度降到最低,再在 LTDC 控制器关闭后完全关闭背光电源。这样做的好处是:即使关闭过程中有一两帧异常画面,也会因为背光已经降暗而不易被用户察觉。直接瞬间关闭背光,虽然简单粗暴,但容易在关闭边缘捕捉到一个撕裂的残影,影响观感。

3.5 第五步:关闭外设时钟,准备进入低功耗

前面四步做完,视频链路上的外设已经全部停止,但它们的时钟可能还挂在系统时钟树上。如果此时直接进入低功耗模式,有些外设的时钟还在翻转,功耗自然降不下来。

所以我最后一步是逐个关闭 DMA2D、LTDC、DSI(如果启用)的时钟,同时检查它们对应的中断是否已屏蔽。

void video_peripheral_clk_off(void) { /* 关闭 DMA2D 时钟 */ __HAL_RCC_DMA2D_CLK_DISABLE(); /* 关闭 LTDC 时钟 */ __HAL_RCC_LTDC_CLK_DISABLE(); /* 关闭 DSI 时钟(如果启用) */ __HAL_RCC_DSI_CLK_DISABLE(); /* 如果 LTDC 的像素时钟来自独立 PLL,关闭对应 PLL */ HAL_RCCEx_DisablePll(...); /* 按实际工程调整 */ }

这里强烈建议在低功耗模式下关掉像素时钟相关的 PLL。LTDC 的像素时钟通常由一个独立 PLL 生成,进入低功耗前不关掉它,系统功耗会额外多出几毫安,这在用电池供电的便携设备里是没法接受的。

完整的五步流程跑完之后,我建议再用一个状态函数做一次收尾检查,确保所有寄存器状态都符合预期:

void video_stop_assert(void) { assert_param((LTDC->GCR & LTDC_GCR_LTDCEN) == 0); assert_param(HAL_DMA2D_GetState(&hdma2d) == HAL_DMA2D_STATE_READY); assert_param((DSI->CR & DSI_CR_EN) == 0); }

这一步相当于把“停止”和“已经完全停止”区分开,宁可多花几十微秒,也绝不让任何外设带着未知状态进入低功耗。

4. 实测踩过的坑:闪屏、撕裂、残留和重启后花屏

4.1 闪屏:停止顺序里最容易被忽略的“先关哪头”

我在第一版实现里,是严格按照“关解码 → 关 DMA2D → 关 LTDC → 关 DSI → 关时钟”的顺序写的,但还是偶发闪屏。后来抓了很长时间才发现,问题出在背光关闭的时机上。

我当时先关背光,再走后面的链路,从背光熄灭到 LTDC 真正停稳之间,有一小段窗口期,屏幕虽然背光灭了,但 LTDC 还在继续输出信号,面板的扫描电路仍然在工作,当 DMA2D 最后一帧数据写一半时,由于背光已经灭了,用户看不到这个变化,但如果此时触发了一瞬间的行场抖动,会让面板内部开始执行异常复位,从而在画面完全消失前闪出一丝亮线。

后来我把顺序调成:先通知背光进入降亮度模式,再执行完整停止流程,最后完全关闭背光电源。这样即使停止过程中有异常,背光处于低亮度状态,人眼几乎察觉不到。解决闪屏问题的关键不是让某个寄存器变快,而是调整好感知层面的时机。

4.2 撕裂:没等 VSYNC 就切换帧缓冲的下场

另一个高频问题:撕裂。我测试视频暂停再恢复时,偶尔会出现画面在中间位置产生明显的上下错位,好像一条锯齿形的横线把画面分成两半。这就是经典的 tearing 现象,原因是在 LTDC 扫描到屏幕中间某一行时,我切换了帧缓冲地址,LTDC 的上半行读的是旧帧缓冲地址,下半行读的是新帧缓冲地址,两个地址的内容不一样,于是显示错位。

解决方式就是我 3.3 节里说的:必须在 VSYNC 垂直消隐期间切换帧缓冲或关闭图层。ST 的 LTDC 外设本身并没有硬件上的帧缓冲自动切换保护,需要软件配合 VSYNC 中断来做安全的 buffer 切换。

我用 LTDC 的行中断做了一个简单的 “帧同步门闩”:

void switch_frame_buffer(uint32_t new_lcd_address) { ltdc_vsync_marker = 0; while (!ltdc_vsync_marker) { } /* 等到垂直消隐到来 */ LTDC_LAYER_1->CFBAR = new_lcd_address; __HAL_LTDC_RELOAD_CONFIG(&hltdc); }

实测下来,加上这个门闩之后,撕裂问题就再也没有出现过。代价是切换可能会等待一整帧的时间(比如 60Hz 屏最多等 16.7ms),但显示效果永远优先于速度。

4.3 残留画面:帧缓冲不清空会污染下一次启动

如果你需要彻底关闭视频显示而不是暂停,那么帧缓冲里的内容必须被显式清空,否则下一次开机时可能会看到上次残留的画面。

我遇到的现象是:设备从低功耗唤醒后,屏幕先短暂显示了一帧上一次的视频画面,然后才切换到主界面菜单。用户反馈就像“屏幕被冻住了”。排查后发现,原因是低功耗前我关闭了 LTDC,但帧缓冲里还保留着视频最后一帧的数据。唤醒后 LTDC 重新使能时,首先扫描的就是这个残留缓冲,如果此时背光也已经打开,用户会看到这帧残留画面一闪而过。

解决方法是:在最后一步关闭 LTDC 之前,先把当前显示用的帧缓冲全部填充为背景色(比如黑色或纯色)。

void clear_frame_buffer(uint32_t *buf, uint32_t size, uint32_t color) { for (uint32_t i = 0; i < size; i++) { buf[i] = color; } SCB_CleanDCache_by_Addr((uint32_t *)buf, size * sizeof(uint32_t)); }

注意这里有个缓存一致性的大坑。STM32U5 带 D-Cache,如果你用 CPU 直接写帧缓冲,写完必须做 Cache Clean 操作,否则数据可能还停留在 Cache 里,LTDC 读的真实内存地址还是旧数据。我在第一次实现清理函数时忘了加 CleanDCache,结果清了又好像没清,在项目里折腾了半天。

4.4 复位后花屏:DMA2D 还忙就把时钟关了

这个坑是让我真正重视“停止顺序”的导火索。现象是:从 STOP2 模式唤醒后,屏幕 90% 概率花屏,而且花的图案每次都不同,没有任何规律。

用调试器抓现场后,我发现 DMA2D 的状态机在唤醒后直接进入 ERROR 状态,而且 LTDC 的配置寄存器里还有未清零的层使能位。再往前追,问题出在进 STOP2 之前,我的代码把 DMA2D 时钟关了,但 DMA2D 当时其实还有一个 pending 的传输请求(这是我在某个中断服务程序里启动的异步操作,主流程并不知道它还在运行)。时钟一断,DMA2D 的状态机停在一个非法状态,复位时硬件不会自动清掉它,唤醒后自然就误动作。

这让我后来在代码里加了一个强制检查:关闭任何外设时钟之前,先查询外设状态,必要时等待超时后再关。宁可让停止流程多花几十毫秒,也不能让外设带着未完成任务被时钟“砍掉”。

4.5 定位这类问题的调试手段

遇到这类显示相关 bug,我常用的手段有三种,按优先级排序。

第一种,串口日志加时间戳,把每个停止步骤的进入和退出时间都打出来。这样能快速看出来哪个步骤异常,比如某个等待超时了,或者顺序有问题。我在项目里给每个停止函数都加了宏封装,打印耗时,很快就发现 DMA2D 等待完成经常需要额外 300ms,因为我在等它之前还有个外设一直没停干净。

第二种,逻辑分析仪 / 示波器挂在 HSync 和 VSync 引脚上观察时序。如果停止后 VSync 还在持续翻转,说明 LTDC 主使能或时钟配置没关干净。通过在 VSync 引脚上触发停止命令,能精确定位到哪一行扫描时执行了关闭操作,从而判断是否有撕裂风险。

第三种,也是最容易被忽略的,查看 DSI 面板端的错误报告寄存器。很多 MIPI DSI 面板内部有错误状态位,比如收到非法指令、CRC 错误、ECC 错误等。通过 DSI 读回面板寄存器,能快速确认面板是否已经正确进入了关闭状态,而不是停留在某个半开半关的异常状态。

5. 停止后功耗优化:从十几毫安降到几微安的实际配置

5.1 STM32U5 低功耗模式下显示外设的处理原则

STM32U5 系列主打低功耗,但如果你把整个显示链路的外设时钟都开着,它是很难进入真正的超低功耗状态的。我实测过,LTDC、DMA2D、DSI 三个外设的时钟如果不关,进入 STOP2 模式前的系统电流一直在 8~12mA 左右,这对电池供电设备来说是灾难性的。

进入低功耗前,需要确保的不只是外设时钟关闭,还包括中断状态的清理。外设时钟关掉后,如果对应的中断标志位没有手动清除,唤醒时容易触发误中断,导致系统从 STOP2 唤醒后立刻又跑进某个显示相关的中断服务程序,重新把外设时钟打开,功耗功亏一篑。

所以在进入低功耗前,我会做一次外设中断的全面掩码操作:

void video_peripheral_irq_mask(void) { NVIC_DisableIRQ(DMA2D_IRQn); NVIC_DisableIRQ(LTDC_IRQn); NVIC_DisableIRQ(LTDC_ER_IRQn); NVIC_DisableIRQ(DSI_IRQn); /* 清除挂起的中断标志 */ NVIC_ClearPendingIRQ(DMA2D_IRQn); NVIC_ClearPendingIRQ(LTDC_IRQn); NVIC_ClearPendingIRQ(DSI_IRQn); }

这一步不做干净的话,即使 RCC 里的时钟已经关闭,中断控制器里挂起的 pending 状态也会在唤醒瞬间把你重新带进显示流程,非常坑。

5.2 带外部 PSRAM/SDRAM 时不能直接进 STOP2

如果帧缓冲放在外部 PSRAM 或 SDRAM 里,进低功耗前还需要确认外部存储器的状态。我用的 PSRAM 是通过 OctoSPI 接口连接的,进入 STOP2 前我做了两件关键操作:先把 PSRAM 切换到自刷新模式,再关闭 OctoSPI 控制器时钟。

PSRAM 进入自刷新模式后,即使外部时钟停了,它内部也会自己维持数据,不会丢内容。这相当于让外部存储器和 CPU 解耦,CPU 进低功耗,PSRAM 自己保持数据。唤醒后先重新使能 OctoSPI 时钟,再执行一次“退出自刷新”命令,就能正常继续访问。

这个顺序在数据手册里有明确操作步骤,但我第一次做时只做了“退出自刷新”,没有在进入低功耗前先执行“进入自刷新”,结果唤醒后读回来数据全是 0xFF,因为我那时候的 PSRAM 由于失去外部时钟,内部状态其实已经乱了。

5.3 实测三组功耗数据对比

我把同一套配置、同一块板子,测试了三组不同停止策略下的功耗数据,结果非常直观:

测试场景停止策略待机电流(典型值)
场景 A只调用 HAL_LTDC_Stop(),不关外设时钟,不关 DMA2D11.8 mA
场景 B按顺序停止数据源/DMA2D/LTDC/DSI,并关闭外设时钟1.4 mA
场景 C场景 B + 外部 PSRAM 自刷新 + 关闭 PLL + 屏蔽中断4.2 µA

场景 A 和场景 C 之间差了接近 3000 倍,这就是为什么“把 Video Stop 做干净”在功耗敏感型产品里极其重要。如果只是把功能跑通,不追求低功耗,场景 A 也能用;但如果你想做便携式仪表、电池门显、手持终端,必须做到场景 C。

另外,进入 STOP2 模式后,还建议关闭内部 LDO 稳压器的旁路模式,如果硬件设计上启用了 SMPS 开关电源供电,要让 STM32U5 切换到 SMPS 模式下,确保内核电压域的效率达到最高。这些细节在参考手册的电源管理章节里都有,实测能再降零点几毫安。

6. 长期稳定做 Video Stop 调试,我养成的几个习惯

做视频停止这个功能,前前后后折腾了差不多三周,最后稳定运行后,我总结出几个值得长期保留的习惯,分享给后来者。

第一个习惯:给每个停止步骤加状态机。不要用一堆无状态的顺序函数拼接停止流程,而是用一个 stop_state 变量维护当前阶段,每个阶段完成后再推进到下一个阶段。这样如果某个阶段卡住或者超时,你可以迅速定位到是哪个外设的问题,而且方便做“重复停止”操作——用户可能在视频已经停止的状态下再次按下停止键,无状态函数没法区分“已经在停止过程中”和“全新的停止请求”。

第二个习惯:统一的超时机制。所有等待循环,包括等待 DMA2D 完成、等待 VSYNC、等待 DSI 命令发送,都必须加超时保护。我最初没加,结果遇到一个极端情况下 DMA2D 永远等不到完成中断,整个系统死循环在停止流程里,连看门狗喂狗的任务都被饿死了。后来我把所有等待都统一封装成wait_with_timeout(),超时后强制进入错误恢复流程,虽然不能完全避免异常,但至少不会让系统卡死。

第三个习惯:做停止前后的状态快照对比。我把停止前和停止后关键寄存器的值都保存下来,比如 DMA2D_ISR、LTDC_GCR、LTDC_L1CR、DSI_CR,通过对比能快速发现哪些标志位没有清零。这个方法在好几次疑难问题排查里起了大作用,比肉眼盯调试变量列表高效得多。

第四个习惯:把 Video Stop 做成可重复调用的“恢复友好”函数。停止后重新启动视频,不是简单地把停止流程反过来走一遍,因为有些外设的状态不是对称的。比如 DMA2D 停止后,它内部的状态机可能还保留着上一个配置,重新启动前必须重新 Init 或者 Reset;DSI 面板从 Sleep In 恢复过来需要至少 100ms 延时。这些不对称的恢复时序,我在代码里单独用一个 video_start_after_stop() 函数维护,避免和首次启动的路径混在一起。

回到最初那个“黑屏但没断电”的故障,现在回看,本质原因只有一个:我只让软件停止,没有让硬件链路上的每个环节都停止。嵌入式开发里这种问题非常多见,表面上是“一个功能没做好”,实际上是对硬件的完整数据路径缺乏掌控。希望这篇关于 STM32U5G9ZJT6Q Video Stop 的复盘,能帮你在做类似功能时少走几个弯路。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 9:10:44

微信小程序+云开发:校园跑步社交系统的设计与实现

简介&#xff1a;这是一套面向计算机类本科毕业生的完整毕业设计资源&#xff0c;聚焦校园场景下的运动社交需求&#xff0c;提供从开发到答辩的全流程支撑材料。项目以微信小程序为载体&#xff0c;实现跑步轨迹记录、实时配速里程、整公里语音提醒、周/月排行榜、打卡分享、线…

作者头像 李华
网站建设 2026/9/13 9:11:15

别再来一个 Log4Shell:深入解析 Log4j 2 反序列化允许列表绕过

作者&#xff1a;来自 Elastic Ruben Groenewoud, Terrance DeJesus, Bryan Porras Blanch, Eric Forte 这不是另一个 Log4Shell。Log4Shell&#xff08;CVE-2021-44228&#xff09; 是由日志记录字符串触发的 JNDI 查找。而 8 月报告的这个绕过问题是 Log4j 2 的 FilteredObje…

作者头像 李华
网站建设 2026/9/13 9:09:58

表格识别:从「一行行文字」到「一格一格的表」--OnnxOCRRapidTable

表格识别&#xff1a;从「一行行文字」到「一格一格的表」–OnnxOCR&RapidTable 1、写在前面 8月初&#xff0c;整个东北笼罩在穹顶之下&#xff0c;酷热难耐之时&#xff0c;我到传奇小城鹤岗的客户现场出差&#xff0c;算是躲过了热浪。 我对这座城市最久远的印象应该是学…

作者头像 李华
网站建设 2026/9/13 9:09:59

大厂语音算法岗笔试怎么考?网易2018真题考点拆解与备战指南

每年这个时候都有不少准备投语音方向的同学来问我&#xff0c;说网易这类大厂的算法工程师笔试卷到底考什么、难不难、怎么准备。翻到当年的网易2018校招语音算法工程师笔试卷&#xff0c;说实话&#xff0c;哪怕放到现在来看&#xff0c;这份卷子的考点覆盖和出题思路依然很有…

作者头像 李华
网站建设 2026/9/1 13:02:58

GradCuit:测试时推理的梯度流与信用分配机制解析

第一眼看到 GradCuit 这个题目&#xff0c;我最大的感受是&#xff1a;它把测试时推理从“搜多少条文本路径”变成了“在连续潜在空间里按梯度流优化状态”&#xff0c;再通过信用分配告诉每个中间潜在步骤&#xff0c;你该为最终结果承担多少责任。这不算一个和思维链完全对立…

作者头像 李华