上次我们完成了停止模式的学习,这次我们完成低功耗的最后一步,待机模式学习。
一.准备
1.编程准备

首先复制一下我们的RTC实时时钟的代码。

起个新名字。

编译一下,方便我们的代码提示。

因为目前我们OLED显示屏上面没有显示位置了,所以我们修改一下显示。
第一条我们显示CNT也就是秒计数器。

第二行显示我们的ALR。这个是闹钟值。

第三行显示我们的ALRF。这个是闹钟标志位。

第四行删除,这就是完整的显示。

年月日时分秒,也全部删除。

然后CNT的位置放在一行六列,DIV也清除不用。

我们编译下载一下。

按住复位键下载,之后我们可以看到,第一行显示秒数,第二行,第三行没有显示。
二.程序编写
1.设置闹钟
我们可以在while循环上面设定,在每次复位之后设置闹钟值,闹钟值也不用特意去记录。我们直接设置为秒数+10。这样每次复位后闹钟就是10S后了。

我们需要用到这个函数来进行闹钟设置。

闹钟值也是一个32位的数值。

我们直接给一个RTC_GetCounter()就是当前的秒数+10就是闹钟值。之后在下面,我们还计划显示一下这个闹钟值,但是这个设置闹钟值的寄存器是只写的,写进去之后就读取不出来了,怎么判断他是只读的还是只写的呢。

我们可以看一下手册的闹钟寄存器。有一排字母W,这就是对应位的读写特征。

我们看一下CNT计数器可以看到r/w这钟就是读写的。

像是这种余数寄存器只写了一个r就是只读不可写的寄存器。

关于读写的字母可以参考一下这个表格。

所以我们可以定义这一个变量,然后存起来。

然后我们把这个值写入到设置闹钟值当中。

然后调用OLED函数显示一下。
之后随着NT增大会和ALR相等,然后触发闹钟标志位置1。如果开启了闹钟中断还会进一步进入到中断函数里面。

然后我们可以在显示一下,不断地获取闹钟的标志位,看看闹钟响了吗。

我们最后加一个Running显示。
2.小测试

我们编译下载一下。


然后我们可以看到,第一行是当前秒数,第二行是闹钟值,第三行是闹钟标志位,等记录到时间后,闹钟标志位置1。
3.加入待机模式

我们依旧在开头进行PWR时钟开启,因为要用到PWR外设,就必须先开启PWR的时钟。

之后再主循环的最后调用待机模式。

之后我们来进入函数里面,看看函数干了什么活。

第一步就是清除WeakUp标志位。
第二步选择STANDBY模式。实际上就是把PDDS置1。PDDS为1,表示进入待机模式。兄弟们一定要注意这里,如果你用的固件库是比较新的版本,里面可能会没有清除唤醒标志位的操作,这会导致你调用这个函数后只有第一次是运行正常的。
第三步是SLEEPDEEP置1,进入深度睡眠。
最后一步,调用WFI指令。进入待机模式。可以看到其实待机模式,并没有区分,WFI和WFE,库函数这里也不能选择。统一都调用的是WFI。这一部分也不难理解,WFI和WFE的区别就是唤醒方式的不同。

对于待机模式,唤醒条件就是指定的4个信号。没有对中断唤醒和事件唤醒做出区分。所以我们也就不用进行区分了。
4.测试

接下来,进行测试。

原始样子,之后我们进行复位

复位之后,Running闪烁一次之后,进入了待机模式。当然这时CNT也不会刷新了。

然后等一等,闹钟触发,待机模式唤醒。CNT和闹钟值刷新一下。running闪烁一次。这就是目前程序的现象。并且每次唤醒之后,闹钟值就重新设定。通过这一现象,我们可以确定,待机模式唤醒后,程序是从头开始执行的。因为我们闹钟设定的代码再while循环之前。如果程序不是从头开始的,那么闹钟值也不会更新。然后在进入待机模式之后高速时钟也都会关闭。在退出待机模式时候,程序从开头开始执行。再程序刚开始时候自动调用Systeminit初始化时钟。所以待机模式就不用像停止模式一样,自己调用systeminit函数了。另外在执行完这个函数进入待机模式之后,这个函数之后的代码。

就再也执行不到了。因为退出待机模式,程序是从头开始的。并且这个while循环,也是执行一次的,所以也可以去掉while循环,程序照常运行。
再STM32进入待机模式之前,一定要把能关的模块全部关掉,能断电都断掉。这样才能最大程度上的省电。这部分需要精心设计电路,可以使用一个带有使能端的稳压器来实现。


所以我们加一个清屏,来清理一下。之后我们再加一个字符串

之后显示时间给到1S。

编译下载。

可以看到OLED是熄灭的。

然后按一下复位。然后显示一些信息,之后停留1S息屏。

之后闹钟触发后,唤醒一次。

然后再次进入息屏,这就是我们的待机模式程序现象。
5.wakeup唤醒

我们可以使用这个函数来进行wakeup的唤醒。

程序很简单,就这一条代码就可以了。可能会有疑问,目前用到了GPIO的引脚,需不需要进行GPIO的初始化呢,答案是不需要

我们看一下手册

