【STM32 + CubeMX】低功耗 -- Stop 停止模式

STM32 低功耗共有三种模式:

Sleep 睡眠模式 、Stop 停止模式、Standby 待机模式。

三种模式的 操作图解 地址:

本文将重点拆解Stop 停止模式 的代码实现、关键事项 。

目录

[一、 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 */ 区域声明:
cpp 复制代码
uint8_t wakeUpFlag = 0;   // 唤醒标志; 0-无任务、1-已唤醒,按需执行任务

2. 进入 Stop 模式

进入Stop 模式的核心函数是 HAL_PWR_EnterSTOPMode ()。

我们编写一个函数:把进入 Stop 前的准备工作、唤醒后的时钟恢复工作,整合在一起 。

关键点提醒:

  1. 进入前:需要关闭可能频繁唤醒的中断,如SysTick。
  2. 唤醒后:必须调用 SystemClock_Config() 重新配置系统时钟。否则,程序将运行在 HSI 内部高速时钟。
  3. 唤醒后:恢复中断源(如 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、问题根源

有调试经验的老司机,毒眼可以一下子看穿,并已串联成排查思路了。

  1. 这些空白占位,其实是有数据的,只是波特率不对,导致数据错误,并非是可正常显示的编码。
  2. 波特率不对,一般就是 系统时钟配置 错误!
  3. 进入Stop前显示正常,结束后也显示正常,唯独 闹钟B 中断回调函数内 printf 异常!
  4. 那么,bug的定位,就在 进入Stop 后 与 闹钟B回调函数的 第一行printf之间!!

是的,问题就在闹钟B回调函数的 printf之前 !!

回顾我们的 EnterStopMode() 函数:

上图能清晰地看到流程上的问题所在:

虽然我们在唤醒后,重新配置了系统时钟,但位置没安排妥当,导致中断回调函数里的运行,是内部低速时钟HSI时钟,8M,并非 168MHz。

4、正确的操作 (修正问题)

在唤醒的第一时间 (中断回调函数开头,先配置系统时钟,再执行其它操作!!

具体操作:

  • 删除 EnterStopMode() 函数里的 系统配置 函数
  • 中断回调函数里,第一行,即唤醒后执行的第一行,配置系统时钟!完整函数如下:
cpp 复制代码
void 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 模式前串口发送缓冲区有残留数据,唤醒后这些数据可能会以无效字符形式输出,表现为空格或乱码。

优化建议 :若希望彻底消除该现象,避免出现不可控情况,可采用 "中断仅置标志,在主循环恢复时钟并执行任务" 的模式,确保所有外设操作(包括串口输出)都在时钟稳定、外设状态完全确定的环境下进行,从而获得干净的调试信息。


文毕,欢迎指正、交流。~~

相关推荐
jghhh012 小时前
51单片机控制42步进电机程序
单片机·嵌入式硬件·51单片机
没有余地 EliasJie2 小时前
FFmpeg介绍与ESP32资源受限下的视频流传输优化策略
单片机·物联网·ffmpeg
zmj3203242 小时前
PLC与单片机(微控制器MCU)、传统继电器控制系统
单片机·嵌入式硬件·plc
LCG元3 小时前
STM32嵌入式开发:基于PID算法的直流电机闭环调速控制
stm32·嵌入式硬件·算法
AnalogElectronic3 小时前
ESP-01S 和树莓派pico,普中51单片机开发板,综合比较
单片机·嵌入式硬件·51单片机
济6173 小时前
STM32SPI实验:外部Flash掉电记忆实验---STM32 HAL库专栏
stm32·嵌入式·stm32hal库编程
我在人间贩卖青春3 小时前
U(S)ART 串口应用
单片机·串口·uart·usart
Shang180989357266 小时前
SSD202D星宸科技SigmaStar一颗高度集成的嵌入式智能触控显示板解决方案SSD202集成了硬件H.264/H.265视频解码器、内置了DDR
科技·嵌入式硬件·fpga开发·ssd202d嵌入式智能显示
’长谷深风‘12 小时前
51单片机入门(4温度采集:DS18B20)
单片机·嵌入式硬件·51单片机·ds18b20·温度采集