STM32 低功耗共有三种模式:
Sleep 睡眠模式 、Stop 停止模式、Standby 待机模式。
三种模式的 操作图解 地址:
- 【STM32 + CubeMX】低功耗 -- Sleep 睡眠模式
- 【STM32 + CubeMX】低功耗 -- STOP 停止模式 【本文】
- 【STM32 + CubeMX】低功耗 -- Standby 待机模式 【待更】
本文将重点拆解Stop 停止模式 的代码实现、关键事项 。
- Keil 工程链接 -- 【STM32F407 低功耗 --- Stop 停止模式】 待更中...
- 实验所用硬件 --【STM32F407VE 开发板】
目录
[一、 STM32 三种低功耗模式 区别](#一、 STM32 三种低功耗模式 区别)
[三、STOP 停止模式 代码实现](#三、STOP 停止模式 代码实现)
[四、 验证 & 测试 & 排查](#四、 验证 & 测试 & 排查)
一、 STM32 三种低功耗模式 区别
1、睡眠模式 Sleep Mode
- 功耗:芯片工作电流约 10~20mA (参考值)
- 特点:CPU在执行完 Sleep 这一行后暂停;外设正常工作;唤醒时间极快;
- 注意1:能被所有中断事件唤醒;若无法进入 Sleep,优先检查是否未关闭 SysTick 等频繁中断。
- 注意2:被中断唤醒后,先进入对应中断函数执行,完成后再运行 Sleep 下一行代码。
- 注意3:Sleep 模式无硬件唤醒标志,唤醒后无需额外处理;通常自定义软件标志配合业务逻辑。
|------|------------------------------------------------------|
| 停止资源 | CPU内核停止运行、内核时钟关闭、程序暂停运行 |
| 保持资源 | 系统时钟、所有外设时钟保持运行 GPIO 口状态保持运行时的电平 内存、寄存器 数据保持不变 |
| 唤醒方式 | 任意中断 或唤醒事件均可唤醒 (如 GPIO中断、Systick、UART、RTC 等) |
| 唤醒后 | 程序正常运行,无需重新初始化时钟 / 外设。 |
| 适合场景 | 短时间无需 CPU 运算, 但需要外设(如串口、ADC、定时器)持续工作的场景 |
2、停止模式 Stop Mode
- 功耗:芯片工作电流约 1mA (参考值)
- 特点:系统时钟关闭;CPU和外设停止;内存数据保持;唤醒后仅需恢复系统时钟
|------|--------------------------------------------------------------------|
| 停止资源 | CPU 与所有外设停止工作 内核时钟、外设时钟,主时钟(HSE/HSI)被关闭 外设停止工作 |
| 保持资源 | 低速时钟(LSI/LSE,用于 RTC)继续运行 内核1.8V供电保持 内存、寄存器 数据保持不变; GPIO 口状态保持运行时电平 |
| 唤醒方式 | 外部中断 EXTI RTC 闹钟 IWDG 独立看门狗 |
| 唤醒后 | 不需要重新初始化 UART、SPI、ADC等外设 必须重新配置系统时钟(如启用 HSE/PLL) |
| 适合场景 | 需要低功耗、同时要求快速唤醒、数据不丢失的场合。 |
3、待机模式 Standby Mode
- 功耗:芯片工作电流约 4uA (参考值)
- 特点:几乎断电,数据丢失,唤醒后相当于复位
|------|--------------------------------------------------------------------------------------|
| 停止资源 | 内核1.8V电源关闭 内核与外设时钟关闭,但 LSI/LSE 可在备份域继续给 RTC 供电 内存、寄存器数据全部丢失 IO 口变为高阻态(模拟输入/浮空输入) |
| 保持资源 | 备份域(Backup Domain):备份寄存器、RTC 备份区域; 唤醒引脚(PA0) RTC 相关引脚 复位引脚 |
| 唤醒方式 | WAKEUP 引脚(PA0)上升沿 RTC 闹钟 |
| 唤醒之后 | 系统完全复位 程序从 main 函数从头开始执行。 可通过检查 PWR_CSR 的 SBF / WUF 标志判断是待机唤醒还是普通复位 |
| 适合场景 | 长时间休眠、对功耗要求极致的电池供电设备、遥测终端等 |
注意:
WAKEUP 引脚 (PA0) 的上升沿自动唤醒功能, 仅适用于 Standby 待机模式;
虽然同为 PA0,睡眠、停机模式下,PA0 无自动唤醒功能。但可配置为普通 EXTI 中断以唤醒。
二、工程准备
1、基础工程
三种低功耗模式均支持 RTC 闹钟唤醒,因此统一使用 RTC 闹钟作为工程模板,添加低功耗测试。
不熟悉 RTC 闹钟的朋友,参考教程:
本文所用工程模板:【 Keil 工程文件 -- RTC 闹钟 】
当然,你可以使用自己的工程模板、需要的唤醒方式:
- Sleep 睡眠模式:任意中断事件唤醒。
- Stop 停止模式:外部中断 EXTI 、RTC闹钟、IWDG 独立看门狗
- Standby模式:WAKEUP 引脚 PA0 的上升沿 、 RTC 闹钟
2、烧录问题
STM32 进入Stop、Standby模式后, 无法正常响应烧录信号,仿真器可能无法正常烧录。
STLink 仿真器的解决方法:
- 方法1:按着板子复位按键,不放手,keil 点击 Load 后,马上放开复位按键。利用复位后的空档期识别烧录信号。
- 方法2:BOOT0接线3.3V,按一次复位按键,烧录。拔掉BOOT0接线,按一次复位按键,即可。
CMSIS-DAP 仿真器的解决方法:
- Connect这一项:选择 under Reset,即可随时烧录。

JLink 仿真器:
- Stop 模式,不影响,随时可烧录。
三、Stop 停止模式 代码实现
进入 Stop 停止模式本身很简单,只需调用HAL库函数。
真正的重点在于:进入前的准备、唤醒后的处理、以及低功耗进入的时机。
重点:Stop 模式 唤醒后,必须手动恢复系统时钟,否则所有外设、串口、延时全部异常!
本节以 RTC 闹钟 B 周期性唤醒 为例,演示进入 Stop 停止模式、被中断唤醒、并处理唤醒后任务。
方案仅作思路参考,实际项目可灵活扩展。
1. 声明自定义唤醒标志
Stop 模式 没有硬件唤醒标志位,唤醒后也无需清零任何硬件标志。
我们通常自定义一个软件标志变量,用于在中断与主循环之间传递 "已唤醒" 状态,属于状态机编程思路。
具体操作
- main.c文件的 /* USER CODE BEGIN PV */ 区域声明:
cppuint8_t wakeUpFlag = 0; // 唤醒标志; 0-无任务、1-已唤醒,按需执行任务
2. 进入 Stop 模式
进入Stop 模式的核心函数是 HAL_PWR_EnterSTOPMode ()。
我们编写一个函数:把进入 Stop 前的准备工作、唤醒后的时钟恢复工作,整合在一起 。
关键点提醒:
- 进入前:需要关闭可能频繁唤醒的中断,如SysTick。
- 唤醒后:必须调用 SystemClock_Config() 重新配置系统时钟。否则,程序将运行在 HSI 内部高速时钟。
- 唤醒后:恢复中断源(如 SysTick)。
具体操作:
- main.c文件 ,在 /* USER CODE BEGIN 0 */ 区域
- 在闹钟B中断回调函数的下方,添加函数。(按你所用中断回调函数位置而定)
- 注意:这个函数预先埋下了一个bug,用于展示 唤醒后系统时钟变化。
cpp/****************************************************************************** * 函 数: EnterStopMode * 功 能: 进入 STOP 低功耗模式 * 说 明: 1. CPU、外设停止工作,主时钟关闭 * 2. 内存/寄存器数据保持 * 3. 可被任意EXTI中断或RTC事件唤醒 * 4. 唤醒后,系统时钟工作在HSI(8MHz), 必须重新配置系统时钟(HSE/PLL),否则后续代码无法正常运行 * 5. 无需重新初始化外设(UART、SPI等) * 6. 特别地注意: * 按芯片手册所描述,STOP模式是不能被外设中断唤醒的,如systick、UART、SPI等。 * 但是有一个特殊情况: * 如果进入STOP模式时SysTick中断是使能的,并且DBG_STOP位在DBGMCU_CR中被置位(启用了Stop模式调试),系统会立即从 STOP 模式唤醒。 * 至于DBG_STOP为什么会被置位?当连接着调试器时,可能会自动或间接地改变DBGMCU_CR中DBG_STOP位的状态。 * 这是WFI指令的工作机制 + 调试模式下的硬件限制共同作用的结果。 * 因此,为了避免意外情况,进入低功耗前关闭 SysTick 是最佳选择。 * 参 数: 无 * 返回值: 无 * 备 注: 最后修改_2026年03月24日 ******************************************************************************/ void EnterStopMode(void) { // 打印提示 printf("\r\n\r\n"); // 打印 换行 printf("① 即将进入 STOP 模式!========== \r\n"); // 打印 提示 /* 配置RTC闹钟 5秒后触发 */ RTC_SetAlarmB(5); // 设置RTC的闹钟B, 5秒后触发一次 /* 进入前:关闭可能频繁唤醒的中断源 */ HAL_SuspendTick(); // 关闭SysTick中断,避免意外唤醒 /* 进入STOP模式:CPU暂停在此处 */ // 执行完此函数后,系统时钟(HSE/PLL)已停止,MCU处于低功耗状态。 // 被中断唤醒后,程序会跳进中断函数执行,再回到此函数继续执行下一行。 HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI); // 进入STOP模式; 参数:PWR_LOWPOWERREGULATOR_ON 更低的静态功耗,PWR_STOPENTRY_WFI 等待中断唤醒 /* 唤醒后:重新配置系统时钟,这一步至关重要! */ SystemClock_Config(); // 调用CubeMX生成的系统时钟配置函数,恢复为168MHz主频 /* 唤醒后:恢复中断源 */ HAL_ResumeTick(); // 恢复Systick中断 // 打印提示 printf("② 已退出 STOP 模式!\r\n"); // 打印 提示 退出STOP模式 }
重要 1:设置RTC闹钟B
这里的示范是在每次进入 Stop 模式前,重新设置闹钟B:进入 Stop 后,将5秒后触发、唤醒。
你也可以在其它需要的位置、条件下操作。
重要 2: 系统时钟配置函数 SystemClock_config ()
SystemClock_Config() 函数是CubeMX自动生成的,每个HAL库工程都有此函数,位于 main.c 。
此函数的作用是:设置系统时钟 ,从HSI切换为HSE,并配置为 168Mhz 。
重要 3: SysTick 中断 与 Stop 模式
按芯片手册所描述:Stop 模式无法通过 SysTick、UART、SPI 等常规外设中断直接唤醒,只能通过 GPIO 中断、RTC 闹钟、 IWDG 唤醒。
网上众多教程,Stop 模式的操作,也无需关闭、重开 SysTick 中断。
而上面编写的函数:在进入 Stop 前关闭SysTick中断,唤醒后重新开启SysTick中断。
这个额外的操作,是为了避免一个特殊情况:
如果进入 Stop 模式时SysTick中断是使能的,并且DBG_STOP位在DBGMCU_CR中被置位(启用了Stop模式调试),系统会立即从 Stop 模式唤醒。
这个DBG_STOP什么时候被置位了?当芯片连接着调试器时,会自动或间接地改变DBGMCU_CR中DBG_STOP位的状态。(个人理解,并非准确的官方描述)。
这是WFI指令的工作机制 + 调试模式下的硬件限制共同作用的结果。
因此,为了避免这种情况,进入 Stop 模式前,主动关闭 SysTick 是最佳选择。
这个情况,除了关闭SysTick中断外,还有另一种方法,在进入 Stop 前调用:
cpp
HAL_DBGMCU_DisableDBGStopMode (); // 禁用Stop模式调试,避免勘误触发
即可主动、明确地禁用Stop模式调试,避免勘误触发。
3. 唤醒 Stop 模式 (以 RTC闹钟B 为例)
Stop 模式的唤醒支持:外部中断(EXTI)、RTC闹钟、IWDG 独立看门狗。
本篇使用 RTC 闹钟B 的中断来唤醒。在回调函数中设置唤醒标志,并重新配置下一个闹钟。
具体操作:
- 在闹钟B的中断回调函数里,将唤醒标志置1,即添加:wakeUpFlag = 1;
cpp/****************************************************************************** * 函 数: HAL_RTCEx_AlarmBEventCallback * 功 能: RTC 闹钟B 中断回调函数 * 说 明: 1. 闹钟B触发时自动调用 * 2. 函数名固定,闹钟B必须带 Ex (与闹钟A中断回调函数不同) * 3. 如需循环触发,必须重新执行 RTC_SetAlarmB() * 参 数: hrtc:RTC句柄 * 返回值: 无 * 备 注: 最后修改_2026年03月17日 ******************************************************************************/ void HAL_RTCEx_AlarmBEventCallback(RTC_HandleTypeDef *hrtc) { /* 闹钟触发提示 */ printf("闹钟B 中断触发!! \r\n"); /* 读取 RTC 时间日期 */ RTC_TimeTypeDef time = { 0 }; // 时间 时-分-秒 RTC_DateTypeDef data = { 0 }; // 日期 年-月-日 HAL_RTC_GetTime(hrtc, &time, RTC_FORMAT_BIN); // 读取时间数据; 必须先读时间后读日期; 写操作时,也必须先写时间后写日期,否则数据可能错乱 HAL_RTC_GetDate(hrtc, &data, RTC_FORMAT_BIN); // 读取日期数据; 必须先读时间后读日期; 写操作时,也必须先写时间后写日期,否则数据可能错乱 /* 打印 时间日期,便于观察调试 */ printf("当前时间: "); printf("%04d年%02d月%02d日 ", 2000 + data.Year, data.Month, data.Date); // 打印 日期 printf("%02d:%02d:%02d \r\n", time.Hours, time.Minutes, time.Seconds); // 打印 时间 /* 【关键】设置唤醒标志,通知主循环执行任务 */ wakeUpFlag = 1; // 设置唤醒标志,通知主循环执行任务 }
重要提醒:
绝对禁止在中断回调函数里直接调用低功耗模式函数,极易造成程序卡死或死循环。
编写完成后,参考如下:

4、主循环:唤醒后处理任务
具体操作:
- 在while循环中循环中判断唤醒标志,执行业务逻辑,执行完成后再次进入睡眠。
cpp/** 检查是否被唤醒 **/ if (wakeUpFlag == 1) { /* ===== 唤醒后执行的任务 ===== */ printf("③ 唤醒后执行任务...!"); // 执行各种任务,如, 读取温度, 电机动作 等 /* 任务完成,清除标志 */ wakeUpFlag = 0; // 唤醒标志 置0,等待下一次唤醒 } else { /* 无任务时进入睡眠,降低功耗 */ EnterStopMode(); // 进入 STOP 停止模式 }
添加完成后,位置参考下图:
这里的 else,将在首次上电就立即进入Stop模式(因为wakeUpFlag初始为0)。
也可以改为由特定条件触发进入Stop, 而非else分支。
四、 验证 & 测试 & 排查
现在,Stop 模式 的所有操作:标志、进入(含时钟恢复)、唤醒、执行唤醒任务,都已处理完毕。
1、程序运行设想
按正常的设想,串口助手 printf 的顺序是:
进入Stop -> 闹钟B触发 -> 设置闹钟B -> 退出Stop -> while执行任务
2、真实运行效果
编译烧录后,串口助手输出如下图:
- 闹钟B 正常触发、Stop 正常唤醒
- Stop 进入前、结束前,printf 正常
- 闹钟B回调函数里的 printf 输出:乱码或空白

3、问题根源
有调试经验的老司机,毒眼可以一下子看穿,并已串联成排查思路了。
- 这些空白占位,其实是有数据的,只是波特率不对,导致数据错误,并非是可正常显示的编码。
- 波特率不对,一般就是 系统时钟配置 错误!
- 进入Stop前显示正常,结束后也显示正常,唯独 闹钟B 中断回调函数内 printf 异常!
- 那么,bug的定位,就在 进入Stop 后 与 闹钟B回调函数的 第一行printf之间!!
是的,问题就在闹钟B回调函数的 printf之前 !!
回顾我们的 EnterStopMode() 函数:

上图能清晰地看到流程上的问题所在:
虽然我们在唤醒后,重新配置了系统时钟,但位置没安排妥当,导致中断回调函数里的运行,是内部低速时钟HSI时钟,8M,并非 168MHz。
4、正确的操作 (修正问题)
在唤醒的第一时间 (中断回调函数开头,先配置系统时钟,再执行其它操作!!
具体操作:
- 删除 EnterStopMode() 函数里的 系统配置 函数
- 中断回调函数里,第一行,即唤醒后执行的第一行,配置系统时钟!完整函数如下:
cppvoid HAL_RTCEx_AlarmBEventCallback(RTC_HandleTypeDef *hrtc) { /* 唤醒后:重新配置系统时钟,这一步至关重要! */ // 调用 CubeMX 生成的系统时钟配置函数,恢复为 168MHz 主频 */ SystemClock_Config(); // 配置系统时钟,重新运行于168MHz /* 闹钟触发提示 */ printf("闹钟B 中断触发!! \r\n"); // 打印提示 /* 读取 RTC 时间日期 */ RTC_TimeTypeDef time = { 0 }; // 时间 时-分-秒 RTC_DateTypeDef data = { 0 }; // 日期 年-月-日 HAL_RTC_GetTime(hrtc, &time, RTC_FORMAT_BIN); // 读取时间数据; 必须先读时间后读日期; 写操作时,也必须先写时间后写日期,否则数据可能错乱 HAL_RTC_GetDate(hrtc, &data, RTC_FORMAT_BIN); // 读取日期数据; 必须先读时间后读日期; 写操作时,也必须先写时间后写日期,否则数据可能错乱 /* 打印 时间日期,便于观察调试 */ printf("当前时间: "); printf("%04d年%02d月%02d日 ", 2000 + data.Year, data.Month, data.Date); // 打印 日期 printf("%02d:%02d:%02d \r\n", time.Hours, time.Minutes, time.Seconds); // 打印 时间 /* 【关键】设置唤醒标志,通知主循环执行任务 */ wakeUpFlag = 1; // 设置唤醒标志,通知主循环执行任务 }
修改后的 闹钟B 回调函数,如下图:

再次编译、烧录!
串口助手输出调试信息,如下:

现在好了,printf 按设想正常输出了。谨记,唤醒后,先配置系统时钟,再执行其它重要操作。
5、还有一个常见问题
现象 :虽然中断回调内的 printf 能够正常输出内容,但输出字符串前出现了几个不必要的空格(细看上图,在 "闹钟B 中断触发!!" 之前)。
原因分析 :该现象是由于系统时钟恢复与串口外设重新同步的时序差异造成的。当 SystemClock_Config() 在中断回调开头执行时,串口波特率发生器从默认的 HSI(8 MHz)时钟切换到 HSE(168 MHz)时钟,这个过程可能引起发送移位寄存器状态短暂不稳定,导致第一个字符的起始位被误识别。此外,若进入 Stop 模式前串口发送缓冲区有残留数据,唤醒后这些数据可能会以无效字符形式输出,表现为空格或乱码。
优化建议 :若希望彻底消除该现象,避免出现不可控情况,可采用 "中断仅置标志,在主循环恢复时钟并执行任务" 的模式,确保所有外设操作(包括串口输出)都在时钟稳定、外设状态完全确定的环境下进行,从而获得干净的调试信息。
文毕,欢迎指正、交流。~~