就可以看到。

编译下载。


可以看到OLED是熄灭的,wakeup引脚默认是下拉的引脚悬空就是低电平。

我们把线插到高电平试一下。可以看到WakeUp接高电平时刻,待机模式被唤醒了。
6.小实验
我们把板子背面的LDO稳压器和灯去掉,之后在进行电流测量,看看是不是如手册说的一样省电。因为LDO默认输出端会有假负载,一般是500R~1K之间,会造成损耗。

我们先把程序显示时间给到10S。

待机闹钟给到100S。

编译下载,记得按住复位。

然后万用表放在200mA挡位,可以看到,这是正常工作的电流。目前是20多毫安,之后复位一下。

然后我们把挡位放在200uA档,可以看到目前待机的供电电流是3.3uA这就和手册上的数据基本吻合了。
三.所有程序
1.main.c
cs
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "MyRTC.h"
#include "OLED.h"
//uint8_t Keynum;
int main(void)
{
OLED_Init();
MyRTC_Init();
//开启PWR的时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
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_GetCounter()就是当前的秒数+10就是闹钟值。
RTC_SetAlarm(Alarm);
OLED_ShowNum(2, 6, Alarm, 10);
while(1)
{
OLED_ShowNum(1, 6, RTC_GetCounter(), 10);
//不断获取RTC的ALR闹钟标志位。
OLED_ShowNum(3, 6, RTC_GetFlagStatus(RTC_FLAG_ALR), 1);
//running指示
OLED_ShowString(4, 1, "Running");
Delay_ms(100);
OLED_ShowString(4, 1, " ");
Delay_ms(100);
OLED_ShowString(4, 9, "STANDBY");
Delay_ms(1000);
OLED_ShowString(4, 9, " ");
Delay_ms(1000);
OLED_Clear();
//开启待机模式
PWR_EnterSTANDBYMode();
}
}
2.MyRTC.c
cs
#include "stm32f10x.h" // Device header
#include <time.h>
uint16_t MyRTC_Time[] = {2023, 1, 1, 23, 59, 55};
void MyRTC_SetTime(void);
//初始化RTC
void MyRTC_Init(void)
{
//开启PWR和BKP时钟,使能BKP和RTC的访问
RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
PWR_BackupAccessCmd(ENABLE);
if(BKP_ReadBackupRegister(BKP_DR1) != 0xA5A5)
{
//开启LSE时钟,并等待LSE时钟启动完成
RCC_LSEConfig(RCC_LSE_ON);
//等待LSE启动完成。
while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET);
//选择RTCCLK时钟源。
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
//使能时钟
RCC_RTCCLKCmd(ENABLE);
//等待同步
RTC_WaitForSynchro();
//等待上一次操作完成
RTC_WaitForLastTask();
//配置预分频器
RTC_SetPrescaler(32768 - 1);
//等待写操作完成
RTC_WaitForLastTask();
//设置初始时间
RTC_SetCounter(1672588795);
//等待上一次操作完成
RTC_WaitForLastTask();
//初始化设置时间
MyRTC_SetTime();
//写标志位
BKP_WriteBackupRegister(BKP_DR1, 0xA5A5);
}
else
{
//等待同步
RTC_WaitForSynchro();
//等待上一次操作完成
RTC_WaitForLastTask();
}
}
//数组时间转化为秒数写道CNT中
void MyRTC_SetTime(void)
{
//定义变量
time_t time_cnt;
struct tm time_data;
time_data.tm_year = MyRTC_Time[0] - 1900;//念减去一个偏移因为是1900年开始
time_data.tm_mon = MyRTC_Time[1] - 1;
time_data.tm_mday = MyRTC_Time[2];
time_data.tm_hour = MyRTC_Time[3];
time_data.tm_min = MyRTC_Time[4];
time_data.tm_sec = MyRTC_Time[5];
//调用函数 参数是struct tm*,所以我们给地址
//返回值给cnt
//日期时间到秒数的转换
time_cnt = mktime(&time_data) - 8*60*60;
//把指定的秒数写道CNT里面
RTC_SetCounter(time_cnt);
//等待上一次操作完成
RTC_WaitForLastTask();
}
//读取时间函数
void MyRTC_ReadTime(void)
{
//定义变量
time_t time_cnt;
struct tm time_data;
//读取CNT秒数
time_cnt = RTC_GetCounter() + 8*60*60;
//使用localtime函数得到日期时间
//参数是COUNT time_t,所以把地址传进去
//返回值是struct tm*
time_data = *localtime(&time_cnt);
//传给数组
MyRTC_Time[0] = time_data.tm_year + 1900;
MyRTC_Time[1] = time_data.tm_mon + 1;
MyRTC_Time[2] = time_data.tm_mday;
MyRTC_Time[3] = time_data.tm_hour;
MyRTC_Time[4] = time_data.tm_min;
MyRTC_Time[5] = time_data.tm_sec;
}
3.MyRTC.h
cs
#ifndef __MYRTC_H
#define __MYRTC_H
extern uint16_t MyRTC_Time[];
void MyRTC_Init(void);
void MyRTC_ReadTime(void);
void MyRTC_SetTime(void);
#endif