第12章 PWR电源控制
12.1 PWR电源控制
12.1.1 PWR简介
芯片在3种低功耗模式下,是没法直接再下载程序的。这是因为芯片在睡眠,不会关注调试端口了。解决办法就是:1.按住复位键不动;2.点下载按钮;3.及时从开复位键。这样就能下载成功了。如果不小心禁用了调试端口,其实也可以这样来解决。
PWR(Power Control)电源控制;
PWR负责管理STM32内部的电源供电部分,可以实现可编程电压监测器和低功耗模式的功能;
可编程电压监测器(PVD)可以监控VDD电源电压,当VDD下降到PVD阀值以下或上升到PVD阀值之上时,PVD会触发中断,用于执行紧急关闭任务;
这个功能预想二点场景是使用电池供电,或者对安全要求比较高的设备,如果供电电压在逐渐下降,在电压过低的情况下,可能会导致内部或者外部电路发生不确定的错误。为了避免不确定的因素,在电源电压低于设定的阈值时,我们可以主动出击,提前发出警告,并且关闭比较危险的设备。
低功耗模式包括睡眠模式(Sleep)、停机模式(Stop)和待机模式(Standby),可在系统空闲时,降低STM32的功耗,延长设备使用时间。
正常运行的状态下,程序永远都不会停下来,所以主程序的最后,一般都是个死循环,即使需要空闲,让程序停下来,也得来个空循环让程序一直转圈卡住,但是程序运行就会耗电,空循环的耗电量是很大的,比如遥控器,如果不用它的时候,程序会一直空循环,那么用不了几天,电池就没电了。所以说对于这些设备,需要低功耗模式,在空闲状态时,关闭不必要的硬件,比如直接把CPU断电或者关闭时钟,这样程序就不会运行了,但是在低功耗模式下,我们也需要保留必要的唤醒电路,比如串口接收数据的中断唤醒、外部中断唤醒、RTC闹钟唤醒等等,在需要设备工作时,STM32能够立刻重新投入工作,这样才行。所以低功耗模式,我们要考虑管壁哪些硬件、保留哪些硬件,以及如何去唤醒,当然关闭越多的硬件,设备约省电,唤醒就越麻烦。
12.1.2 电源框图
整体上看,这个图可以分为3个部分,最上面是模拟部分供电,叫VDDA(VDD Analog);中间是数字部分供电,包括两块区域,VDD供电区域和1.8V供电区域;下面是后备供电,叫做VBAT(V Battery)。
VDDA供电区域主要负责模拟部分的供电,其中包括AD转换器、温度传感器、复位模块、PLL锁相环。这些电路的供电正极是VDDA,负极是VSSA。其中AD转换器,还有两根参考电压的供电脚,叫做VREF+和VREF-,这两个脚在引脚多的型号里会单独引出来,在引脚上的型号,比如我们这个C8T6,VREF+和VREF-在内部已经分别接到了VDDA和VSSA了。
中间部分供电由两部分组成,左边部分是VDD供电区域,其中包括IO电路、待机电路(唤醒逻辑和独立看门狗),右边部分是VDD通过电压调节器,降压到1.8V,提供给后面这一块的1.8V供电区域。1.8V供电区域包括CPU核心、存储器和内置数字外设。可以看出,STM32内部的大部分关键电路,CPU、存储器和外设,其实都是以1.8V的低电压运行的。当这些外设需要与外界进行交流时,才会通过IO电路转换到3.3V。所以我们从外部看好像STM32内部全是3.3V。实际上,它内部的CPU、外设等,都是以1.8V供电运行,使用低电压运行的最主要目的是降低功耗。电压越低,内部电路运行的功耗就相对越低。
VBAT和后备供电区域:RCC BDCR是RCC的寄存器,叫备份域控制寄存器。也是和后备域有关的寄存器,所以也可以由VBAT供电。低电压检测器可以控制开关,VDD有电啥时,由VDD供电,VDD没电时,由VBAT供电。
12.1.3 上电复位和掉电复位
当VDD或者VDDA电压过低时,内部电路直接产生复位,让STM32复位住,不要乱操作。 这个复位和不复位的界限之间,设置了一个40mV的迟滞电压,大于上限POR(Power On Reset)时解除复位。小于下辖PDR(Power Down Reset)时复位。这是一个典型的迟滞比较器,设置两个阈值的作用,就是防止电压在某个阈值附近波动时,造成输出也来回抖动。下面的复位信号Reset是低电平有效的,所以在前面和后面,电压过低时,是复位的,中间电压不正常的时候,不复位。那这个电压上限和下限具体是多少值呢?还有接触复位的滞后时间,是多久呢?
12.1.4 可编程电压监测器
PVD触发滞后,芯片还是能正常工作的, 只不过是电源电压过低,该提醒一下用户了。下面那的PVD输出,这个是正逻辑,电压过低时为1,电压正常时为0,这个信号可以去申请中断,在上升沿或者下降沿时,触发中断,以此提醒程序进行适当的处理。PVD的中断申请是通过外部中断实现的,如下图所示,所以要配置PVD的话,记得要配置外部中断。
RTC的闹钟信号也有借道外部中断, 其实RTC自己是有中断的,因为低功耗模式设计的是,只有外部中断可以唤醒停止模式。其它这些设备,也想唤醒停止模式的话,就可以通过借道外部中断来实现。USB和ETH也都有它们的WeakUp唤醒信号接过来了,目的也是为了唤醒停止模式。
12.1.5 低功耗模式
低功耗模式有三种,这三种模式从上到下关闭的电路越来越多,从上到下越来越省电,同时,从上到下,越来越难唤醒。
睡眠模式:这是浅睡眠,就相当于打了个盹。直接调用WFI或者WFE即可进入,这两个东西是内核的指令,对应库函数里,有对应的函数,直接调用函数即可。
其中WFI(Wait For Interrupt)、等待中断,意思就是我先睡了,如果有中断发生的话,再叫我起来,所以对应的唤醒条件是任一中断,调用WFI进入的睡眠模式,任何外设发生任何中断时,芯片都会立刻醒来,因为中断发生了,所以醒来之后的第一件事,一般就是处理中断函数。下面WFE(Wait For Event)等待事件,唤醒事件可以唤醒,对应的唤醒条件是唤醒事件,这个事件可以是外部中断配置为事件模式,也可以是使能了中断,但是没有配置NVIC。调用WFE进入的睡眠模式,产生唤醒事件时,会立刻醒来。醒来之后一般不需要进行中断函数,直接从睡得地方继续进行。
停机模式:如何进入停机模式,首先SLEEPDEEP位设置为1,告诉CPU,你可以放心的睡,进入深度睡眠模式,PDDS这一位用来区分它是停机模式还是待机模式,PDDS=0进入停机模式,PDDS=1进入待机模式。所以想要进入停机模式,PDDS要先设置为0,之后LPDS用来设置最后的电压调节器,是开启还是进入低功耗模式,LPDS=0,电压调节器开启,LPDS=1,电压调节器进入低功耗,最后,当我们把这些位提前设置好了,最后再调用WFI或者WFE,芯片就可以进入停止模式了。这个模式下,芯片睡的更深,关的东西更多,唤醒条件更苛刻一些,是任一外部中断。
待机模式:和停机模式差不多,首先SLEEPDEEP也是置1,即将深度睡眠,然后PDDS置1,表示即将进入待机模式,最后调用WFI或WFE就可以进入待机模式了。1.8V的电源关闭,内部的存储器和寄存器的数据全部丢失,但是和停止模式一样,并不会主动关闭LSI和LSE这两个低速时钟,因为这两个时钟还要维持RTC和独立看门狗的运行,所以不会关闭。
12.1.6 模式选择
执行WFI(Wait For Interrupt)或者WFE(Wait For Event)指令后,STM32进入低功耗模式。
12.1.7 睡眠模式
执行完WFI/WFE指令后,STM32进入睡眠模式,程序暂停运行,唤醒后程序从暂停的地方继续运行;
SLEEPONEXIT位决定STM32执行完WFI或WFE后,是立刻进入睡眠,还是等STM32从最低优先级的中断处理程序中退出时进入睡眠;
在睡眠模式下,所有的I/O引脚都保持它们在运行模式时的状态;
WFI指令进入睡眠模式,可被任意一个NVIC响应的中断唤醒;
WFE指令进入睡眠模式,可被唤醒事件唤醒。
12.1.8 停止模式
执行完WFI/WFE指令后,STM32进入停止模式,程序暂停运行,唤醒后程序从暂停的地方继续运行;
1.8V供电区域的所有时钟都被停止,PLL、HSI和HSE被禁止,SRAM和寄存器内容被保留下来;
在停止模式下,所有的I/O引脚都保持它们在运行模式时的状态;
当一个中断或唤醒事件导致退出停止模式时,HSI被选为系统时钟;
我们的程序,默认再SystemInit函数里的配置,是使用的HSE外部高速时钟,通过PLL倍频,得到72MHz主频,但是进入停止模式后,PLL和HSE都停止了,而且在退出停止模式时,它并不会再自动帮我们开启PLL和HSE,而是默认用HSI的8MHz,直接作为主频,所以说如果忽略了这个问题,那么就会出现一个现象,程序刚上电是72MHz的主频,但是进入停止模式,再唤醒之后,就变成8MHz的主频了。所以我们一般在唤醒后,第一时间就是重启HSE,配置主频为72MHz,这个操作也不麻烦,配置函数已经写好了,我们只需要再调用SystemInit就行。
当电压调节器处于低功耗模式下,系统从停止模式退出时,会有一段额外的启动延时;
WFI指令进入停止模式,可被任意一个EXTI中断唤醒;
WFE指令进入停止模式,可被任意一个EXTI事件唤醒;
12.1.9 待机模式
执行完WFI/WFE指令后,STM32进入待机模式,唤醒后程序从头开始运行;
整个1.8V供电区域被断电,PLL、HSI和HSE也被断电,SRAM和寄存器内容丢失,只有备份的寄存器和待机电路维持供电;
在待机模式下,所有的I/O引脚变为高阻态(浮空输入);
对于输出来说,既不输出高电平,也不输出低电平,呈现高阻态;对于输入来说,不上拉也不下拉,呈现浮空输入状态。实际上GPIO的配置里,没有高阻态这个配置,它其实就是浮空输入配置,浮空输入,对于输出而言,就是高阻态。所以说如果提前点了个灯,进入待机模式后,无论这个灯是高电平点亮,还是低电平点亮,它都会熄灭,GPIO对外不输出高低电平,也不流过电流。
WKUP引脚的上升沿、RTC闹钟事件的上升沿、NRST引脚上外部复位、IWDG复位退出待机模式。
12.2 修改主频
12.2.1 硬件电路
12.2.2 软件部分
(1)复制《OLED显示屏》工程并改名为《修改主频》
(2)修改"system_stm32f10x.c"文件权限
有关主频的参数在"system_stm32f10x.c"里,但这个文件默认是只读的,因此要写修改其权限。
(3)修改"system_stm32f10x.c"文件频率参数
(4)main.c
cpp
#include "stm32f10x.h" // Device header
#include "Delay.h" // 调用延时头文件
#include "OLED.h"
uint8_t KeyNum;
int main(void)
{
OLED_Init(); // 初始化OLED屏幕
OLED_ShowString(1,1,"SYSCLK:");
OLED_ShowNum(1,8,SystemCoreClock,8);
while(1)
{
OLED_ShowString(2,1,"Running:");
Delay_ms(500); //注意,主频修改后Delay函数的时钟发生变换,所以主频修改后这里也应该进行相应修改
OLED_ShowString(2,1," ");
Delay_ms(500);
}
}
主频修改后相应得延时函数也得跟着更改才能保证准确性,所以一般不建议修改主频。
12.3 睡眠模式+串口发送+接收
12.3.1 硬件电路
12.3.2 软件部分
(1)复制《串口发送+接收》并改名为《睡眠模式+串口发送+接收》
在《串口发送+接收》的基础上,我们要为它加入低功耗的代码,假设我们目前要用STM32在做一个下位机,下位机接收到电脑发过来的指令,然后执行相应的功能,电脑随时都可能通过串口发送指令,当然也可以几个小时、几天都不发送指令,为了随时能响应指令,STM32就得时刻准备着,比如以下代码就一直在不断地检查标志位,但是如果一直不发指令,这些操作都没啥意义,还比较费电。当然你可能说把这段代码放在中断里就行了,但是即使主循环是空的,它CPU也是在不断耗电的,所以对于这种靠中断触发,就没什么事的代码,我们就可以给它加入低功耗模式,没事的时候就低功耗,中断来了,再醒来干活就行了。对于当前这个代码可以加入哪一种低功耗模式呢?
首先,睡眠模式,CPU时钟关闭,程序不再执行,但是外设的时钟不会关,USART硬件电路还是可以接收数据的,USART收到数据后,产生中断,唤醒CPU,所以睡眠模式可以。
之后,停机模式(停止模式),这个模式下,所有1.8V区域的时钟都关了,CPU和外设都不能运行,那自然USART也收不到数据,产生不了中断了,并且USART的中断也不能唤醒停止模式,所以当前这个程序功能,用不了停止模式。所以待机模式自然也不行了。
cpp
#include "stm32f10x.h" // Device header
#include "Delay.h" // 调用延时头文件
#include "OLED.h"
#include "Serial.h"
uint8_t RxData;
int main(void)
{
OLED_Init(); // 初始化OLED屏幕
OLED_ShowString(1,1,"RxData:");
Serial_Init();
while(1)
{
if(Serial_GetRxFlag()== 1) //if成立,就说明收到数据了
{
RxData = Serial_GetRxData();
Serial_SendByte(RxData);
OLED_ShowHexNum(1,8,RxData,2);
//无需手动清零
}
}
}
(2)main.c
cpp
#include "stm32f10x.h" // Device header
#include "Delay.h" // 调用延时头文件
#include "OLED.h"
#include "Serial.h"
uint8_t RxData;
int main(void)
{
OLED_Init(); // 初始化OLED屏幕
OLED_ShowString(1,1,"RxData:");
Serial_Init();
while(1)
{
if(Serial_GetRxFlag()== 1) //if成立,就说明收到数据了
{
RxData = Serial_GetRxData();
Serial_SendByte(RxData);
OLED_ShowHexNum(1,8,RxData,2);
//无需手动清零
}
OLED_ShowString(2,1,"Running");
Delay_ms(100);
OLED_ShowString(2,1," ");
Delay_ms(100);
__WFI(); //中断唤醒,推荐使用中断唤醒,程序会在这里进入睡眠模式,然后产生中断后继续运行
// __WFE(); //事件唤醒,配置比较麻烦
/*不开启睡眠模式时,循环会不断执行,开启睡眠模式后,循环只在中断唤醒发生后执行*/
}
}
不开启睡眠模式时,循环会不断执行,开启睡眠模式后,循环只在中断唤醒发生后执行。
这里直接使用
cpp
__WFI();
进入睡眠模式,在相当于使用了默认的立刻睡眠模式,即下图所示的SLEEPONEXIT=0;
如果一定需要修改,参照下图。
12.4 停止模式+对射式红外传感器计次
12.4.1 硬件电路
12.4.2 软件部分
(1)复制《对射式红外传感器计次》并更改名为《停止模式+对射式红外传感器计次》
(2)外部中断是不需要时钟的,因此可以采用停止模式来让它工作。
(3)PWR库函数
cpp
void PWR_DeInit(void); //恢复缺省配置
void PWR_BackupAccessCmd(FunctionalState NewState); //使能后备区域的访问
void PWR_PVDCmd(FunctionalState NewState); //使能PVD功能
void PWR_PVDLevelConfig(uint32_t PWR_PVDLevel); //配置PVD的阈值电压
void PWR_WakeUpPinCmd(FunctionalState NewState); //使能位于PA0位置的WKUP引脚,配合待机模式使用
void PWR_EnterSTOPMode(uint32_t PWR_Regulator, uint8_t PWR_STOPEntry); //进入停止模式,调用这个函数就可以进入停止模式
void PWR_EnterSTANDBYMode(void); //进入待机模式
FlagStatus PWR_GetFlagStatus(uint32_t PWR_FLAG); //获取标志位
void PWR_ClearFlag(uint32_t PWR_FLAG); //清除标志位
(4)注意下载程序时:1.按住复位键不动;2.点下载按钮;3.及时从开复位键。
(5)注意:这里如果中断函数里写了延时函数,会出现卡住不动的情况,把中断函数去掉。
(6)main.c
cpp
#include "stm32f10x.h" // Device header
#include "Delay.h" // 调用延时头文件
#include "OLED.h"
#include "CountSensor.h"
uint8_t KeyNum;
int main(void)
{
OLED_Init(); // 初始化OLED屏幕
CountSensor_Init();
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR,ENABLE); //开启PWR的外设时钟控制
OLED_ShowString(1,1,"Count:"); // 在1行3列显示字符串
while(1)
{
OLED_ShowNum(1,7,CountSensor_Get(),5); //将中断次数显示到OLED屏幕上
OLED_ShowString(2,1,"Running");
Delay_ms(100);
OLED_ShowString(2,1," ");
Delay_ms(100);
PWR_EnterSTOPMode(PWR_Regulator_ON,PWR_STOPEntry_WFI); //第一个参数开启电压调节器在停止模式里的状态,WFI模式进入停止
SystemInit();
/*我们的程序,默认再SystemInit函数里的配置,是使用的HSE外部高速时钟,通过PLL倍频,得到72MHz主频,但是进入停止模式后,
PLL和HSE都停止了,而且在退出停止模式时,它并不会再自动帮我们开启PLL和HSE,而是默认用HSI的8MHz,直接作为主频,所以说如果忽略了这个问题,
那么就会出现一个现象,程序刚上电是72MHz的主频,但是进入停止模式,再唤醒之后,就变成8MHz的主频了。所以我们一般在唤醒后,第一时间就是重启HSE,
配置主频为72MHz,这个操作也不麻烦,配置函数已经写好了,我们只需要再调用SystemInit就行。*/
}
}
12.5 待机模式+实时时钟
12.5.1 硬件电路
12.5.2 软件部分
(1)复制《实时时钟》工程并改名为《待机模式+实时时钟》
(2)main.c
cpp
#include "stm32f10x.h" // Device header
#include "Delay.h" // 调用延时头文件
#include "OLED.h"
#include "MyRTC.h"
uint8_t KeyNum;
int main(void)
{
OLED_Init(); // 初始化OLED屏幕
MyRTC_Init();
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR,ENABLE);
OLED_ShowString(1,1,"CNT :"); // 显示计数器
OLED_ShowString(2,1,"ALR :"); // 显示闹钟值
OLED_ShowString(3,1,"ALRF:"); // 闹钟标志位
PWR_WakeUpPinCmd(ENABLE); // WAKEUP上升沿唤醒待机
uint32_t Alarm = RTC_GetCounter()+10; // 因为闹钟值寄存器是只写的,写进去读不出来,所以用一个变量存一下
RTC_SetAlarm(Alarm); // 设置闹钟值为当前的10秒后
OLED_ShowNum(2,6,Alarm,10);
while(1)
{
OLED_ShowNum(1,6,RTC_GetCounter(),10);
OLED_ShowNum(3,6,RTC_GetFlagStatus(RTC_FLAG_ALR),1);
OLED_ShowString(4,1,"Running:");
Delay_ms(100);
OLED_ShowString(4,1," ");
Delay_ms(100);
OLED_ShowString(4,9,"STANDBY");
Delay_ms(100);
OLED_ShowString(4,9," ");
Delay_ms(100);
OLED_Clear(); //模拟OLED进入待机模式
PWR_EnterSTANDBYMode(); //STM进入待机模式前应当让其他外设先进入待机模式
}
}
(3)使用待机模式一样要把能关的电路都关掉,才能实现最大限度的省电。