目录
STM32 PWR电源控制 与 低功耗模式 详解
1. PWR 电源控制 简介
2. PWR 电源控制 框图
3. 上电复位和掉电复位 与 可编程电压检测器(PVD)
3.1 内嵌复位与电源控制模块特性图
3.2 上电复位和掉电复位
3.3 可编程电压检测器(PVD)
3.4 总结来说可以看这个图来理解。
4. 低功耗模式介绍
睡眠模式简介
停机模式简介
待机模式简介
5. 低功耗模式选择框图
6. 低功耗三种模式总结
6.1 睡眠模式
6.2 停机模式
6.3 待机模式
7. STM32各个状态下的电量消耗
7.1 睡眠模式
7.2 停机模式和待机模式
8. 修改系统主频的方法
7.1 system_stm32f10x.c中可调用的函数
7.2 system_stm32f10x.c中的宏定义
[扩展]STM32配置系统时钟时都做了什么
9. 编写:睡眠模式+串口收发数据
9.1 为什么一定是睡眠模式呢?
9.2 代码框架介绍
main.c
Serial.c
Serial.h
10. PWR.h 电源控制函数介绍
11. 编写:停止模式+外部中断计次
11.1 为什么是停止模式呢?
11.2 注意事项
11.3 代码框架介绍
main.c
CountSensor.c
CountSensor.h
12. 编写:待机模式+实时时钟
12.1 为什么是待机模式呢?
12.2 注意事项
12.3 代码框架介绍
main.c
MyRTC.c
MyRTC.h
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