代码示例:
接线图:修改主频
接线图:睡眠模式+串口发送+接收
CH340 USB转串口模块。GND和stm32共地。RXD接PA9,TXD接PA10。
接线图:停止模式+对射式红外传感器计次
对射式红外传感器模块的VCC和GND接上供电。DO输出接STM32的PB14引脚。
接线图:待机模式+实时时钟
PA0引出一根线,目的是,可以手动把PA0接到3.3v或断开。因为PA0有个WKUP,可以唤醒待机模式,WKUP引脚上升沿有效。直接接一个线短接到3.3v,来手动产生一个上升沿,测试WKUP的效果。
研究system_stm32f10x.h、system_stm32f10x.c文件:
system_stm32f10x.h、system_stm32f10x.c文件是用来配置系统时钟的,也就是配置RCC时钟树。
RCC时钟树
在这里这个图就是RCC时钟树的全部电路,左边是四个时钟源,HSI、HSE、LSE、LSI用于提供时钟,右边就是各个外设,就是使用时钟的地方,我们用的最多的就是AHB时钟、APB1时钟、APP2时钟。另外还有一些时钟,它们的来源不是AHB和APB,比如I2S的时钟直接来源于system clock,USB的时钟直接来源于PLL。
我们主要关心的就是这个外部的8MHz晶振,它如何进行选择,如何倍频才能得到这个72MHz的SYSCLK系统主频,然后系统主频如何去分配,才能得到AHB、APB1和APB2的时钟频率,
#if叫做预编译,主要是用来兼容不同型号的设备 。修改主频,把宏解除注释(/*)。
当前带钥匙符号代表文件只读,解除只读的方法:打开工程文件夹或在system_stm32f10x.c右键,点打开包含的文件夹
或
右键,属性
把只读的√去掉。
如想整个文件或整个工程文件夹都取消只读,在外面对整个文件夹操作,点击属性,把只读去掉即可。
总结:首先是是Systemlnit函数,进来启动HSI,之后就是各种缺省配置,最后调用SetSysClock;SetSysClock是一个分配函数,根据前面解除不同的宏定义,选择执行不同的配置函数:如SetSysClockTo72、To56、To48等。最后在SetSysClockTo72里配置函数,如配置是:选择HSE作为锁相环输入,之后锁相环进行9倍频,再选择锁相环输出,作为主频,这样主频就是72MHZ。
修改主频
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"int main(void)
{OLED_Init(); //OLED初始化OLED_ShowString(1, 1, "SYSCLK:"); //显示静态字符串OLED_ShowNum(1, 8, SystemCoreClock, 8); //显示SystemCoreClock变量//SystemCoreClock的值表示当前的系统主频频率while (1){OLED_ShowString(2, 1, "Running"); //闪烁Running,指示当前主循环运行的快慢Delay_ms(500);OLED_ShowString(2, 1, " ");Delay_ms(500);}
}
程序现象:
第一行主频频率是36M。第二行Running闪烁周期是2s。
睡眠模式
main.c
#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) //检查串口接收数据的标志位{RxData = Serial_GetRxData(); //获取串口接收的数据Serial_SendByte(RxData); //串口将收到的数据回传回去,用于测试OLED_ShowHexNum(1, 8, RxData, 2); //显示串口接收的数据}OLED_ShowString(2, 1, "Running"); //OLED闪烁Running,指示当前主循环正在运行Delay_ms(100);OLED_ShowString(2, 1, " ");Delay_ms(100);__WFI(); //执行WFI指令,CPU睡眠,并等待中断唤醒//执行WFI这时CPU会立刻睡眠,程序就停在了WFI指令这里,但是各个 外设比如USRT还是工作状态//等到我们用串口助手发送数据时,USRT外设收到数据产生中断,唤醒CPU之后程序在暂停的地方继续运行}
}
程序现象
串口助手发送数据,每发送一个Running闪烁一次,接收区回传一次数据,说明程序在收到数据之后,可以唤醒工作一次。在不影响功能的前提下,CPU能在空闲时睡眠,节约用电。
停止模式
停止模式只能通过外部中断触发(唤醒),所以和停止模式相关的代码肯定得用外部中断。
在停止模式下1.8V区域的时钟关闭,CPU和外设都没有时钟了,但是外部中断的工作是不需要时钟的,这一点从代码里也可以看出来,初始化的时候,根本就没有开启EXTI时钟的参数,这也是EXTI能在时钟关闭的情况下工作的原因。
刚才讲的睡眠模式其实都只是内核的操作,睡眠模式涉及的几个寄存器也都是在内核里,跟PWR外设关系不大,所以刚才我们都没用到PWR的库函数,不过现在停止模式涉及到内核之外的电路操作,这就需要用到PWR外设了,看一下库函数。
PWR.h里常用函数
void PWR_DeInit(void);//恢复缺省配置
void PWR_BackupAccessCmd(FunctionalState NewState); // 使能后备区域的访问// 配置PVD使能电压
void PWR_PVDCmd(FunctionalState NewState); //使能PVD功能
void PWR_PVDLevelConfig(uint32_t PWR_PVDLevel);//配置PVD的阈值电压// 使能WKUP引脚唤醒功能在待机模式下使用
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); //清除标志位
main .c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "CountSensor.h"int main(void)
{/*模块初始化*/OLED_Init(); //OLED初始化CountSensor_Init(); //计数传感器初始化/*开启时钟*/RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE); //开启PWR的时钟//停止模式和待机模式一定要记得开启/*显示静态字符串*/OLED_ShowString(1, 1, "Count:");while (1){OLED_ShowNum(1, 7, CountSensor_Get(), 5); //OLED不断刷新显示CountSensor_Get的返回值OLED_ShowString(2, 1, "Running"); //OLED闪烁Running,指示当前主循环正在运行Delay_ms(100);OLED_ShowString(2, 1, " "); //把Running清除Delay_ms(100);PWR_EnterSTOPMode(PWR_Regulator_ON, PWR_STOPEntry_WFI); //STM32进入停止模式,并等待中断唤醒SystemInit(); //唤醒后,要重新配置时钟。重新启动SHE,配置72MHZ}
}
程序现象:
在空闲时,Running没有闪烁。每遮挡一次,Running闪烁一下。而复位后第一次的Running闪烁很快,是正常的时间,而遮挡之后,Running闪烁变慢。
退出停止模式时,HSI被选为系统时钟,也就是在我们首次复位后,SystemInit函数里配置的是HSE*9倍频的72M主频。 所以复位后第一次Running闪烁很快,而之后进入停止模式,再退出时默认时钟就变成HSI了,HSI是8M,所以唤醒之后的程序运行就会明显变慢。
待机模式
主要任务是,第一,设置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初始化/*开启时钟*/RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE); //开启PWR的时钟//停止模式和待机模式一定要记得开启 虽然MyRTC_Init里开启了,多次开启无所谓,防止其他没调用MyRTC_Init的场景 但时钟没开启外设就不会工作。/*显示静态字符串*/OLED_ShowString(1, 1, "CNT :"); //显示秒计数器OLED_ShowString(2, 1, "ALR :"); //显示闹钟值OLED_ShowString(3, 1, "ALRF:"); //闹钟标志位/*使能WKUP引脚*/PWR_WakeUpPinCmd(ENABLE); //使能位于PA0的WKUP引脚,WKUP引脚上升沿唤醒待机模式/*设定闹钟*/uint32_t Alarm = RTC_GetCounter() + 10; //闹钟为唤醒后当前时间的后10sRTC_SetAlarm(Alarm); //写入闹钟值到RTC的ALR寄存器 ,这个寄存器只写不可读,所以使用变量Alarm显示到OLED上OLED_ShowNum(2, 6, Alarm, 10); //显示闹钟值while (1){OLED_ShowNum(1, 6, RTC_GetCounter(), 10); //显示32位的秒计数器OLED_ShowNum(3, 6, RTC_GetFlagStatus(RTC_FLAG_ALR), 1); //显示闹钟标志位,显示长度为1OLED_ShowString(4, 1, "Running"); //OLED闪烁Running,指示当前主循环正在运行Delay_ms(100);OLED_ShowString(4, 1, " "); //Running清除Delay_ms(100);OLED_ShowString(4, 9, "STANDBY"); //OLED闪烁STANDBY,指示即将进入待机模式Delay_ms(1000);OLED_ShowString(4, 9, " ");Delay_ms(100);OLED_Clear(); //OLED清屏,模拟关闭外部所有的耗电设备,以达到极度省电PWR_EnterSTANDBYMode(); //STM32进入停止模式,并等待指定的唤醒事件(WKUP上升沿或RTC闹钟)/*待机模式唤醒后,程序会重头开始运行*/} //待机模式之后的代码执行不到,下次继续从头开始 在程序刚开始的时候自动调用SystemInit初始化时钟,所以待机模式我们就不用像停止模式那样,自己调用SystemInit了//并且这个while循环,实际上也只有执行一遍的机会,把这个while循环去掉也是可以的
}
程序现象:
复位后,Running闪烁一次,进入待机,当然CNT也不会进行刷新了,然后需等一下,闹钟触发,待机模式唤醒 ,CNT和闹钟值刷新一下,Running闪烁一次。并且,每次唤醒后,闹钟值都会重新设定,通过这一现象,确定待机模式唤醒后,程序是从头开始执行。stm32进入待机模式之前,要把外部接的模块全部关掉。
最开始显示信息,进入待机,OLED清屏,等待一会,闹钟触发之后,唤醒一次。
还有wkup引脚的唤醒功能:wkup引脚默认下拉,引脚悬空,就是低电平,把线插到高电平,待机模式唤醒。
小实验:验证待机模式省电
手册说待机模式的电流只有3微安左右,为此进行了测试,
-
这里我单独找了个stm32板子,把这个电源供电线正极给剪断,串联一个万用表测电流。
当然最开始我直接测试哈,待机模式的电流高达一点多毫安,远超手册里说的3微安,但首先很明显这个电源指示灯肯定是耗电的,所以我先把这个电源指示灯去掉了,再测试电流仍然有几百微安。那说明还有别的东西耗电,之后我就把板子背面的这个LDO稳压器去掉了,这个LDO虽然我们并没有用到哈,但接上它就会有电流损耗,最后去掉电源指示灯和LDO之后,待机模式的电流就下降到3微安了。
首先,万用表接到200mA电流档,最开始看到正常工作的电流
之后等一会,进入待机模式,这个供电电流瞬间变为0,万用表换到200微安档,可以看到,目前待机的供电电流是3.3微安。