目录
STM32 PWR电源控制 与 低功耗模式 详解
1. PWR 电源控制 简介
PWR(Power Control)电源控制
- PWR负责管理STM32内部的电源供电部分,可以实现可编程电压监测器和低功耗模式的功能
- 可编程电压监测器(PVD)可以监控VDD电源电压,当VDD下降到PVD阀值以下或上升到PVD阀值之上时,PVD会触发中断,用于执行紧急关闭任务
- 低功耗模式包括睡眠模式(Sleep)、停机模式(Stop)和待机模式(Standby),可在系统空闲时,降低STM32的功耗,延长设备使用时间
2. PWR 电源控制 框图
- 最上面是模拟供电VDDA
- 主要为AD转换器、温度传感器、复位模块、PLL锁相环供电
- 其中AD转换器,还有两根参考电压的供电引脚:VREF+、VREF-(在C8T6中直接接到了VDDA和VSSA了。其他引脚比较多的情况下会直接引出去)
- 中间为数字部分供电,分为两部分:VDD供电、1.8V供电
- 左边 部分为 VDD供电,其中包括IO供电、待机电路、唤醒逻辑、独立看门狗
- 右边 部分为 VDD通过电压调节器降压到1.8V,提供给1.8V供电区域。 1.8V区域包括CPU核心、存储器、内置的 数字外设。
- 下面为后备供电区域。VBAT
- 其中包括LSE 32K晶体振荡器、后备寄存器BKP、RCC_BOCR寄存器(是RCC的寄存器,是备份域控制寄存器)、RTC
- 低电压检测器是控制供电的。当VDD有电时,这块区域由VDD供电,没电才用VBAT供电。
3. 上电复位和掉电复位 与 可编程电压检测器(PVD)
3.1 内嵌复位与电源控制模块特性图
这个是内嵌的上电掉电复位和可编程电压检测器的框图。填写寄存器对应的值,会有对应的上下限电压阈值。(以典型值为准)
3.2 上电复位和掉电复位
-
当VDD或VDDA电压过低时,内部电路直接产生复位,让stm32复位住,不要乱操作
-
图中的40mV的迟滞。是用来定义上下限的。用来复位和解除复位。
这是一个典型的迟滞比较器。迟滞比较器设置两个阈值比设置单一的阈值要好。可以防止电压在某个阈值附近波动时,造成输出也来抖动
- 当电压小于下线PDR时,复位。
- 当电压大于上限POR时,解除复位
-
由上表可知,在STM32中 复位的下限为1.88V、上限为1.92V、每次复位持续时间为2.5ms)
-
可以看到:在低于下限电压时,复位信号一直为0,代表复位(低电平有效)。 而高于上限时,复位信号为1,代表不复位。
3.3 可编程电压检测器(PVD)
电压检测器和上电复位和掉电复位电路。都是用来监测VDD和VDDA的供电电压的。
而PVD的区别是:
- 阈值电压可以用程序指定
- 范围在数据手册中可以看到,可选范围(大概)是2.2V ~ 2.9V
- PVD的上限和下限的迟滞电压为100mv,是比上电掉电复位的电压要高的。
PVD的工作逻辑:
-
电压超过设置的阈值时,代表正常。PVD输出为0, 而电压低于设置的阈值时,代表电压过低。此时 PVD输出为1。开始警告
-
产生的这个上升沿和下降沿时可以在外部中断申请中断。在中断中可以进行相关操作。
3.4 总结来说可以看这个图来理解。
PVD是来检测2.9V~2.2V左右的电压,可以在这个范围内设置一个警告线,如果低于则触发警告。可以进入中断做一些事情。
上电掉电复位则是提供一个最低的阈值,如果低于这个阈值则复位。
4. 低功耗模式介绍
进入停机或者待机模式之后,需要按住复位再下载程序,否则下载失败 (我这里不行,我会断电重上电,在未进入待机模式前开始烧录代码)
在理解睡眠、停机、待机模式时,参考PWR电源控制框图 效果更佳
- 低功耗模式有 睡眠、 停机、 待机、
- 从上到下,关闭的电路越来越多。
- 从上到下,越来越省电
- 从上到下,越来越难唤醒
睡眠模式简介
-
睡眠模式有两种进入方式:
-
直接调用WFI进入:WFI(Wait For Interrupt)等待中断,有中断才能被唤醒
这个一般是为了进入中断处理中断函数。
-
直接调用WFE进入:WFE(Wait For Event)等待事件,有事件才能被唤醒
(这个事件可以是外部中断配置为事件模式,也可以是使能了中断,但没有配置NVIC)
这个一般是不需要进入中断的,直接从睡的地方继续运行。
-
-
睡眠模式对电路的影响:
- 对 1.8V区域的时钟 影响是 CPU时钟关、对其他时钟和ADC时钟无影响、 对 VDD区域的时钟 无影响 对 电压调节器 操作为开 (电压调节器是VDD通过调节后的1.8V,给后备区域用的)
**总结就是,睡眠模式只把CPU时钟关闭,对其他电路没有任何操作 CPU时钟关闭之后,程序就会暂停。但是寄存器的数据都还在 注意:睡眠进入不了的可能原因:**系统定时器SysTick一直产生中断
停机模式简介
- 停机模式要操控的标志位:
- PDDS位 :设置停机还是待机模式**。0为停机模式**、设置为1为待机模式
- LPDS位 : 设置电压调节器的开关,0为开启。1为进入低功耗 电压调节器是否关闭,1.8V区域的寄存器仍然能保持寄存器的数据。不过开启后更省电,但唤醒更慢罢了。
- SLEEPDEEP位 : 设置是否进入深度睡眠模式。 1 为进入。
- 最后调用WFI或者WFE,芯片就可以进入停机模式了。
- 对电路有何影响:
- 关闭1.8V区域的时钟。 这就代表CPU时钟和内置数字外设的时钟都会停止。比如定时器、串口… 不过没关闭电源。寄存器的数据都还在。(原因见PWR电源框图)
- HSI和HSE的振荡器关闭, 因为外设都关了。所以要高速时钟也没用了。所以会关闭HSI内部高速时钟、HSE外部高速时钟。两个低速时钟不会主动关闭,因为这两个时钟要维持RTC和独立看门狗的运行。
- 如何唤醒
- 在停机模式下。任一外部中断(在外设中断寄存器中设置)就可以唤醒
待机模式简介
- 停机模式要操控的标志位:
- PDDS位 :设置停机还是待机模式。0为停机模式、设置为1为待机模式
- SLEEPDEEP位 : 设置是否进入深度睡眠模式**。 1 为进入。**
- 最后调用WFI或者WFE,芯片就可以进入待机模式了。
- 对电路有何影响:
- 关闭1.8V区域的时钟。 这就代表CPU时钟和内置数字外设的时钟都会停止。比如定时器、串口… 不过没关闭电源。寄存器的数据都还在。(原因见PWR电源框图)
- HSI和HSE的振荡器关闭, 因为外设都关了。所以要高速时钟也没用了。所以会关闭HSI内部高速时钟、HSE外部高速时钟。两个低速时钟不会主动关闭,因为这两个时钟要维持RTC和独立看门狗的运行。
- 强制关闭电压调节器,1.8V电源直接关断,意味着内部的寄存器数据全部丢失
- 如何唤醒
- WKUP的引脚产生上升沿、
- RTC闹钟事件
- NRST引脚上的外部复位
- IWDG复位
5. 低功耗模式选择框图
执行WFI(Wait For Interrupt)或者 WFE(Wait For Event)指令后,STM32进入低功耗模式。(这两条指令为最终开启低功耗模式的触发条件)
可以看到。
一旦WFI 或者 WFE 执行了。 芯片是通过判断这些标志位来进入低功耗的三种模式的。
- SLEEPDEEP 位 决定是否进入深度睡眠。0为浅睡眠模式(睡眠模式)、1为深度睡眠模式(停机或待机)
-
在睡眠模式时
SLEEPONEXIT位的0、1标志可以决定立刻睡眠,还是等待中断事件处理结束后才睡眠。一般不用,只有在中断中调用睡眠模式,才会考虑这个标志位。
-
在深度睡眠模式时
- 判断PDDS位,为0则进入停机。为1则进入待机模式
- 停机模式下,会继续判断LPDS位,为0则开启电压调节器。为1则电压调节器开启低功耗(更省电,但唤醒时间更长)
- 判断PDDS位,为0则进入停机。为1则进入待机模式
-
使用睡眠模式时,一般是直接使用使用__WFI来配置为睡眠模式,对于刚才所说的SLEEPDEEP = 0和 SLEEPONEXIT 的位 使用默认的就可以了。所以是不需要配置的。直接输入__WFI 而停止模式和待机模式,都有对应的函数可以直接一键配置
要是真想去配置的话,输入SCB->SCR = 0x….;
就可以配置这些位了
6. 低功耗三种模式总结
6.1 睡眠模式
- 执行完WFI/WFE指令后,STM32进入睡眠模式,程序暂停运行,唤醒后程序从暂停的地方继续运行
- SLEEPONEXIT位决定STM32执行完WFI或WFE后,是立刻进入睡眠,还是等STM32从最低优先级的中断处理程序中退出时进入睡眠
- 在睡眠模式下,所有的I/O引脚都保持它们在运行模式时的状态
- WFI指令进入睡眠模式,可被任意一个NVIC响应的中断唤醒
- WFE指令进入睡眠模式,可被唤醒事件唤醒
6.2 停机模式
- 执行完WFI/WFE指令后,STM32进入停止模式,程序暂停运行,唤醒后程序从暂停的地方继续运行
- 1.8V供电区域的所有时钟都被停止,PLL、HSI和HSE被禁止,SRAM和寄存器内容被保留下来
- 在停止模式下,所有的I/O引脚都保持它们在运行模式时的状态
- 当一个中断或唤醒事件导致退出停止模式时,HSI被选为系统时钟 所以在停止模式唤醒之后,第一时间就是重启HSE,配置主频为72MHZ
- 当电压调节器处于低功耗模式下,系统从停止模式退出时,会有一段额外的启动延时
- WFI指令进入停止模式,可被任意一个EXTI中断唤醒
- WFE指令进入停止模式,可被任意一个EXTI事件唤醒
6.3 待机模式
- 执行完WFI/WFE指令后,STM32进入待机模式,唤醒后程序从头开始运行
- 整个1.8V供电区域被断电,PLL、HSI和HSE也被断电,**SRAM和寄存器内容丢失,**只有备份的寄存器和待机电路维持供电
- 在待机模式下,所有的I/O引脚变为高阻态(浮空输入)
- WKUP引脚的上升沿、RTC闹钟事件的上升沿、NRST引脚上外部复位、IWDG复位退出待机模式
- 在进入待机模式之前,一般都需要把所有的外设 都关闭,比如屏幕、电机等等。这样才能达到 最最最省电的效果
7. STM32各个状态下的电量消耗
7.1 睡眠模式
睡眠模式下,各个频率的电流 是mA级别的电流。
7.2 停机模式和待机模式
电流变为了μA级别。这是非常省电的。
对于一个300mAh的电池:睡眠(10h),停机(1w h),待机(10w h), 的使用时间能相差很大!
备份区域的供电 消耗电量也很小,仅仅1μA
8. 修改系统主频的方法
修改主频可以有效的降低功耗。并且在停机模式下唤醒,也需要修改主频到72MHZ
这个需要去system_stm32f10x.c
和system_stm32f10x.h
中查看学习
7.1 system_stm32f10x.c
中可调用的函数
-
在
system_stm32f10x.c
文件中有两个外部可调用的函数和一个外部可调用的变量.(可以在头文件中看到。)两个函数是
SystemInit()
和SystemCoreClockUpdate()
一个变量是
SystemCoreClock
-
SystemInit()
是用来配置时钟树的,他是在执行main函数之前,在启动文件中调用的。 -
SystemCoreClock
表示主频频率的值。如果我们要显示当前频率就可以直接调用显示 -
SystemCoreClockUpdate()
更新上边的SystemCoreClock
的值。
7.2 system_stm32f10x.c
中的宏定义
解除对应的注释,来选择想要的系统主频(带有钥匙的文件无法修改,需要取消文件的只读属性)
这个if和 lese的意思是:只要你不是VL(超值系列)。就可以配置那么多的主频(lese下边的)
[扩展]STM32配置系统时钟时都做了什么
系统在进入main函数之前,会由system_stm32f10x.c函数来配置系统时钟。
首先system会启动HSI内部高速时钟
然后会进行各种恢复缺省配置(比如)
最后调用SetSysClock函数(这是一个分配函数,根据我们之前解除的宏定义,来选择执行不同的配置函数。比如SetSysClockTo72、To56、To48…..)
在执行的配置函数中。才会真正的对stm32的寄存器开始配置。
比如To72的配置为:选择HSE外部告诉始终作为锁相环输入,锁相环进行9倍频 再选择锁相环输出作为主频。
9. 编写:睡眠模式+串口收发数据
9.1 为什么一定是睡眠模式呢?
- 因为在停机模式和待机模式下。会关闭1.8V区域的时钟。这就导致了USART外设不能接收数据而产生中断。 并且停机模式和待机模式只能是由外部中断唤醒的。
所以只能使用睡眠模式,因为睡眠模式仅仅是关闭了CPU的时钟。使程序不再往下运行。1.8V供电区域和电压调节器都是开着的。可以使用USART的中断进行唤醒。
9.2 代码框架介绍
- main.c 仅仅添加了一行代码:__WFI 中断睡眠。 其余的不用配置,使用默认为0的就行(睡眠模式0 + 立刻睡眠0) 另外还加入了Running来验证程序是否进入睡眠模式
- 执行流程:__WFI()后,睡眠,串口发送数据,产生中断,唤醒cpu,执行中断,执行主函数,遇到__WFI() ,睡眠,
- Serial.c 串口收发数据
- Serial.h 串口收发数据头文件
- 注意事项:如果Delay.c中的延时实现是靠SysTick滴答定时器中断实现的。这个中断也会触发唤醒操作。需要关闭SysTick的中断
main.c
#include "stm32f10x.h" // Device header
#include "led.h"
#include "delay.h"
#include "Key.h"
#include "Serial.h"
#include "OLED.h"
uint8_t RxData;
int main()
{
OLED_Init();//初始化OLED
Delay_Init();//初始化延时函数
Serial_Init();//初始化串口
OLED_ShowString(1,1,"RxData:");
while(1)
{
if(Serial_GetRxfalg() == 1)
{
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();
}
}
Serial.c
#include "Serial.h"
//初始化
void Serial_Init(void)
{
//使能A9 A10所在的GPIO
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
//使能A9 A10的复用外设时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
//配置PA9端口为复用推挽输出
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;//复用推挽模式
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; //9
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);//初始化
//这里对于F4芯片可以单独 分开的设置复用以及上下拉。不用分开配置,
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;//复用推挽模式
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; //9
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);//初始化
USART_InitTypeDef USART_InitStructure;
USART_InitStructure.USART_BaudRate = 9600;//波特率。会自动算好填入BRR寄存器
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//硬件流控制、不使用.
USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;//用| 使用两个功能
USART_InitStructure.USART_Parity = USART_Parity_No; //校验位。
USART_InitStructure.USART_StopBits = USART_StopBits_1;//停止位1位。
USART_InitStructure.USART_WordLength = USART_WordLength_8b; //不需要校验,所以字长选择8位字长。
USART_Init(USART1,&USART_InitStructure);
USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef NCIC_InitStructure;
NCIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NCIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NCIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NCIC_InitStructure.NVIC_IRQChannelSubPriority = 1;//优先级
NVIC_Init(&NCIC_InitStructure);
USART_Cmd(USART1,ENABLE);
}
//发送字节
void Serial_SendByte(uint16_t Byte)
{
USART_SendData(USART1,Byte);
while(USART_GetFlagStatus(USART1,USART_FLAG_TXE) == RESET);//如果写标志位没有置1(没有发送完),那么就等
}
//发送数组
void Serial_SendArray(uint8_t* Array,uint16_t len)
{
uint16_t i = 0;
for(i = 0; i < len; i++)
{
Serial_SendByte(Array[i]);
}
}
//发送字符串
void Serial_SendString(char* String)//有结束的\\0 所以不用再传长度了,
{
uint16_t i = 0;
for(i = 0; (String[i] != '\\0'); i++)
{
Serial_SendByte(String[i]);
}
}
//求次方函数
uint32_t Serial_Pow(uint32_t X,uint8_t Y)
{
int Num = 1;
while(Y--)
{
Num *= X;
}
return Num;
}
//发送字符形式的数字
void Serial_SendNumber(uint32_t Number,uint8_t len)
{
//从高位像低位取数字然后输出
uint8_t i = 0;
for(i = 0; i < len; i++)
{
Serial_SendByte(Number / Serial_Pow(10,len - i - 1) %10 + '0');
}
}
//下边没学过,直接搬过来的。
/**
* 函 数:使用printf需要重定向的底层函数
* 参 数:保持原始格式即可,无需变动
* 返 回 值:保持原始格式即可,无需变动
*/
int fputc(int ch, FILE *f)
{
Serial_SendByte(ch); //将printf的底层重定向到自己的发送字节函数
return ch;
}
/**
* 函 数:自己封装的prinf函数
* 参 数:format 格式化字符串
* 参 数:... 可变的参数列表
* 返 回 值:无
*/
void Serial_Printf(char *format, ...)
{
char String[100]; //定义字符数组
va_list arg; //定义可变参数列表数据类型的变量arg
va_start(arg, format); //从format开始,接收参数列表到arg变量
vsprintf(String, format, arg); //使用vsprintf打印格式化字符串和参数列表到字符数组中
va_end(arg); //结束变量arg
Serial_SendString(String); //串口发送字符数组(字符串)
}
uint8_t Serial_RxData;
uint8_t Serial_RxFlag;
uint8_t Serial_GetRxfalg(void)
{
if(Serial_RxFlag == 1)
{
Serial_RxFlag = 0;
return 1;
}
return 0;
}
uint8_t Serial_GetRxData(void)
{
return Serial_RxData;
}
void USART1_IRQHandler(void)//中断
{
if(USART_GetFlagStatus(USART1,USART_FLAG_RXNE) == SET)
{
Serial_RxData = USART_ReceiveData(USART1);//读取
Serial_RxFlag = 1;
USART_ClearITPendingBit(USART1,USART_FLAG_RXNE);//手动清除标志位
}
}
Serial.h
#ifndef __SERIAL_H
#include "stm32f10x.h"
#include "stdio.h"
#include "stdarg.h"
#define __SERIAL_H
//初始化串口
void Serial_Init(void);
//发送字节
void Serial_SendByte(uint16_t Byte);
//发送数组
void Serial_SendArray(uint8_t* Array,uint16_t len);
//发送字符串
void Serial_SendString(char* String);
//发送字符形式的数字
void Serial_SendNumber(uint32_t Number,uint8_t len);
//移植printf
void Serial_Printf(char *format, ...);
//获取输入的RXNE标志位
uint8_t Serial_GetRxfalg(void);
//获取输入的字符
uint8_t Serial_GetRxData(void);
#endif
10. PWR.h 电源控制函数介绍
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);
- 进入停止模式
- 指定电压调节器在停止模式中的状态、使用WFI或WFI指令进入
void PWR_EnterSTANDBYMode(void);
- 进入待机模式
- 使用WFI或WFI指令进入
FlagStatus PWR_GetFlagStatus(uint32_t PWR_FLAG);
- 获取标志位
void PWR_ClearFlag(uint32_t PWR_FLAG);
- 清除标志位
11. 编写:停止模式+外部中断计次
11.1 为什么是停止模式呢?
- 刚才我们已经验证过睡眠模式了。而停止模式只能由外部中断来触发。所以只能选择外部中断类型的代码来验证
- 在停止模式下, 1.8V的区域的时钟关闭,CPU和外设都没有时钟了。
- 但是外部中断的工作是不需要时钟过的
11.2 注意事项
-
我们需要使用PWR外设,来操作进入停止模式、待机模式。它在APB1总线上
-
在停止模式被唤醒之后,会默认使用内部高速时钟HSI。
需要手动调整时钟为外部高速HSE。如果不设置,在退出停止模式之后程序运行速度会变慢。重新启动主频函数为:
SystemInit();
11.3 代码框架介绍
- main.c 测试停止模式工作,使用running闪烁来判断是否停止
- 执行流程:复位后初始化、进入主循环、进入停止模式、外部中断发生时,唤醒停止模式(此时主频被设置为HSI内部高速时钟)、进入中断执行中断内容、继续从停止位置继续执行。设置主频为72MHZ、继续执行后续函数、然后进入停止
- CountSensor.c 外部中断计次
- CountSensor.h 外部中断计次头文件
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "oled.h"
#include "Delay.h"
#include "CountSensor.h"
int main()
{
OLED_Init();//初始化OLED;
CountSensor_Init();//初始化
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR,ENABLE);//开启PWR外设
OLED_ShowString(1,1,"Count:");
while(1)
{
OLED_ShowSignedNum(1, 7,CountSenSor_Get(),5);
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();//重新启动主频
}
}
CountSensor.c
#include "stm32f10x.h" // Device header
//引脚为A2
//计次变量
uint16_t CountSensor_Count = 0;
//初始化
void CountSensor_Init(void)
{
//开启A2的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
//开启挂载在APB2外设的AFIO外设时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);
//EXTI时钟和NVIC的时钟不用手动打开。
//配置GPIO
GPIO_InitTypeDef GPIO_InitStructrue;
GPIO_InitStructrue.GPIO_Mode = GPIO_Mode_IPU; //上拉输入模式
GPIO_InitStructrue.GPIO_Pin = GPIO_Pin_2;
GPIO_InitStructrue.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructrue);
//配置AFIO (F1中在GPIO.h文件中)(目的是为了把GPIOA的PIN2引脚映射到AFIO中。)
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA,GPIO_PinSource2);//这里需要根据PIn和GPIOx来选择
//配置EXTI
EXTI_InitTypeDef EXTI_InitStructure;
EXTI_InitStructure.EXTI_Line = EXTI_Line2; //选择中断线路,这里是PIN2 所以为2
EXTI_InitStructure.EXTI_LineCmd = ENABLE; //是否使能指定的中断线路
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; //中断或响应模式
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;//上升或下降或边沿触发
EXTI_Init(&EXTI_InitStructure);
//配置NVIC(因为NVIC属于内核,所以被分配到内核的杂项中去了,在misc.c)
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//配置抢占和响应优先级
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = EXTI2_IRQn; //在stm32f10x.h文件里。让你找IRQn_Type里的一个中断通道。这里使用的是md的芯片(如果引脚是15-10或者9-5则需要去找对应的那个)这里我是PIN2.所以找2
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //是否使能指定的中断通道
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;//抢占优先级(这里可以看表。看范围,)
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; //指定响应优先级
NVIC_Init(&NVIC_InitStructure);
}
//获取当前的计数
uint16_t CountSenSor_Get(void)
{
return CountSensor_Count;
}
//在STM32中,中断的函数都是固定的。他们在启动文件中存放xxx.s
//以IRQHandler结尾的就是中断函数的名字。
//在这里需要找到对应的中断函数,我这里是2
void EXTI2_IRQHandler(void)//中断函数是无参无返回值的。中断函数必须写对,写错就进不去
{
//在进入中断后,一般要判断一下这个是不是我们想要的那个中断源触发的中断。
//但是在这里。我是GPIOA的PIN2引脚,所以不用写。
//如果是5-9 10-15的引脚。他们EXTI到NVIC是几个共用的。
//所以需要根据EXTI输入时的16根引脚。来判断是16根引脚的那一根发送的中断请求。
//这里规范写的话需要加上去
//查找标志位函数在exit.h中。
if(EXTI_GetITStatus(EXTI_Line2) == SET)//第一个参数是行数.判断这个线的标志位是不是== SET。是则是我们想要的
{
CountSensor_Count++;
EXTI_ClearITPendingBit(EXTI_Line2);//中断结束后,要调用清除标志位的函数。如果你不清除,程序会一直进入中断
}
}
CountSensor.h
#ifndef __COUNTSENSOR_H
#define __COUNTSENSOR_H
//初始化旋转编码计次
void CountSensor_Init(void);
//获取当前计数值
uint16_t CountSenSor_Get(void);
#endif
12. 编写:待机模式+实时时钟
12.1 为什么是待机模式呢?
- 刚才我们已经验证过睡眠模式、停止模式了。 待机模式只能由WKUP引脚的上升沿、RTC闹钟事件的上升沿、NRST引脚上外部复位、IWDG复位来退出待机模式。
- 在待机模式恢复后,会重头开始执行命令。所以不需要配置时钟了。
- 在待机模式下, 1.8V的区域的时钟关闭,电压调节器也关闭。这会使除了备份区域外的所有寄存器清除数据,并且IO口对外呈现为高阻态。
12.2 注意事项
- 在进入待机模式之前,一般都需要把所有的外设 都关闭,比如屏幕、电机等等。 否则就无法极度省电
- 待机模式会重头开始执行命令,所以不需要再配置时钟了。
- 待机模式烧录代码可以按住复位再烧录 (我这里不行,我会断电重上电,在未进入待机模式前开始烧录代码)
- 测试WKUP引脚时,只需要函数:
PWR_WakeUpPinCmd(ENABLE);
就可以了。程序会自动帮我们把WKUP(PA0)引脚设置为下拉输入的配置,不需要GPIO初始化。
12.3 代码框架介绍
- main.c 测试停止模式工作,使用running闪烁来判断是否进入待机模式
- 执行流程:复位后初始化、进入主循环、进入待机模式(所有IO口浮空、寄存器重置、CPU核心停止。但是后备区域不会受到影响)、唤醒待机模式、程序重头开始运行。然后再次进入待机模式
- MyRTC.c RTC实时时钟
- MyRTC.h RTC实时时钟头文件
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "MyRTC.h"
int main(void)
{
/*模块初始化*/
OLED_Init(); //OLED初始化
MyRTC_Init(); //RTC初始化
/*显示静态字符串*/
OLED_ShowString(1, 1, "CNT:"); //时间戳计数器
OLED_ShowString(2, 1, "ALR:"); //闹钟值
OLED_ShowString(3, 1, "ALRF:"); //闹钟标志位
PWR_WakeUpPinCmd(ENABLE);
//存下要显示的闹钟值(闹钟寄存器只可写不可读,所以要存下,方便显示)
uint32_t Alarm = RTC_GetCounter() + 10;
//设定闹钟为十秒后
RTC_SetAlarm(Alarm);
//显示闹钟
OLED_ShowNum(2, 5, Alarm, 10);
//开启PWR时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
while (1)
{
OLED_ShowNum(1, 5, RTC_GetCounter(), 10); //显示32位的秒计数器
OLED_ShowNum(3, 6, RTC_GetFlagStatus(RTC_FLAG_ALR), 1); //显示32位的秒计数器
OLED_ShowString(4,1,"Running");
Delay_ms(100);
OLED_ShowString(4,1," ");
Delay_ms(100);
//在进入待机模式之前,一般都需要把所有的外设 都关闭,比如屏幕、电机等等。
//这里清个屏表示一下
OLED_Clear();
PWR_EnterSTANDBYMode();//进入待机模式
}
}
MyRTC.c
#include "stm32f10x.h" // Device header
#include <time.h>
uint16_t MyRTC_Time[] = {2024, 8, 18, 23, 46, 0}; //定义全局的时间数组,数组内容分别为年、月、日、时、分、秒
void MyRTC_SetTime(void); //函数声明
/**
* 函 数:RTC初始化
* 参 数:无
* 返 回 值:无
*/
void MyRTC_Init(void)
{
/*开启时钟*/
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE); //开启PWR的时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE); //开启BKP的时钟
/*备份寄存器、RTC访问使能*/
PWR_BackupAccessCmd(ENABLE); //使用PWR开启对备份寄存器和RTC的访问
if (BKP_ReadBackupRegister(BKP_DR1) != 0xA5A5) //通过写入备份寄存器的标志位,判断RTC是否是第一次配置
//if成立则执行第一次的RTC初始化
{
RCC_LSEConfig(RCC_LSE_ON); //开启LSE时钟
while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) != SET); //等待LSE准备就绪
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); //选择RTCCLK来源为LSE
RCC_RTCCLKCmd(ENABLE); //RTCCLK使能
RTC_WaitForSynchro(); //等待同步
RTC_WaitForLastTask(); //等待上一次操作完成
RTC_SetPrescaler(32768 - 1); //设置RTC预分频器,预分频后的计数频率为1Hz
RTC_WaitForLastTask(); //等待上一次操作完成
MyRTC_SetTime(); //设置时间,调用此函数,全局数组里时间值刷新到RTC硬件电路
BKP_WriteBackupRegister(BKP_DR1, 0xA5A5); //在备份寄存器写入自己规定的标志位,用于判断RTC是不是第一次执行配置
}
else //RTC不是第一次配置
{
RTC_WaitForSynchro(); //等待同步
RTC_WaitForLastTask(); //等待上一次操作完成
}
}
/**
* 函 数:RTC设置时间
* 参 数:无
* 返 回 值:无
* 说 明:调用此函数后,全局数组里时间值将刷新到RTC硬件电路
*/
void MyRTC_SetTime(void)
{
time_t time_cnt = 0; //定义秒计数器数据类型
struct tm time_date; //定义日期时间数据类型
time_date.tm_year = MyRTC_Time[0] - 1900; //将数组的时间赋值给日期时间结构体
time_date.tm_mon = MyRTC_Time[1] - 1;
time_date.tm_mday = MyRTC_Time[2];
time_date.tm_hour = MyRTC_Time[3];
time_date.tm_min = MyRTC_Time[4];
time_date.tm_sec = MyRTC_Time[5];
time_cnt = mktime(&time_date) - 8 * 60 * 60; //调用mktime函数,将日期时间转换为秒计数器格式
//- 8 * 60 * 60为东八区的时区调整
RTC_SetCounter(time_cnt); //将秒计数器写入到RTC的CNT中
RTC_WaitForLastTask(); //等待上一次操作完成
}
/**
* 函 数:RTC读取时间
* 参 数:无
* 返 回 值:无
* 说 明:调用此函数后,RTC硬件电路里时间值将刷新到全局数组
*/
void MyRTC_ReadTime(void)
{
time_t time_cnt; //定义秒计数器数据类型
struct tm time_date; //定义日期时间数据类型
time_cnt = RTC_GetCounter() + 8 * 60 * 60; //读取RTC的CNT,获取当前的秒计数器
//+ 8 * 60 * 60为东八区的时区调整
time_date = *localtime(&time_cnt); //使用localtime函数,将秒计数器转换为日期时间格式
MyRTC_Time[0] = time_date.tm_year + 1900; //将日期时间结构体赋值给数组的时间
MyRTC_Time[1] = time_date.tm_mon + 1;
MyRTC_Time[2] = time_date.tm_mday;
MyRTC_Time[3] = time_date.tm_hour;
MyRTC_Time[4] = time_date.tm_min;
MyRTC_Time[5] = time_date.tm_sec;
}
MyRTC.h
#ifndef __MYRTC_H
#define __MYRTC_H
extern uint16_t MyRTC_Time[];
void MyRTC_Init(void);
void MyRTC_SetTime(void);
void MyRTC_ReadTime(void);
#endif