1、为什么要低功耗管理
在一款成熟的产品中,低功耗管理是一个非常基础且核心的功能。
对于一直保持电源供应的设备,即使没有做低功耗管理,用户也不会有非常明显直观的感受,但是依靠电池供电的设备,特别是需要保持轻量外形设计的产品,能负载电池非常的微小。这个时候,低功耗管理就是缺一不可的,否则就会骂声一片。试想一下,你买了一款智能手表,因为产品的开发者没有做任何的低功耗处理,导致用了半天就要对手表充一次电,那这样的产品,是否会受到大众的欢迎和支持呢?
因此,一款成熟的电子产品,配备低功耗管理的功能重要性不言而喻,特别是偏向于物联网和电池供电的产品,延长设备的工作时间,减少频繁充电和更换电池。
现今,大部分主流的MCU、MPU都是内置了电源管理控制器,可以帮助我们快速实现低功耗操作,接下来主要以STM32F4为例,梳理总结ST的低功耗实现。
2、STM32低功耗的三大模式
(1)、基础铺垫
STM32使用过程中,按功耗的低到高,可以归类为4种工作模式,正常模式、睡眠模式、停止模式和待机模式。在默认情况下,在系统复位或上电复位后,微控制器进入到正常运行模式。在运行模式下,CPU通过HCLK提供时钟,并执行用户程序代码。
在STM32中,系统提供了3种主要的低功耗模式,分别是睡眠模式、停止模式和待机模式。在不同模式下,启动时间和唤醒方式是不同的,开发者需要根据实际的需要选择配置具体的低功耗模式,以达到一个最优的需求和效果匹配。
当然,除了上面提到的三种模式外,也可以另辟蹊径的通过降低STM32的系统时钟主频和将不使用的外设时钟进行关闭,这样也可以实现一个降设备功耗,省电的效果。
如下图所示为STM32F4xx中文参考手册中的关于低功耗模式汇总图。

(2)、睡眠模式--Sleep
在睡眠模式下,关闭了内核时钟,也就是CPU停止运行,外设保持运行,唤醒源为所有的中断(包括外部中断和内部中断)。
在正常运行情况下,有两种方式进入睡眠模式,分别是执行WFI(Wait For Interrupt)或者WFE(Wait for Event)指令时,即可进入睡眠模式。

在STM32中,通过对系统寄存器中的SLEEPONEXIT位的设置,可以实现立即休眠和退出时休眠两种方案。
进入睡眠模式:
如果SLEEPONEXIT位清0,MCU将在执行WFI或WFE指令时,立即进入休眠模式。
如果SLEEPONEXIT位置1,MCU将在退出优先级最低的ISR时,立即进入睡眠模式。
退出睡眠模式:
如果使用 WFI 指令进入睡眠模式,则嵌套向量中断控制器 (NVIC) 确认的任意外设中断都会将器件从睡眠模式唤醒。
如果使用 WFE 指令进入睡眠模式,MCU 将在有事件发生时立即退出睡眠模式。
唤醒后的动作:
如果是由中断唤醒的,会先进入到中断,退出中断服务程序后,然后接着执行WFI执行后的程序。
如是由事件唤醒,就会直接接着执行WFE后面的程序。

由于没有在进入/退出中断时浪费时间,此模式下的唤醒时间最短。
(3)、停止模式--Stop
停止模式也就是深度睡眠模式。简单来说,在Stop模式下,相较于前面的睡眠模式,它关闭了更多的资源。CPU停止运行,并且关闭了时钟从而导致外设也都停止运行了,只有内部SRAM和原来的寄存器可以保持原来的值。
它的唤醒源为外部中断。也就是说通过定时器、串口中断这样的已经无法唤醒了,只能通过类似按个按键给一个上升沿高电平信号这样的外部中断才能唤醒。
因为还有一些资源没有被关闭掉,所以在停止模式中被唤醒时,系统还可以继续执行上次停止的地方继续执行代码。
但是停止模式关闭了一些影响系统正常工作的资源,因此会有唤醒延迟。其唤醒后具体执行内容,和上述的睡眠模式一致。
如果在执行FLASH编程,停止模式的进入会延迟到存储器访问结束后执行;如果正在访问APB域,停止模式的进入会延迟到APB访问结束才开始。
需要注意的是,在停止模式下,ADC和DAC也会产生功耗,除非在进入停止模式前,将其禁止了。
进入停止模式主要是将内核寄存器的SLEEPDEEP = 1,PWR_CR寄存器中的 PDDS = 0,然后调用WFI或WFE指令即可进入停止模式;通过PWR_CR寄存器的LPDS位也可以设置调压器工作在什么模式,当LPDS = 0,工作在正常模式,当LPDS = 1时,工作在低功耗模式。

