STM32低功耗休眠详解——睡眠、停止与待机模式实战,综合应用(三)

前言

对于电池供电的物联网节点、可穿戴设备或野外传感器,功耗直接决定了续航时间。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 内容、追求极致功耗,可考虑待机模式。

二、硬件设计关键点

低功耗不仅靠软件,硬件同样重要:

  1. 空闲 GPIO :所有未使用引脚必须设置为 模拟输入(GPIO_Mode_AIN),关闭施密特触发器,降低漏电流。
  2. 悬空引脚:避免任何引脚悬空,固定电平可直接接 VDD 或 GND。
  3. 调压器 :进入停止模式前将调压器设为低功耗(PWR_Regulator_LowPower)。
  4. 外设时钟:进入休眠前关闭不需要的外设时钟(ADC、TIM、USART 等)。
  5. 调试器 :测量功耗时必须断开调试器,并可通过 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,实现数年的续航。

这种"睡眠‑唤醒‑干活‑再睡"的模式,已成为物联网终端的标准节能范式。配合前文的调度器与状态机,你完全可以搭建出高可靠、低功耗的裸机多任务应用。

若有任何疑问或需要更复杂的低功耗策略(如动态调频、外设按需供电),欢迎在评论区留言,我们一起探讨!

相关推荐
嵌入式小站10 小时前
STM32 零基础可移植教程 06:外部中断按键,不用一直在 while 里盯着它
stm32·单片机·嵌入式硬件
大卡片10 小时前
GPIO控制器原理
单片机·嵌入式硬件
余生皆假期-10 小时前
J-link Commander 命令操作 MCU 连接、调试、烧录、擦除等
单片机·嵌入式硬件
lingzhilab10 小时前
零知派ESP32——ULN2003AN驱动28BYJ-48步进电机控制系统
单片机·嵌入式硬件
╰ㄣ浮华若梦︶ _11 小时前
51单片机的SPI协议
单片机·嵌入式硬件·51单片机·8051·spi协议
NPE~11 小时前
[嵌入式]嵌入式在线仿真平台 —— Wokwi 入门指南
stm32·嵌入式·esp32·教程·平台
崇山峻岭之间11 小时前
单片机按键实验
单片机·嵌入式硬件
踏着七彩祥云的小丑11 小时前
嵌入式测试学习第 16 天:复位电路、电源电路基础原理
单片机·嵌入式硬件
小手智联老徐12 小时前
Arduino IDE环境搭建与点亮ESP32 D1板载LED
嵌入式硬件·esp32·arduino