STM32 低功耗 STOP 模式唤醒后时钟重配:为什么唤醒后外设时序全乱了?从 HSI 回切 HSE/PLL 的恢复流程

举报
yd_223794387 发表于 2026/11/05 08:09:54 2026/11/05
【摘要】 一、问题从哪里来做电池供电的 STM32 产品时,STOP 模式是最常用的低功耗档位:内核停了,大部分时钟停了,SRAM 和寄存器内容保留,RTC 还能继续走,功耗可以从运行时的几十毫安降到几微安到几十微安。很多工程问题都出在“唤醒之后”:程序明明从 STOP 里出来了,串口波特率却变了,定时器周期不对,ADC 采样间隔漂了,甚至 I2C 从机不再应答。查了半天代码逻辑没问题,最后发现是时钟...

一、问题从哪里来
做电池供电的 STM32 产品时,STOP 模式是最常用的低功耗档位:内核停了,大部分时钟停了,SRAM 和寄存器内容保留,RTC 还能继续走,功耗可以从运行时的几十毫安降到几微安到几十微安。很多工程问题都出在“唤醒之后”:程序明明从 STOP 里出来了,串口波特率却变了,定时器周期不对,ADC 采样间隔漂了,甚至 I2C 从机不再应答。查了半天代码逻辑没问题,最后发现是时钟没恢复——STOP 唤醒后,系统时钟被硬件强制切回了内部高速时钟 HSI,而原来用的 HSE 加 PLL 的高速时钟链路并没有自动回来。
这个行为不是芯片 bug,而是数据手册里写明的复位后默认值逻辑:进入 STOP 时,HSE 和 PLL 会被关闭以省电;退出 STOP 时,时钟控制逻辑先用 HSI 把内核拉起来,保证程序能继续执行,至于要不要回到原来的高速时钟,由软件自己决定。如果软件只写了进入 STOP 的代码,忘了写唤醒后的时钟恢复代码,系统就会一直跑在 HSI 上。HSI 典型频率是 16MHz(不同系列有 8MHz 或 16MHz 等取值,以具体型号手册为准),而原来 PLL 之后可能是 72MHz、168MHz 甚至更高。分频没变,时钟源变了,所有依赖系统时钟推导出来的外设时序都会按比例错位。
二、一个直观的算式:波特率为什么会错
假设原来系统时钟 SYSCLK 是 72MHz,USART1 挂在 APB2 上,波特率 115200。进入 STOP 前,串口分频按 72MHz 算好;唤醒后 SYSCLK 变成 16MHz 的 HSI,如果 USART 的 BRR 寄存器没有重新计算,实际波特率会变成:实际波特率 ≈ 115200 × 16 / 72 ≈ 25600。对方按 115200 收,就会全是乱码。定时器也一样:原来 72MHz 下 7200 分频得到 10kHz 计数,唤醒后同样分频只能得到约 2.22kHz,PWM 周期和采样节拍全部拉长。ADC 采样率、I2C 的 SCL 速率、SPI 的 SCK 速率,推导链路里只要有 SYSCLK 或 APB 时钟,都会跟着错。所以“唤醒后先恢复时钟,再恢复外设时序”应当写成固定流程,而不是靠事后排查。
三、进入 STOP 之前要做的准备
第一,确认唤醒源已经配好。常用的是 RTC 唤醒定时器、外部中断线(EXTI)或 WKUP 引脚。只配了进入 STOP、没配唤醒源,芯片会一直睡下去,只能靠复位唤醒,调试时很容易误以为程序死机。
第二,处理好正在进行的外设传输。DMA 正在搬数、串口正在发数据时直接进 STOP,唤醒后传输状态可能停在半截。稳妥做法是:发送完成后再进 STOP,或者在进入前挂起 DMA 并记录传输进度,唤醒后按需续传或重发一帧。I2C 主机正在等从机应答时进 STOP,唤醒后总线可能停在奇怪状态,必要时按 I2C 总线恢复流程发 9 个时钟脉冲把从机顶出来。
第三,保存关键外设的状态。STOP 会关掉不少时钟域,唤醒后部分外设寄存器需要重新初始化。可以在进入前把“需要恢复的外设清单”记下来,唤醒后逐项恢复,而不是指望所有寄存器原样保留。
四、唤醒后的标准恢复流程
推荐的顺序是:先恢复系统时钟,再恢复 SysTick,再恢复外设。顺序反了,外设初始化时用的时钟还是错的,算出来的分频还是错的。典型代码结构如下(以 HAL 库为例,寄存器名与函数名按所用系列调整):

/* 进入 STOP 之前:选择 RTC 唤醒等唤醒源已配好,挂起 SysTick */
HAL_SuspendTick();
HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI);
/* 从 WFI 唤醒,执行到这里时 SYSCLK 已是 HSI */
/* 第 1 步:恢复系统时钟,回到 HSE + PLL */
SystemClock_Config();
/* 第 2 步:恢复 SysTick,HAL_Delay 才能正常工作 */
HAL_ResumeTick();
/* 第 3 步:按需重新初始化对时钟敏感的外设 */
MX_USART1_UART_Init();
MX_TIM2_Init();