(4)、待机模式--Standby
在待机模式(Standby)下,可使设备的功耗达到最低。
在此时,CPU停、外设停、SRAM和寄存器停(相当于整个系统都关机了)。所以设备中的SRAM和寄存器内容将会丢失,只有备份区域和待机电路中的寄存器维持供电。
进入待机模式,需要将内核寄存器的SLEEPDEEP = 1,PWR_CR寄存器中的 PDDS = 1,PWR_CR寄存器中的唤醒位WUF = 0,然后再调用一下WFI或WFE即可进入待机模式了。
在待机模式下,主要有4中唤醒模式,分别是:WKUP引脚上升沿(PA0),RTC闹钟事件、IWDG独立看门狗和NRST引脚。
在待机模式下,大部分的I/O引脚都是出于高阻态的,除了复位引脚、RTC_AF1引脚、WKUP引脚(使能后)。
在待机模式被唤醒后,系统中没有之前代码运行的记录信息,需要对芯片复位,重新加载运行程序。
因为待机模式唤醒会将芯片复位,因此其唤醒到正常工作延时是最大的。

3、低功耗编程应用示例
(1)、睡眠模式
睡眠模式的配置,直接调用**__WFI()或者__WFE()**指令函数即可直接接入。
但是需要注意,如果是HAL库进行配置,会有一个系统定时器,也要把它的中断关掉,不然就会出现调用了进入睡眠模式指令函数,但是没什么用,因为系统定时器1ms一次中断。
cpp
#include "stm32f4xx.h"
#include "led.h"
#include "USART.h"
#include "KEY.h"
#include "stdio.h"
#include "stdlib.h"
#include "delay.h"
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
USART1_Init(115200);
printf("\r\n---------------Sleep Mode test Starting---------------\r\n");
LED_Init();
Key_Init();
LED_RED_ON;
LED_BLUE_OFF;
//如果是HAL,会有一个系统定时器,也要把中断关掉,不然就会出现配置了中断,但是没什么用,因为系统定时器1ms一次中断
//SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk;
delay_ms(1000);
while(1)
{
LED_RED_OFF;
LED_BLUE_ON;
delay_ms(1000);
printf("Enter Sleep Mode\r\n");
__WFI();//注意是调用这个函数!!!不能没有括号的,否则编译器会忽略掉
printf("Exit Sleep Mode\r\n");
LED_BLUE_OFF;
LED_RED_ON;
delay_ms(1000);
}
}
通过上面的代码实现的效果就是,初始化完毕后,进入到主循环中,执行__WFI()后,就进入了睡眠模式,因为程序中配置了串口中断和按键中断,因此,可以通过串口工具发送数据唤醒,也可以通过按下按键唤醒。

(2)、停止模式
配置Stop低功耗模式时,需要开启电源管理时钟并且使用PWR_EnterSTOPMode进入停止模式。同时需要注意,唤醒后时钟频率会发生变化,需要进一步恢复,可以直接使用SystemInit函数,也可以自己手动配置,在这里就是一个笔记,演示记录一下,所以具体更进一步的配置就不展示了。
cpp
#include "stm32f4xx.h"
#include "stm32f4xx_pwr.h"
#include "led.h"
#include "USART.h"
#include "KEY.h"
#include "stdio.h"
#include "stdlib.h"
#include "delay.h"
void enter_stop_mode(void);
void resume_sys_clk(void);
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
USART1_Init(115200);
printf("\r\n---------------Sleep Mode test Starting---------------\r\n");
LED_Init();
Key_Init();
LED_RED_ON;
LED_BLUE_OFF;
delay_ms(1000);
while(1)
{
LED_RED_OFF;
LED_BLUE_ON;
delay_ms(1000);
printf("\r\nEnter Sleep Mode\r\n");
// 进入停止模式前确保唤醒中断使能
// 唤醒后执行,此时已退出停止模式
enter_stop_mode();
resume_sys_clk();
delay_ms(200);
printf("\r\nExit Sleep Mode\r\n");
LED_BLUE_OFF;
LED_RED_ON;
delay_ms(1000);
}
}
//任意EXTI线唤醒
void enter_stop_mode(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE); //开电源管理时钟
//PWR_EnterSTOPMode(PWR_LowPowerRegulator_ON, PWR_STOPEntry_WFI);
PWR_EnterSTOPMode(PWR_Regulator_ON, PWR_STOPEntry_WFI); //进入停机模式
}
void resume_sys_clk(void)
{
SystemInit();
}
在上述的程序代码中,按键配置了EXIT中断,因此在按下按键后,可以唤醒设备,但是串口使用的是串口中断,未配置EXIT中断,因此,通过上位机向设备发送数据时,无法唤醒设备。

