待机低功耗唤醒实验
低功耗模式为何而来?
在系统或者电源复位后,微控制器出于运行状态之下,HCLK为CPU内核提供时钟,CPU内核执行代码。当CPU不需要继续运行时,可以利用多种低功耗模式来节省功耗。
三种低功耗模式运行时都具备哪些功能?
① 睡眠模式
只有CPU停止工作,各个外设正常工作,依靠任何中断/事件唤醒。
② 停止模式(深度睡眠模式)
所有时钟都已停止,但是1.8V内核电源工作。PLL,HIS和HSE等外部时钟源功能禁止,同时电压检测器也可进入低功耗模式,但是用于保存数据的寄存器,例如SRAM存储器中的内容被保留。此时只能依靠外部中断(EXTI)和RTC时钟才能唤醒。
③ 待机模式
1.8V供电区域被时钟停止,内部HSI,PLL,外部时钟HSE均关闭,同时电压检测器也可进入低功耗模式,SRAM和不属于待机电路的寄存器也被断电关闭,此时即使是外部中断(EXTI)也不能将其唤醒,只能通过复位(外部复位,看门狗复位)、唤醒引脚、RTC时钟来唤醒。
注意:无论是在停止模式 还是待机模式下,RTC和IWDG及其时钟源不会被关闭。
睡眠模式简介
在睡眠模式中,仅关闭了内核时钟,内核停止运行,但其片上外设,CM4核心的外设全都还照常运行。有两种方式进入睡眠模式,它的进入方式决定了从睡眠唤醒的方式,分别是WFI(wait for interrupt)和WFE(wait for event),即由等待"中断"唤醒和由"事件"唤醒。
进入睡眠模式的流程
当CPU处理完中断后软件必须能使其进入睡眠模式,当CPU执行WFI指令,其将会立即进入睡眠模式,当异常产生或中断被挂起时,其立即被唤醒。
当CPU执行WFE指令时,它首先会检查对应的事件标志位,当事件标志寄存器为0时进入睡眠模式,否则将寄存器事件标志清0并继续执行程序。事件标志可由外部事件标志或SEV指令产生。
通过执行WFI或WFE指令进入睡眠状态。根据Cortex™-M3系统控制寄存器中的SLEEPONEXIT 位的值,有两种选项可用于选择睡眠模式进入机制:
① SLEEP-NOW:如果SLEEPDEEP=0,那么被设为睡眠模式,此时CPU时钟立刻停止;
② SLEEP-ON-EXIT:在SLEEPDEEP=0的基础上,若令被唤醒的STM32执行完中断处理任务后进入睡眠模式,则可将SLEEPONEXIT设为1。
在睡眠模式下,所有的I/O引脚都保持它们在运行模式时的状态。
退出睡眠模式的流程
⑴ 如果执行WFI指令进入睡眠模式,任意一个被嵌套向量中断控制器响应的外设中断都能将系统从睡眠模式唤醒。
⑵ 如果执行WFE指令进入睡眠模式,则一旦发生唤醒事件时,微处理器都将从睡眠模式退出。唤醒事件可以通过下述两种方式产生:
① 使能外设中断但不使能NVIC中断,同时将SEVONPEND设为1,此时任何中断和事件都可以唤醒内核(原理详见“事件与中断的区别”),此时必须清除相应外设的中断挂起位(例如,清除使用库函数USART_ClearITPendingBit()清除USART外设寄存器中对应的中断标志位)和外设NVIC中断通道挂起位(在NVIC中断清除挂起寄存器中,调用NVIC_ClearPendingIRQ()库函数就OK了)。
② 配置一个外部或内部EXTI线为事件模式,当CPU从WFE恢复后,因为事件无挂起标志位且事件模式不会触发中断标志位悬挂,因此不必清除相应外设的中断挂起位或NVIC中断通道挂起位。
如何理解“退出睡眠模式的流程”?
由于睡眠模式只是停止了CPU的时钟,外设时钟正常使用,因此外设是可以正常运转的,但是我们要区分CPU和外设的功能:
① 外设的功能:
外设的功能是执行CPU给外设分配的任务,并且及时的反馈给CPU信息(触发中断进而在CPU中执行中断服务函数);
② CPU的功能:
程序是在CPU中执行的,CPU与外设的关系正如下图所示:
因此,虽然我们配置了相应外设对应的NVIC中断通道与外设自己的中断属性,但是并不可以进入中断服务函数中去执行程序,因此此时的NVIC总线上的中断标志是处于悬挂状态。
这就是我们为什么“在唤醒CPU后,一定要先清除相应外设的中断挂起位和外设NVIC中断通道挂起位”,因为如果我们一直悬挂着中断标志位那我们无法进入睡眠模式,因此我们为了再次进入唤醒模式,一定要清除正在处于悬挂状态的中断标志位。
低功耗疑难解答
MCU通过WFI命令进入sleep/stop模式后,当系统产生中断时,MCU被唤醒,这时候相关的中断处理函数会被执行吗?还是直接从WFI命令被执行的地方继续运行?
当MCU被唤醒之后,会优先执行“中断服务函数中的程序”,然后返回进入睡眠模式的程序位置向下继续执行。
为何我们配置中断总需要配置NVIC控制器和外设的控制器?
NVIC控制器是在ARM公司设计的Cortex-M3内核中,EXTI控制器是ST公司自己设计的外设。
在NVIC控制器中,有“Interrupt set-enable register & Interrupt clear-enable register”,它们来控制是否使能中断源EXTI0_IRQn,而EXTI控制器中,有“Interrupt mask register”来控制中断线EXTI_Line0的中断标志位是否传送给NVIC控制器。
在平时清除中断标志位的时候,为什么只需要清除EXTI的中断标志位就行,而不需要清除NVIC的悬起寄存器SETPEND?
有些程序员在中断服务程序结束的位置清除中断源的时候,还调用了NVIC_ClearPendingIRQ()函数清除中断源的悬起标志位,其实是不需要的。
如果中断发生时,正在处理同级或高优先级异常,或者被掩蔽,则中断不能立即得到响应。此时中断被悬起。中断的悬起状态可以通过“中断设置悬起寄存器(SETPEND) ”和“中断悬起清除寄存器(CLRPEND) ”来读取,还可以写它们来手工悬起中断。
由此我们可以得出结论:如果中断服务程序得到执行(中断得到响应),此时中断就不会被悬起。所以我们的中断服务程序不需要清除“悬起标志位”。
有人可能还会这么想:倘若一个中断服务程序因为处理器正在执行同级中断服务程序而不能被执行,中断被悬起,那么当轮到这个中断得到响应后,这个中断服务程序需要清楚“悬起标志位”吗?
我认为还是不需要,因为中断标志位的悬起和解悬,硬件都是可以自动控制的。也就是说,当悬起的中断被响应的时候,“悬起标志位”自动被硬件予以清除,不必手动清除。
但是对于“睡眠模式”来说,为了让我们再次进入“睡眠模式”,因此我们要清楚“唤醒睡眠模式的所有触发条件”。
电源调节器在三种低功耗模式下的状态
器件复位后电压调节器总是打开着的,其根据应用模式有三种不同的工作模式:
① 运行模式:调节器以全功耗模式为域 (内核,内存和数字外设) 提供1.8V电源。
② 停止模式:调节器以低功耗模式为保持寄存器及SRAM数据部分域提供1.8V的电源。
③ 待机模式:调节器断电,除了待机电路及备份域电路外,寄存器和SRAM的内容全部丢失。
事件与中断的区别
① 中断与事件发生的先后顺序
事件:是表示检测到某一动作(电平边沿)触发事件发生了。
中断:有某个事件发生并产生中断,并跳转到对应的中断处理程序中。
这也就是说:事件是中断的触发源,事件可以触发中断,也可以不触发,开放了对应的中断屏蔽位,则事件可以触发相应的中断。 事件还是其它一些操作的触发源,比如DMA,还有TIM中影子寄存器的传递与更新。
② 有中断优先级但是无事件优先级
中断有可能被更优先的中断屏蔽,事件不会。
③ 中断与事件的触发信号不同
事件本质上就是一个触发信号,是用来触发特定的外设模块或核心本身(唤醒)。
事件只是一个触发信号(脉冲),而中断则是一个固定的电平信号 。
④ 中断与事件触发后后续操作不同
简单点就是中断一定要有中断服务函数,但是事件却没有对应的函数。事件可以作为另一种动作的触发信号(定时器输入捕获上升沿信号后,可以出发LED0闪烁);
明白了外部中断的请求机制,就很容易理解事件的请求机制了。图中红色虚线箭头,标出了外部事件信号的传输路径,外部请求信号经过编号3的或门后,进入编号5的与门,这个与门的作用与编号4的与门类似,用于引入事件屏蔽寄存器的控制;最后脉冲发生器的一个跳变的信号转变为一个单脉冲,输出到芯片中的其它功能模块。
⑤ 中断与事件占用硬件资源情况不同()
事件可以在不需要CPU干预的情况下,执行这些操作,但是中断则必须要CPU介入.。
⑥ 事件相较于中断的优越性
事件机制提供了一个完全有硬件自动完成的触发到产生结果的通道,不要软件的参与,降低了CPU的负荷,节省了中断资源,提高了响应速度(硬件总快于软件),是利用硬件来提升CPU芯片处理事件能力的一个有效方法。
低功耗模式相关寄存器简介
SCR寄存器——SCR控制进入和退出低功率状态
SEVONPEND位:
0: 只有完全使能的中断或事件才能唤醒内核。
1: 任何中断和事件都可以唤醒内核。
SEVONPEND(发送事件在待命状态)位允许中断进入待命状态以触发唤醒事件。请注意:如果未在NVIC中启用这些中断,则仍会产生唤醒事件,但不会输入ISR(中断服务程序)。
SLEEPDEEP位:
0: 低功耗模式为睡眠模式。
1: 进入低功耗时为深度睡眠模式。
注意:睡眠状态用作睡眠模式和低功耗睡眠模式的基础,而深度睡眠状态用作停止模式和待机模式的基础。
SLEEPONEXIT位:
0: 立即进入睡眠模式。
1: 被唤醒后执行完相应的中断处理函数后进入睡眠模式。
电源控制寄存器PWR_CR
其中与低功耗有关的控制位:
CSBF: 清除待机标志,该位始终读出为0,写1清除待机标志。
CWUF: 清除唤醒标志,该位始终读出为0。写1清除唤醒标志。
PDDS: 掉电深睡眠
0: 当CPU进入深睡眠时进入停机模式,调压器的状态由LPDS位控制。
1: 当CPU进入深睡眠时进入待机模式。
LPDS: 深睡眠下的低功耗
0: 在停机模式下电压调节器开启
1: 在停机模式下电压调节器处于低功耗模式
电源控制/状态寄存器(PWR_CSR)
EWUP2/EWUP1: 使能WKUP2或EWUP1引脚。
0: WKUP1/2 引脚作为通用IO口。WKUP1引脚上的事件不能将CPU从待机模式唤醒。
1: WKUP1/2引脚用于将CPU从待机模式唤醒,WKUP1引脚被强置为输入下拉的配置(WKUP1引脚上的上升沿将系统从待机模式唤醒)。
SBF: 待机标志位
该位由硬件设置,只能设置电源控制寄存器PWR_CR的CSBF位清除。
0:系统不在待机模式
1:系统进入待机模式
WUF: 唤醒标志
由硬件设置,只能设置电源控制寄存器PWR_CR的CWUF位清除。
0: 没有唤醒事件发生
1: 从WKUP或RTC闹钟产生一个唤醒事件
注意:当WKUP引脚已经是高电平时,在(通过设置EWUP位)使能WKUP引脚时,会检测到一个额外唤醒的事件。
PRIMASK中断屏蔽寄存器
它仅包含一个可配置位PM(可优先中断屏蔽),如果将其设置为1,它将禁用所有具有可配置优先级的中断,如果将其设置为0,则不会发生任何作用。
睡眠模式的各种特性
当恢复到正常运行状态,不同低功耗模式所需的操作
睡眠模式:由中断或事件唤醒,唤醒后务必清除唤醒标志(WFE:外设中断标志位与NVIC中断标志位)。
停止模式:唤醒后需恢复时钟设置,并将改动的I/O设置恢复。
待机模式:唤醒后系统被恢复,随后系统被复位,可清除待机标志。
不同低功耗模式被唤醒后,如何执行程序?
模式 | 唤醒后,如何执行程序? | 原因 |
睡眠模式 | 在主程序中继续向下运行 | flash,SRAM等数据存储寄存器仍处于供电状态 |
停止模式 | 在主程序中继续向下运行 | flash,SRAM等数据存储寄存器仍处于供电状态 |
待机模式 | 从头开始运行主程序(复位) | 除了备份寄存器和待机电路其他外设全部停止运行 |
注:STM32的主程序一般存储在flash之中,也可以存储在SRAM之中。
待机模式简介
待机模式,它除了关闭所有的时钟,还把1.2V区域的电源也完全关闭了,也就是说,从待机模式唤醒后,由于没有之前代码的运行记录,只能对芯片复位,重新检测boot条件,从头开始执行程序。它有四种唤醒方式,分别是WKUP(PA0)引脚的上升沿,RTC闹钟事件,NRST引脚的复位和IWDG(独立看门狗)复位。
注意:在以上讲解的睡眠模式、停止模式及待机模式中,若备份域电源正常供电,备份域内的RTC都可以正常运行、备份域内的寄存器及备份域内的SRAM数据会被保存,不受功耗模式影响。
停机模式简介
在停止模式中,进一步关闭了其它所有的时钟,于是所有的外设都停止了工作,但由于其1.2V区域的部分电源没有关闭,还保留了内核的寄存器、内存的信息,所以从停止模式唤醒,并重新开启时钟后,还可以从上次停止处继续执行代码。停止模式可以由任意一个外部中断(EXTI)唤醒。在停止模式中可以选择电压调节器为开模式或低功耗模式,可选择内部FLASH工作在正常模式或掉电模式。
注意:
① “掉电”的含义:
掉电是描述用电设备因断电、失电、或电的质量达不到要求而不能正常工作的术语。由于断电、失电、或电的质量达不到要求而导致用电设备不能正常工作的现象。
② Flash掉电的意义:
在保存flash原有数据的基础上,进一步降低功耗。
CMSIS-CORE标准中低功耗函数解析
函数 | 功能 |
__WFI() | 执行WFI指令并进入低功耗模式 |
__WFE() | 执行WFE指令并进入低功耗模式 |
__disable_irq() | 清除PM位 |
__enable_irq() | 设置PM位并且屏蔽所有中断 |
__ get_PRIMASK() | 返回PRIMASK寄存器当前状态 |
PVD可编程电压监测器的使用
PVD的作用是监视供电电压,在供电电压下降到给定的阀值以下时,产生一个中断,通知软件做紧急处理。当供电电压又恢复到给定的阀值以上时,也会产生一个中断,通知软件供电恢复。供电下降的阀值与供电上升的PVD阀值有一个固定的差值,引入这个差值的目的是为了防止电压在阀值上下小幅抖动,而频繁地产生中断。
PVD在应用中的实际作用如下:
当条件触发,需要系统进入特别保护状态,执行紧急关闭任务:对系统的一些数据保存起来,同时对外设进行相应的保护操作。
电源控制/状态寄存器(PWR_CSR)中的PVDO标志用来表明VDD是高于还是低于PVD的电压阀值。
#include "exti.h"
#include "stm32f10x.h"
void PVD_InitConfig()
{
NVIC_InitTypeDef NVIC_InitStructure;
EXTI_InitTypeDef EXTI_InitStructure;
PWR_PVDLevelConfig(PWR_PVDLevel_2V4);
PWR_PVDCmd(ENABLE);
EXTI_InitStructure.EXTI_Line = EXTI_Line16;
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising_Falling;
EXTI_Init(&EXTI_InitStructure);
NVIC_InitStructure.NVIC_IRQChannel = PVD_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStructure);
}
void PVD_IRQHandler()
{
if(EXTI_GetFlagStatus(EXTI_Line16) == SET)
{
GPIO_ResetBits(GPIOA,GPIO_Pin_5);
}
EXTI_ClearITPendingBit(EXTI_Line16);
}
注意:
① 外部中断线映射关系图如下:
② NVIC与EXTI的关系:
EXTIx线上产生中断信号后(一条外部中断线上只允许在同一时刻产生一种中断信号),会经由NVIC总线将中断信号传入MCU的CPU内核中,进而执行中断服务程序(PVD_IRQHandler)。
③ EXTI外部中断触发模式选择:
EXTI_Trigger_Rising ---表示电压从低电压上升到高于设定的电压阀值产生中断;
EXTI_Trigger_Falling --- 表示电压从高电压下降到低于设定的电压阀值产生中断;
EXTI_Trigger_Rising_Falling --- 表示电压从高电压下降到低于设定的电压阀值、或从低电压上升到高于设定的电压阀值产生中断。
④ PVD电压阈值的选择:
STM32的3种低功耗模式的工作参数
STM32F0器件实现了五种低功耗模式:低功耗运行模式、睡眠模式、低功耗睡眠模式、停止模式和待机模式。一般而言,随着功耗的下降;性能下降,唤醒时间增加,唤醒源数量减少。
注:STM32F10XZET6的低功耗模式仅有SLEEP,STOP,STANDBY三种模式,因此我们只参考这三种模式即可。
睡眠模式配置代码(自定义配置)
Lowpower.c
#include "lowpower.h"
#include "exti.h"
#include "stm32f10x.h"
void Enter_SleepMode()
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR,ENABLE);
EXTI_InitConfig();
NVIC_SystemLPConfig(NVIC_LP_SLEEPDEEP,DISABLE); // 启动睡眠模式
NVIC_SystemLPConfig(NVIC_LP_SLEEPONEXIT,ENABLE); // 执行完中断后才能进入睡眠模式
NVIC_SystemLPConfig(NVIC_LP_SEVONPEND,ENABLE); // 任何中断或者事件都可以唤醒MCU
// SCB->SCR &= ~SCB_SCR_SLEEPDEEP; // 与上面的库函数为等价操作
// SCB->SCR |= SCB_SCR_SLEEPONEXIT;
// SCB->SCR |= SCB_SCR_SEVONPEND;
__WFI(); // 在以上条件下执行WFI(等待中断)
}
Lowpower.h
#ifndef _LOWPOWER_H
#define _LOWPOWER_H
void Enter_SleepMode();
#endif
Exti.c
#include "exti.h"
#include "stm32f10x.h"
void EXTI_InitConfig()
{
GPIO_InitTypeDef GPIO_InitStructure;
EXTI_InitTypeDef EXTI_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOE|RCC_APB2Periph_AFIO,ENABLE);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOE, &GPIO_InitStructure);
GPIO_EXTILineConfig(GPIO_PortSourceGPIOE,GPIO_PinSource4); // 将PE5映射至EXTI_Line_4上
EXTI_InitStructure.EXTI_Line = EXTI_Line4;
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; // 这里配置为EXTI_Mode_Event也是可以的
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;
EXTI_Init(&EXTI_InitStructure);
NVIC_InitStructure.NVIC_IRQChannel = EXTI4_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;
NVIC_Init(&NVIC_InitStructure);
}
Exit.h
#ifndef _EXTI_H
#define _EXTI_H
void EXTI_InitConfig();
#endif
为什么EXTI外部中断实验要开启AFIO端口复用时钟?
记住一句话:AFIO时钟只有在开启了端口重映射,外部中断,事件控制,这三种情况下需要开启AFIO。
配置EXTI外部中断时需开启IO口复用时钟,设置IO口与中断线的映射关系。
目的如下:
STM32的IO口与中断线的对应关系需要配置外部中断配置寄存器EXTICRx,这样我们要先开启复用时钟,然后配置IO口与中断线的对应关系。才能把外部中断与中断线连接起来。
停止模式配置代码(固件库中获取)
void PWR_EnterSTOPMode(uint32_t PWR_Regulator, uint8_t PWR_STOPEntry)
{
uint32_t tmpreg = 0;
/* Check the parameters */
assert_param(IS_PWR_REGULATOR(PWR_Regulator));
assert_param(IS_PWR_STOP_ENTRY(PWR_STOPEntry));
/* Select the regulator state in STOP mode ---------------------------------*/
tmpreg = PWR->CR;
/* Clear PDDS and LPDS bits */
tmpreg &= CR_DS_MASK;
/* Set LPDS bit according to PWR_Regulator value */
tmpreg |= PWR_Regulator;
/* Store the new value */
PWR->CR = tmpreg;
/* Set SLEEPDEEP bit of Cortex System Control Register */
SCB->SCR |= SCB_SCR_SLEEPDEEP;
/* Select STOP mode entry --------------------------------------------------*/
if(PWR_STOPEntry == PWR_STOPEntry_WFI)
{
/* Request Wait For Interrupt */
__WFI();
}
else
{
/* Request Wait For Event */
__WFE();
}
/* Reset SLEEPDEEP bit of Cortex System Control Register */
SCB->SCR &= (uint32_t)~((uint32_t)SCB_SCR_SLEEPDEEP);
}
为什么先置位SLEEPDEEP位然后又清除SLEEPDEEP位?
假设:STM32 低功耗入口 PWR_EnterSTOPMode()函数中设置了SCB_SysCtrl的SleepDeep位。退出函数时,也没有清除该位。
因此,如果我的程序以如下方式运行:
...
int main()
{
...
while(xxx)
{
__WFI(); //此处简单进入sleep模式以降低功耗
}
//处理结束,进入低功耗,2秒以后醒来
RTCSetAlarm(...)
PWR_EnterSTOPMode(xx,xx);
}
第一次运行没有问题,一旦运行PWR_EnterSTOPMode再唤醒后,由于SleepDeep位没有清除,那么只调用 __WFI()会导致进入Stop模式而不是Sleep模式。
待机模式的代码(固件库中获取)
void PWR_EnterSTANDBYMode(void)
{
/* Clear Wake-up flag */
PWR->CR |= PWR_CR_CWUF;
/* Select STANDBY mode */
PWR->CR |= PWR_CR_PDDS;
/* Set SLEEPDEEP bit of Cortex System Control Register */
SCB->SCR |= SCB_SCR_SLEEPDEEP;
/* This option is used to ensure that store operations are completed */
#if defined ( __CC_ARM )
__force_stores();
#endif
/* Request Wait For Interrupt */
__WFI();
}
为什么这个函数不用注意像停止模式配置时的注意事项?
因为待机模式被唤醒后相当于复位,从头开始又执行了一遍,此时系统中除电源控制/状态寄存器(PWR_CSR)外的所有寄存器的值均初始化为上电默认值(电源控制/状态寄存器(PWR_CSR)将会指示内核由待机状态退出),因此我们不用“先置位SLEEPDEEP位然后又清除SLEEPDEEP位”来防止“只调用 __WFI()会导致进入Stop模式而不是Sleep模式”。
低功耗固件库库函数介绍
函数参数说明
PWR_EnterSTOPMode(uint32_t PWR_Regulator, uint8_t PWR_STOPEntry)中PWR_Regulator参数用于配置“电压调节器”,有两个选择:
PWR_Regulator_LowPower | 在停机模式下电压调压器开启 |
PWR_Regulator_ON | 在停机模式下电压调压器处于低功耗模式 |
相关寄存器(PWR_CR寄存器)的位如下:
PWR_STOPEntry参数是用于配置“唤醒停止模式的条件的”,对应的选择如下:
PWR_STOPEntry_WFI | 中断将MCU从停止模式中唤醒 |
PWR_STOPEntry_WFE | 事件将MCU从停止模式中唤醒 |
模式的含义如下:
如何配置这两种模式:
WFI实现函数 | __WFI() |
WFE实现函数 | __WFE() |
综合代码示例1(外部中断从停止模式/睡眠模式中唤醒MCU)
Led.c
#include "led.h"
#include "stm32f10x.h"
void LED_InitConfig()
{
GPIO_InitTypeDef GPIO_InitStructure;;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOE|RCC_APB2Periph_GPIOB,ENABLE);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOE,&GPIO_InitStructure);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStructure);
GPIO_WriteBit(GPIOE,GPIO_Pin_5,1);
GPIO_WriteBit(GPIOB,GPIO_Pin_5,1);
}
Led.h
#ifndef _LED_H
#define _LED_H
#include "sys.h"
void LED_InitConfig();
#define LED0 PEout(5)
#define LED1 PBout(5)
#endif
Lowpower.c
#include "lowpower.h"
#include "exti.h"
#include "stm32f10x.h"
void Enter_SleepMode()
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR,ENABLE);
EXTI_InitConfig();
NVIC_SystemLPConfig(NVIC_LP_SLEEPDEEP,DISABLE); // 启动睡眠模式
NVIC_SystemLPConfig(NVIC_LP_SLEEPONEXIT,ENABLE); // 执行完中断后才能进入睡眠模式
NVIC_SystemLPConfig(NVIC_LP_SEVONPEND,ENABLE); // 任何中断或者事件都可以唤醒MCU
// SCB->SCR &= ~SCB_SCR_SLEEPDEEP; // 与上面的库函数为等价操作
// SCB->SCR |= SCB_SCR_SLEEPONEXIT;
// SCB->SCR |= SCB_SCR_SEVONPEND;
__WFI(); // 在以上条件下执行WFI(等待中断)
}
Lowpower.h
#ifndef _LOWPOWER_H
#define _LOWPOWER_H
void Enter_SleepMode();
#endif
Main.c(睡眠模式版本)
#include "stm32f10x.h"
#include "key.h"
#include "led.h"
#include "exti.h"
#include "lowpower.h"
int main()
{
KEY_InitConfig();
LED_InitConfig();
EXTI_InitConfig();
while(1)
{
if(SingleKEY_Scan(KEY1,KEY1_PRESS) == KEY1_PRESS)
{
Enter_SleepMode(); // 睡眠模式——由于设置为了WFI模式因此外部中断可以将其唤醒继续执行程序
}
}
}
Main.c(停止模式版本)
#include "stm32f10x.h"
#include "key.h"
#include "led.h"
#include "exti.h"
#include "lowpower.h"
int main()
{
KEY_InitConfig();
LED_InitConfig();
EXTI_InitConfig();
while(1)
{
if(SingleKEY_Scan(KEY1,KEY1_PRESS) == KEY1_PRESS)
{
PWR_EnterSTOPMode(PWR_Regulator_LowPower,PWR_STOPEntry_WFI); // 停止模式——外部中断可以将其唤醒继续执行程序
}
}
}
Exit.c
#include "exti.h"
#include "led.h"
#include "stm32f10x.h"
void EXTI_InitConfig()
{
GPIO_InitTypeDef GPIO_InitStructure;
EXTI_InitTypeDef EXTI_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOE|RCC_APB2Periph_AFIO,ENABLE);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOE, &GPIO_InitStructure);
GPIO_EXTILineConfig(GPIO_PortSourceGPIOE,GPIO_PinSource4); // 将PE5映射至EXTI_Line_4上
EXTI_InitStructure.EXTI_Line = EXTI_Line4;
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; // 这里配置为EXTI_Mode_Event也是可以的
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;
EXTI_Init(&EXTI_InitStructure);
NVIC_InitStructure.NVIC_IRQChannel = EXTI4_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;
NVIC_Init(&NVIC_InitStructure);
LED_InitConfig();
}
void PVD_InitConfig()
{
NVIC_InitTypeDef NVIC_InitStructure;
EXTI_InitTypeDef EXTI_InitStructure;
PWR_PVDLevelConfig(PWR_PVDLevel_2V4);
PWR_PVDCmd(ENABLE);
EXTI_InitStructure.EXTI_Line = EXTI_Line16;
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising_Falling;
EXTI_Init(&EXTI_InitStructure);
NVIC_InitStructure.NVIC_IRQChannel = PVD_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStructure);
}
void PVD_IRQHandler()
{
if(EXTI_GetFlagStatus(EXTI_Line16) == SET)
{
GPIO_ResetBits(GPIOA,GPIO_Pin_5);
}
EXTI_ClearITPendingBit(EXTI_Line16);
}
void EXTI4_IRQHandler()
{
if(EXTI_GetFlagStatus(EXTI_Line4) == SET)
{
LED1 = !LED1;
}
EXTI_ClearITPendingBit(EXTI_Line4);
}
Exit.h
#ifndef _EXTI_H
#define _EXTI_H
void PVD_InitConfig();
void EXTI_InitConfig();
#endif
Key.h
#ifndef _KEY_H
#define _KEY_H
#include "sys.h"
void KEY_InitConfig();
u8 SingleKEY_Scan(u8 KEY_Order,u8 Press_Flag);
u8 KEY_Scan();
#define KEY0 PEin(4)
#define KEY1 PEin(3)
#define KEY0_PRESS 1
#define KEY1_PRESS 2
#endif
Key.c
#include "key.h"
#include "sys.h"
#include "delay.h"
#include "stm32f10x.h"
void KEY_InitConfig()
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOE,ENABLE);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4|GPIO_Pin_3;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOE,&GPIO_InitStructure);
}
u8 SingleKEY_Scan(u8 KEY_Order,u8 Press_Flag)
{
u8 SupportSteadyPressed = 1;
delay_init();
if(SupportSteadyPressed && !KEY_Order)
{
SupportSteadyPressed = 0;
delay_ms(10);
if(!KEY_Order)
{
return Press_Flag;
}
}
else if(!SupportSteadyPressed && !KEY_Order)
{
delay_ms(10);
if(!KEY_Order)
{
SupportSteadyPressed = 0;
}
}
else
{
SupportSteadyPressed = 1;
}
return 0;
}
综合代码示例2(外部中断从待机模式中唤醒MCU)
Key.h
#ifndef _KEY_H
#define _KEY_H
#include "sys.h"
void KEY_InitConfig();
u8 SingleKEY_Scan(u8 KEY_Order,u8 Press_Flag);
u8 KEY_Scan();
#define KEY0 PEin(4)
#define KEY1 PEin(3)
#define KEY_WAKEUP PAin(0)
#define KEY0_PRESS 1
#define KEY1_PRESS 2
#define KEY_WAKEUP_PRESS 3
#endif
Key.c
#include "key.h"
#include "sys.h"
#include "delay.h"
#include "stm32f10x.h"
void KEY_InitConfig()
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOE|RCC_APB2Periph_GPIOA,ENABLE);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4|GPIO_Pin_3;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOE,&GPIO_InitStructure);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
}
u8 SingleKEY_Scan(u8 KEY_Order,u8 Press_Flag)
{
u8 SupportSteadyPressed = 1;
delay_init();
if(SupportSteadyPressed && !KEY_Order)
{
SupportSteadyPressed = 0;
delay_ms(10);
if(!KEY_Order)
{
return Press_Flag;
}
}
else if(!SupportSteadyPressed && !KEY_Order)
{
delay_ms(10);
if(!KEY_Order)
{
SupportSteadyPressed = 0;
}
}
else
{
SupportSteadyPressed = 1;
}
return 0;
}
Led.c
#include "led.h"
#include "stm32f10x.h"
void LED_InitConfig()
{
GPIO_InitTypeDef GPIO_InitStructure;;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOE|RCC_APB2Periph_GPIOB,ENABLE);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOE,&GPIO_InitStructure);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStructure);
GPIO_WriteBit(GPIOE,GPIO_Pin_5,1);
GPIO_WriteBit(GPIOB,GPIO_Pin_5,1);
}
Led.h
#ifndef _LED_H
#define _LED_H
#include "sys.h"
void LED_InitConfig();
#define LED0 PEout(5)
#define LED1 PBout(5)
#endif
Lowpower.c
#include "lowpower.h"
#include "exti.h"
#include "stm32f10x.h"
void Enter_SleepMode()
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR,ENABLE);
EXTI_InitConfig();
NVIC_SystemLPConfig(NVIC_LP_SLEEPDEEP,DISABLE); // 启动睡眠模式
NVIC_SystemLPConfig(NVIC_LP_SLEEPONEXIT,ENABLE); // 执行完中断后才能进入睡眠模式
NVIC_SystemLPConfig(NVIC_LP_SEVONPEND,ENABLE); // 任何中断或者事件都可以唤醒MCU
// SCB->SCR &= ~SCB_SCR_SLEEPDEEP; // 与上面的库函数为等价操作
// SCB->SCR |= SCB_SCR_SLEEPONEXIT;
// SCB->SCR |= SCB_SCR_SEVONPEND;
__WFI(); // 在以上条件下执行WFI(等待中断)
}
void Enter_StandbyMode()
{
RCC_APB2PeriphClockCmd(RCC_APB1Periph_PWR,ENABLE);
PWR_WakeUpPinCmd(ENABLE);
PWR_EnterSTANDBYMode();
}
Lowpower.h
#ifndef _LOWPOWER_H
#define _LOWPOWER_H
void Enter_SleepMode();
void Enter_StandbyMode();
#endif
可以将MCU从待机模式中唤醒的引脚:
复位引脚 |
侵入信号引脚 |
我在key.c中唤醒的key0(key0与中断线0相连可以捕获上升沿信号) |
Main.c
#include "stm32f10x.h"
#include "key.h"
#include "led.h"
#include "exti.h"
#include "lowpower.h"
int main()
{
KEY_InitConfig();
LED_InitConfig();
EXTI_InitConfig();
while(1)
{
if(SingleKEY_Scan(KEY1,KEY1_PRESS) == KEY1_PRESS)
{
LED0 = !LED0; // 用于指示将要进入待机模式
Enter_StandbyMode(); // 当按下KEY1键后,如果不按复位键或者KEY0,MCU就不会相应你的任何操作
}
}
}
Exit.c
#include "exti.h"
#include "led.h"
#include "stm32f10x.h"
void EXTI_InitConfig()
{
GPIO_InitTypeDef GPIO_InitStructure;
EXTI_InitTypeDef EXTI_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOE|RCC_APB2Periph_AFIO,ENABLE);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOE, &GPIO_InitStructure);
GPIO_EXTILineConfig(GPIO_PortSourceGPIOE,GPIO_PinSource4); // 将PE5映射至EXTI_Line_4上
EXTI_InitStructure.EXTI_Line = EXTI_Line4;
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; // 这里配置为EXTI_Mode_Event也是可以的
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;
EXTI_Init(&EXTI_InitStructure);
NVIC_InitStructure.NVIC_IRQChannel = EXTI4_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;
NVIC_Init(&NVIC_InitStructure);
LED_InitConfig();
}
void PVD_InitConfig()
{
NVIC_InitTypeDef NVIC_InitStructure;
EXTI_InitTypeDef EXTI_InitStructure;
PWR_PVDLevelConfig(PWR_PVDLevel_2V4);
PWR_PVDCmd(ENABLE);
EXTI_InitStructure.EXTI_Line = EXTI_Line16;
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising_Falling;
EXTI_Init(&EXTI_InitStructure);
NVIC_InitStructure.NVIC_IRQChannel = PVD_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStructure);
}
void PVD_IRQHandler()
{
if(EXTI_GetFlagStatus(EXTI_Line16) == SET)
{
GPIO_ResetBits(GPIOA,GPIO_Pin_5);
}
EXTI_ClearITPendingBit(EXTI_Line16);
}
void EXTI4_IRQHandler()
{
if(EXTI_GetFlagStatus(EXTI_Line4) == SET)
{
LED1 = !LED1;
}
EXTI_ClearITPendingBit(EXTI_Line4);
}
Exit.h
#ifndef _EXTI_H
#define _EXTI_H
void PVD_InitConfig();
void EXTI_InitConfig();
#endif
疑难解答
RTC_IRQHandler与RTCAlarm_IRQHandler有何不同?
RTC_IRQHandler对应的中断有三个overflow,second和alarm中断,因其包含了RTC的所有中断类型因此也被称为RTC的全局中断,但是RTCAlarm_IRQHandler不同,我们知道RTC闹钟中断的触发过程如下:
由此可见,当我们用RTC闹钟中断去唤醒停止模式中的MCU时,我们需要在两个中断服务函数中分别清楚一个中断标志位,对应关系如下:
中断向量类型 | 中断向量名称 | 所需清除的标志位 |
RTC全局中断服务函数入口 | RTC_IRQHandler | RTC_ClearITPendingBit(RTC_IT_ALR) |
外部中断线17终端服务函数入口 | RTCAlarm_IRQHandler | EXTI_ClearITPendingBit(EXTI_Line17) |
除此之外,其他中断线对应的外部中断源为:
EXTI0~15映射到了IO口 |
EXTI16连接到PVD的输出 |
EXTI17连接到RTC闹钟事件 |
EXTI18连接到USB唤醒事件 |
EXTI19连接到ETH唤醒事件 |
RTC_IRQHandler与RTCAlarm_IRQHandler中断优先级有何不同?
针对于这个问题,我们可以参考“stm32中文参考手册”中的中断向量优先级:
我们可以看到:RTC全局中断在默认情况下要优先于EXTI_Line_17中断执行。
用RTC闹钟事件唤醒停止状态的MCU注意事项
void RTC_InitConfig(u8 AlarmValue)
{
u8 temp = 0;
NVIC_InitTypeDef NVIC_InitStructure;
EXTI_InitTypeDef EXTI_InitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);
PWR_BackupAccessCmd(ENABLE); // 注意:使能 RTC 和后备寄存器访问才可以进行读写操作
delay_init();
if(BKP_ReadBackupRegister(BKP_DR1) == 0x5050)
{
BKP_DeInit(); // 备份区域复位
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
RCC_RTCCLKCmd(ENABLE);
while(RCC_GetFlagStatus(RCC_FLAG_LSERDY) == SET && temp <= 250) // 看看LSE低速时钟是否准备好了
{
temp++;
delay_ms(10);
}
if(temp >= 250) return; // 发生错误,直接返回
RTC_WaitForLastTask(); // 等待前面的写操作完成
RTC_WaitForSynchro(); // 等待时钟同步
RTC_ITConfig(RTC_IT_ALR,ENABLE); // 配置RTC_CR(APB1接口寄存器——非RTC核心寄存器)
RTC_WaitForLastTask(); // 等待前面的写操作完成
RTC_WaitForSynchro(); // 等待时钟同步
RTC_EnterConfigMode(); // 允许配置RTC核心寄存器
RTC_SetPrescaler(32767);
RTC_WaitForSynchro(); // 等待时钟同步
RTC_WaitForLastTask(); // 等待前面的写操作完成
RTC_SetCounter(0); // 设置计数器初始计数值为0
RTC_SetAlarm(AlarmValue); // 设置闹钟值
RTC_WaitForLastTask(); // 等待前面的写操作完成
RTC_ExitConfigMode(); // 退出配置
NVIC_InitStructure.NVIC_IRQChannel = RTCAlarm_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStructure);
EXTI_InitStructure.EXTI_Line = EXTI_Line17;
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising;
EXTI_Init(&EXTI_InitStructure); // 配置EXTI_Line17为输出闹钟事件(上升沿触发)
BKP_WriteBackupRegister(BKP_DR1,0x5050);
}
else
{
delay_ms(50); // 等待唤醒后的系统时钟初始化
RTC_WaitForSynchro(); // 等待时钟同步
RTC_WaitForLastTask(); // 等待前面的写操作完成
RTC_ITConfig(RTC_IT_ALR,ENABLE); // 打开RTC闹钟中断
RTC_SetCounter(0); // 重新设置计数器初始值
RTC_WaitForLastTask(); // 等待前面的写操作完成
}
}
我们第一次配置RTC的时候,将0x5050写入RTC的BKP备份区域中,以后我们只要在使能对RTC的写操作后就可以读取BKP后被区域的信息,进而就可以知道我们是否以前初始化过RTC,这样就节省了再次重新初始化RTC所浪费的不必要时间。