SystemClock_Config 里的关键动作是:先使能 HSE 并等待就绪,再使能 PLL 并等待锁定,最后把系统时钟源切到 PLL,逐级设置 AHB、APB1、APB2 分频。不能跳过“等待就绪”这一步:HSE 起振需要时间,PLL 锁定也需要时间,时钟源没稳就切过去,内核可能跑在不稳定的时钟上,表现为偶发死机或外设初始化失败。稳妥的写法是每一步都查就绪标志,超时则走错误处理,不要无限死等。

void SystemClock_Config(void)
{
    RCC_OscInitTypeDef RCC_OscInitStruct = {0};
    RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
    /* HSE + PLL:唤醒后 HSE 是关闭的,必须重新打开 */
    RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
    RCC_OscInitStruct.HSEState = RCC_HSE_ON;
    RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
    RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
    RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9; /* 按目标频率改 */
    if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
    {
        Error_Handler();
    }
    /* 切系统时钟到 PLL,设置总线分频 */
    RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_SYSCLK
                                | RCC_CLOCKTYPE_HCLK
                                | RCC_CLOCKTYPE_PCLK1
                                | RCC_CLOCKTYPE_PCLK2;
    RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
    RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
    RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
    RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
    if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
    {
        Error_Handler();
    }
}

需要注意 Flash 等待周期(FLASH_LATENCY)要和恢复后的目标频率匹配。低频跑时等待周期可以小,高频跑时等待周期必须加大,否则取指会出错。有的系列还需要重新配置电压调节器档位或使能预取指/缓存,这些都应当放在时钟恢复函数里一起处理,不要只恢复时钟源。
五、容易漏掉的四个细节
第一,SysTick。HAL 的延时和超时都依赖 SysTick。进入 STOP 前挂起它,唤醒后必须恢复,否则 HAL_Delay 会一直等不到滴答,或者超时判断全乱。自己用裸机 SysTick 的工程同样要检查重装载值:重装载值是按进入前的时钟算的,时钟恢复后要确认它仍然对应 1ms。
第二,LSE 与 RTC。RTC 唤醒用的是 LSE 或 LSI,STOP 期间 RTC 时钟域一般保持工作,唤醒后不要重复初始化 RTC 而把计数器清零。需要确认的是备份域的访问权限与 RTC 唤醒标志:唤醒后及时清标志,否则下一次进入 STOP 时可能立刻被旧标志唤醒,表现为“刚睡下就醒了”。
第三,GPIO 与外部电路状态。STOP 期间 GPIO 状态一般保持,但唤醒后如果重新初始化 GPIO,要注意不要把正在驱动外部器件的引脚短暂拉成别的电平。功耗敏感的设计里,浮空输入引脚要在进入 STOP 前配成模拟输入或上下拉,避免漏电流把省下来的电又漏回去。
第四,功耗与唤醒时间的取舍。STOP 模式有不同的子模式,有的系列分 STOP0、STOP1、STOP2,稳压器档位和外设保留范围不同,唤醒时间也不同。选子模式时要同时看两张表:功耗表和唤醒时间表。需要快速唤醒、频繁唤醒的场景,选唤醒快但功耗稍高的档位反而总功耗更低;一天只唤醒几次的场景,选最深的档位才划算。不要只看“最低功耗”那一行就定方案。
六、调试方法:先看时钟,再看外设
遇到“唤醒后时序全乱”,调试顺序应当是:先确认唤醒后的 SYSCLK 到底是多少,再查外设。可以在唤醒后立刻用 MCO 引脚把系统时钟输出到示波器,或者在代码里读 RCC 寄存器、把 SYSCLKSource 与各分频打印出来。确认时钟恢复正确后,再逐个查串口、定时器、ADC 的分频配置。如果时钟没恢复就去调外设分频,调完一次、换个唤醒路径又错一次,永远调不完。
另一个实用的自检办法是:在唤醒恢复函数末尾加一个断言性质的检查——读回当前 SYSCLK 频率,与期望值比较,不一致就进错误处理并点灯或打印提示。上线前把这个检查保留在调试版本里,能省掉很多“偶发乱码”的排查时间。
七、小结
STOP 模式省电的关键在进入前,唤醒后不出问题的关键在恢复流程。记住三句话:唤醒后时钟先回 HSI,高速时钟链路不会自动回来;恢复顺序是时钟、SysTick、外设,先恢复时钟再初始化外设;HSE 起振与 PLL 锁定必须等就绪标志,Flash 等待周期要随频率改。把这套流程写成项目里的固定模板,每次用 STOP 模式都走同一套代码,唤醒后的时序问题基本不会再出现。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。