(3)、待机模式
说到这个待机模式,不得不吐槽一下,和开发板可能也有很大的关系,一块可以正常进行唤醒,一块直接就是睡过去了,完全无法唤醒...,拿了一个板子,调来调去,都没解决,换个板子就好了。。。目前还没找到具体的原因,后面找到了说明一下为什么会这样。
下面这个代码是可以正常跑通,并且通过K_UP键进行唤醒的。
在待机模式下,StandBy模式后面的代码是永远不会执行的,每次都是和复位了CPU,重新开始执行程序代码。
通过PWR_GetFlagStatus函数,可以查询出来是否standby模式下唤醒启动的。
在进入待机模式前,最好将能关闭的外设全部关闭了,因为外设耗电量,可能比CPU还耗电。
并且RTC的中断也要关了,不然可能影响正常进入待机模式。
cpp
#include "stm32f4xx.h"
#include <stdio.h>
#include <string.h>
#include "sys.h"
#include "usart.h"
#include "delay.h"
#include "led.h"
void enter_standby_mode(void);
void EXTI0_IRQHandler(void);
void WKUP_Pin_Init(void);
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
USART1_Init(115200);
LED_Init();
WKUP_Pin_Init();
printf("\r\n---------------Standby Mode test Starting---------------\r\n");
if(PWR_GetFlagStatus(PWR_FLAG_WU) == SET)
{
printf("Standy Mode WakeUP\r\n");
}
LED2 = !LED2;
delay_ms(1000);
LED2 = !LED2;
delay_ms(1000);
enter_standby_mode();
//WKUP后面的代码永远执行不了
while(1)
{
LED2=!LED2;
delay_ms(250);//延时250ms
}
}
void enter_standby_mode(void)
{
RCC_AHB1PeriphResetCmd(0X04FF,ENABLE);//复位所有IO口
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);//使能PWR时钟
PWR_BackupAccessCmd(ENABLE);//后备区域访问使能
//关闭相关RTC中断,不关闭可能导致standby模式进不去
RTC_ITConfig(RTC_IT_TS|RTC_IT_WUT|RTC_IT_ALRB|RTC_IT_ALRA,DISABLE);
RTC_ClearITPendingBit(RTC_IT_TS|RTC_IT_WUT|RTC_IT_ALRB|RTC_IT_ALRA);
PWR_ClearFlag(PWR_FLAG_WU);//清除Wake-up标志
PWR_WakeUpPinCmd(ENABLE);//设置WKUP用于唤醒
PWR_EnterSTANDBYMode(); //进入待机模式
}
//中断处理函数,在PA0上升沿时处理,清空中断标志位
void EXTI0_IRQHandler(void)
{
EXTI_ClearITPendingBit(EXTI_Line0); // 清除LINE10上的中断标志位
}
//PA0 引脚初始化,并配置外部中断,上升沿触发
void WKUP_Pin_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
EXTI_InitTypeDef EXTI_InitStructure;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; //PA0
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN;//输入模式
GPIO_InitStructure.GPIO_OType = GPIO_OType_OD;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_DOWN;//下拉
GPIO_Init(GPIOA, &GPIO_InitStructure);
SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOA, EXTI_PinSource0);//PA0 连接到中断线0
EXTI_InitStructure.EXTI_Line = EXTI_Line0;
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 = EXTI0_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x02;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
下面是上述程序运行出来的结果,当在Standby待机模式下,通过WKUP按键唤醒后,会打印提示信息。
