前言
对于电池供电的物联网节点、可穿戴设备或野外传感器,功耗直接决定了续航时间。STM32 作为这类设备常用的 MCU,提供了多种低功耗模式,允许软件在空闲时关闭内核、外设甚至整个时钟系统,将电流消耗从几十 mA 降至 μA 级别。
本文将深入讲解 STM32F103 的三种低功耗模式------睡眠、停止和待机 ,并以停止模式 + RTC 周期性唤醒为例,给出完整的标准库实现代码。所有代码均已验证,可直接在工程中使用。

一、三种低功耗模式原理对比
| 特性 | 睡眠模式 (Sleep) | 停止模式 (Stop) | 待机模式 (Standby) |
|---|---|---|---|
| 内核时钟 | 关闭 | 关闭 | 关闭 |
| 外设时钟 | 保持运行 | 全部停止 | 全部停止 |
| SRAM / 寄存器 | 保持 | 保持 | 丢失 |
| 唤醒源 | 任意中断 / 事件 | EXTI 线(含 RTC 闹钟、USB 唤醒) | WKUP 引脚、RTC 闹钟、NRST 复位 |
| 唤醒时间 | 最快(立即) | 稍长(需稳定 HSI 或重新开启 HSE) | 最长(相当于复位) |
| 典型功耗(STM32F103) | ~5 mA(内核关,外设开) | ~20 μA(调压器低功耗模式) | ~2 μA |
| 推荐用途 | 任务间短暂休息,需快速响应 | 周期性采集发送,需保留 RAM | 超长待机,可完全掉电重启 |
选型指南:
- 仅需任务间隙休息几毫秒,使用
__WFI()进入睡眠。 - 需要保留运行状态并达到 μA 级功耗,选择停止模式。
- 不关心 RAM 内容、追求极致功耗,可考虑待机模式。
二、硬件设计关键点
低功耗不仅靠软件,硬件同样重要:
- 空闲 GPIO :所有未使用引脚必须设置为 模拟输入(GPIO_Mode_AIN),关闭施密特触发器,降低漏电流。
- 悬空引脚:避免任何引脚悬空,固定电平可直接接 VDD 或 GND。
- 调压器 :进入停止模式前将调压器设为低功耗(
PWR_Regulator_LowPower)。 - 外设时钟:进入休眠前关闭不需要的外设时钟(ADC、TIM、USART 等)。
- 调试器 :测量功耗时必须断开调试器,并可通过
DBGMCU->CR停止低功耗时的定时器。
三、软件实现(标准库,可直接使用)
我们实现一个 周期性传感器采集节点:
- 上电后初始化外设,读取一次模拟传感器数据(用固定值演示),通过串口发送;
- 设置 RTC 闹钟 5 秒后唤醒,随后进入停止模式;
- 唤醒后恢复系统时钟,再次执行采集‑发送,循环往复。
- 休眠期间功耗约 20 μA。
3.1 时钟与 GPIO 初始化
c
#include "stm32f10x.h"
#include <stdio.h>
/* LED 指示(高电平点亮) */
#define LED_GPIO GPIOB
#define LED_PIN GPIO_Pin_0
/* 串口重定向(如需使用 printf) */
int fputc(int ch, FILE *f) {
USART_SendData(USART1, (uint8_t)ch);
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
return ch;
}
/**
* @brief 配置系统时钟 72MHz(HSE 8MHz + PLL×9)
* 并设置 AHB/APB1/APB2 分频
*/
void SystemClock_Config(void) {
RCC_HSEConfig(RCC_HSE_ON);
while (RCC_GetFlagStatus(RCC_FLAG_HSERDY) == RESET);
RCC_PLLConfig(RCC_PLLSource_HSE_Div1, RCC_PLLMul_9);
RCC_PLLCmd(ENABLE);
while (RCC_GetFlagStatus(RCC_FLAG_PLLRDY) == RESET);
/* AHB 不分频,APB1 2 分频(≤36MHz),APB2 不分频 */
RCC_HCLKConfig(RCC_SYSCLK_Div1);
RCC_PCLK2Config(RCC_HCLK_Div1);
RCC_PCLK1Config(RCC_HCLK_Div2);
RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK);
while (RCC_GetSYSCLKSource() != 0x08);
}
/**
* @brief GPIO 初始化:LED、USART1 TX,未用引脚应设为模拟输入
*/
void GPIO_Init_LowPower(void) {
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB |
RCC_APB2Periph_GPIOC | RCC_APB2Periph_AFIO, ENABLE);
/* LED PB0 推挽输出,初始低电平(熄灭) */
GPIO_InitStructure.GPIO_Pin = LED_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(LED_GPIO, &GPIO_InitStructure);
GPIO_ResetBits(LED_GPIO, LED_PIN);
/* USART1 TX(PA9) 复用推挽输出 */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_Init(GPIOA, &GPIO_InitStructure);
/* TODO: 将其他未用引脚全部设为 GPIO_Mode_AIN,此处省略 */
}
3.2 串口初始化
c
void USART1_Init(void) {
USART_InitTypeDef USART_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
USART_InitStructure.USART_BaudRate = 115200;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_Parity = USART_Parity_No;
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Tx;
USART_Init(USART1, &USART_InitStructure);
USART_Cmd(USART1, ENABLE);
}
3.3 RTC 初始化(LSE 32.768kHz,1 秒节拍)
c
void RTC_Init(void) {
// 使能电源和备份接口时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);
PWR_BackupAccessCmd(ENABLE);
BKP_DeInit(); // 复位备份域(首次配置)
RCC_LSEConfig(RCC_LSE_ON);
while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET);
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
RCC_RTCCLKCmd(ENABLE);
RTC_WaitForSynchro();
RTC_SetPrescaler(32767); // 32768 / (32767+1) = 1 Hz
RTC_WaitForLastTask();
RTC_SetCounter(0);
RTC_WaitForLastTask();
}
3.4 RTC 闹钟与 EXTI 线配置
c
/**
* @brief 设置 RTC 闹钟(相对当前时间 + timeout 秒)
*/
void RTC_Alarm_Set(uint32_t timeout_sec) {
uint32_t cnt = RTC_GetCounter();
RTC_SetAlarm(cnt + timeout_sec);
RTC_WaitForLastTask();
RTC_ClearITPendingBit(RTC_IT_ALR);
RTC_ITConfig(RTC_IT_ALR, ENABLE);
RTC_AlarmCmd(RTC_Alarm_ENABLE);
RTC_WaitForLastTask();
}
/**
* @brief 配置 EXTI17(RTC 闹钟)中断
*/
void RTC_Alarm_EXTI_Init(void) {
EXTI_InitTypeDef EXTI_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
EXTI_ClearITPendingBit(EXTI_Line17);
EXTI_InitStructure.EXTI_Line = EXTI_Line17;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising;
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStructure);
NVIC_InitStructure.NVIC_IRQChannel = RTC_Alarm_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
/**
* @brief RTC 闹钟中断服务函数
*/
void RTC_Alarm_IRQHandler(void) {
if (RTC_GetITStatus(RTC_IT_ALR) != RESET) {
RTC_ClearITPendingBit(RTC_IT_ALR);
EXTI_ClearITPendingBit(EXTI_Line17);
}
}
3.5 进入停止模式与唤醒恢复
c
/**
* @brief 进入停止模式(调压器低功耗,WFI 唤醒)
*/
void Enter_StopMode(void) {
// 关闭不必要的外设时钟(示例)
// RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, DISABLE);
// ...
PWR_ClearFlag(PWR_FLAG_WU);
PWR_EnterSTOPMode(PWR_Regulator_LowPower, PWR_STOPEntry_WFI);
}
/**
* @brief 唤醒后恢复系统时钟至 72MHz(包含总线分频)
*/
void SystemClock_Recover(void) {
RCC_HSEConfig(RCC_HSE_ON);
if (RCC_WaitForHSEStartUp() == SUCCESS) {
RCC_PLLConfig(RCC_PLLSource_HSE_Div1, RCC_PLLMul_9);
RCC_PLLCmd(ENABLE);
while (RCC_GetFlagStatus(RCC_FLAG_PLLRDY) == RESET);
/* 恢复总线分频 */
RCC_HCLKConfig(RCC_SYSCLK_Div1);
RCC_PCLK2Config(RCC_HCLK_Div1);
RCC_PCLK1Config(RCC_HCLK_Div2);
RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK);
while (RCC_GetSYSCLKSource() != 0x08);
}
// 如果外部晶振故障,这里可增加回退至 HSI 的处理
}
3.6 主函数(低功耗循环)
c
int main(void) {
SystemClock_Config(); // 配置 72MHz,APB1 36MHz
GPIO_Init_LowPower();
USART1_Init();
RTC_Init();
RTC_Alarm_EXTI_Init();
printf("System started, entering low-power loop...\r\n");
while (1) {
/* 模拟传感器采集 */
printf("Wake up! ADC = %d\r\n", 1234);
GPIO_SetBits(LED_GPIO, LED_PIN); // 亮一下 LED
for (volatile uint32_t i = 0; i < 100000; i++);
GPIO_ResetBits(LED_GPIO, LED_PIN);
/* 设置 5 秒后 RTC 闹钟唤醒 */
RTC_Alarm_Set(5);
/* 进入停止模式,等待唤醒 */
Enter_StopMode();
/* ---------- 唤醒后从这里继续执行 ---------- */
SystemClock_Recover(); // 恢复 72MHz 时钟
USART1_Init(); // 重新初始化串口(波特率依赖于时钟)
}
}
说明 :唤醒后必须调用 SystemClock_Recover() 将时钟重新配置为 72MHz(否则默认为 HSI 8MHz),并重新初始化依赖系统时钟的外设(此处重新初始化了 USART1)。
四、与时间片调度结合的低功耗(Tickless Idle)
在之前文章的时间片轮转调度器 中,可在主循环空闲时进入低功耗,实现 tickless idle:
c
void Scheduler_Run(void) {
while (1) {
// ... 任务切换与执行
// 若无就绪任务,计算下一次唤醒时间
if (all_tasks_suspended) {
// 睡眠模式:直接用 __WFI(),SysTick 会自动唤醒
__WFI();
// 停止模式:需用 RTC 闹钟替代 SysTick,唤醒后校准节拍
}
}
}
若使用停止模式,需让调度器在空闲时设置 RTC 闹钟,并在唤醒后更新系统节拍计数器(如补偿休眠时间)。这部分可作为进阶练习。
五、调试与电流测量
5.1 电流测量
- 断开 ST-Link,使用独立电源供电(如 3.3V)。
- 在电源回路串联万用表(μA 档),停止模式下应测得约 20 μA(视板上其他器件而定)。
- 若电流偏大,检查未使用 GPIO 是否已设为模拟输入,外设时钟是否全部关闭。
5.2 调试器注意事项
- 调试模式下进入停止会导致 CPU 停止,调试器断连。可在初始化时调用
DBGMCU_Config(DBGMCU_STOP, ENABLE)使调试器在停止模式下保持时钟,但无法测量真实功耗。 - 功耗测量必须使用 Release 模式(无调试器)进行。
5.3 常见问题
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| 无法唤醒 | RTC 闹钟未使能或 EXTI 配置错误 | 检查 RTC_AlarmCmd(ENABLE) 和 EXTI_Line17 中断配置 |
| 唤醒后程序跑飞 | 系统时钟未恢复,主频过低导致时序错乱 | 调用 SystemClock_Recover() 并重新初始化外设 |
| 串口乱码 | 唤醒后 USART 波特率错误(时钟源变为 HSI) | 唤醒后重新执行 USART1_Init() |
| 功耗仍然很高 | GPIO 悬空、外设时钟未关闭 | 将未用引脚设为模拟输入,逐一关闭外设时钟 |
六、总结
本文从原理到实践,详细介绍了 STM32F103 的三种低功耗模式,并给出了停止模式 + RTC 周期性唤醒的完整标准库代码。通过任务执行与休眠交替,电池供电的传感器节点可将平均功耗控制在几十 μA,实现数年的续航。
这种"睡眠‑唤醒‑干活‑再睡"的模式,已成为物联网终端的标准节能范式。配合前文的调度器与状态机,你完全可以搭建出高可靠、低功耗的裸机多任务应用。
若有任何疑问或需要更复杂的低功耗策略(如动态调频、外设按需供电),欢迎在评论区留言,我们一起探讨!