目前我们要实现的就是简单的读写BKP的功能,所以大体的步骤就是先初始化,然后写DR再读DR,看看写进去和读出来的是否一样。
由于有关BKP的代码很少,所以这里就暂时不对BKP进行封装了,直接在主函数里演示一下他的功能
主要步骤如下:
1设置RCC_APB1ENR的PWREN和BKPEN,使能PWR和BKP时钟
2设置PWR_CRd的DBP,使能BKP和RTC的访问
之后,写入数据的话BKP有写入的函数,读取数据也有个BKP读取函数
接下来看看BKP相关的库函数:
void BKP_DeInit(void);//恢复缺省配置,可以手动清零BKP所有的数据寄存器,也可以手动把数据寄存器都写入0
void BKP_TamperPinLevelConfig(uint16_t BKP_TamperPinLevel);//这两个函数用于配置TAMPER侵入检测功能,可以配置TAMPER引脚的有效电平,就是选择高电平触发还是低电平触发
void BKP_TamperPinCmd(FunctionalState NewState);//是否开启侵入检测功能,需要侵入检测的话,先配置TAMPER引脚有效电平再用该函数使能就好了
void BKP_ITConfig(FunctionalState NewState);//中断配置,选择是否开启中断
void BKP_RTCOutputConfig(uint16_t BKP_RTCOutputSource);//时钟输出功能的配置,可以选择在RTC引脚上输出时钟信号,输出RTC校准时钟,RTC闹钟脉冲或者秒脉冲
void BKP_SetRTCCalibrationValue(uint8_t CalibrationValue);//设置RTC校准值,就是写入RTC校准寄存器
void BKP_WriteBackupRegister(uint16_t BKP_DR, uint16_t Data);//写备份寄存器,第一个参数指定要写在哪个DR里,第二个参数就是要写入的数据了
uint16_t BKP_ReadBackupRegister(uint16_t BKP_DR);//读备份寄存器,参数选择要读取的DR
void PWR_BackupAccessCmd(FunctionalState NewState);//备份寄存器访问使能,设置PWR_CR寄存器里的DBP位,对应了前面主要步骤里的第二步
接下来可以开始写代码了:
#include “stm32f10x.h” // Device header
#include “Delay.h”
#include “OLED.h”
int main(void)
{
OLED_Init();
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE);
PWR_BackupAccessCmd(ENABLE);//使能BKP和RTC的访问
BKP_WriteBackupRegister(BKP_DR1, 0x1234);//当前STM32F103C8T6是中容量芯片,DR范围是1~10
OLED_ShowHexNum(1, 1, BKP_ReadBackupRegister(BKP_DR1), 4);
while (1)
{
}
}
这个数据在主电源掉电的情况下由备用电池维持,将写入的代码注释掉之后,再复位芯片,也会这样显示,说明备用电源能够保护数据不丢失
接下来是RTC实时时钟
RTC初始化的流程可以参考以上流程图
和BKP一样,在使用这个RTC外设之前,我们同样要注意一下两点
1设置RCC_APB1ENR的PWREN和BKPEN,使能PWR和BKP时钟
2设置PWR_CRd的DBP,使能BKP和RTC的访问
第二步,启动RTC时钟,这里我们计划使用LSE作为系统时钟,所以我们要使用RTC模块里的函数开启LSE时钟,这个时钟为了省电,默认是关闭的。
第三步,配置RTCCLK这个数据选择器,指定LSE为RTCCLK,这一步函数也是在RTC模块里的
第四步,要注意一下事项:

一个是第一条的等待同步,另一个是第三条的等待上一次操作完成,这两个函数照例调用就行
第五步,配置预分频器,给PRL重装寄存器一个合适的分频值以确保输出给计数器的频率是1Hz
第六步,配置CNT的值,给RTC一个初始时间,如果需要闹钟可以配置闹钟,需要中断可以配置中断部分
因为RTC比较简单,所以库函数没有使用结构体来配置,RTC也没有RTC_Cmd这种函数,只要开启时钟就能运行了
接下来看库函数:
首先需要了解几个RCC时钟配置相关的函数,以下几个是和RTC时钟相关的函数
void RCC_LSEConfig(uint8_t RCC_LSE);//配置LSE外部低速时钟,启动LSE时钟就是调用这个函数
void RCC_LSICmd(FunctionalState NewState);//配置LSI内部低速时钟
void RCC_RTCCLKConfig(uint32_t RCC_RTCCLKSource);//RTCCLK配置,用来选择RTCCLK的时钟源,实际上就是配置上图的数据选择器
void RCC_RTCCLKCmd(FunctionalState NewState);//启动RTCCLK,在调用上一个函数选择时钟之后,我们还需要调用这个函数来使能一下
FlagStatus RCC_GetFlagStatus(uint8_t RCC_FLAG);//获取标志位,因为LSE时钟不是想启动就能立刻启动的,调用这个启动时钟函数之后,我们还需要等待一下标志位,等标志位置一之后才算启动完成
接下来我们看RTC的库函数
void RTC_ITConfig(uint16_t RTC_IT, FunctionalState NewState);//配置中断输出
void RTC_EnterConfigMode(void);//进入配置模式,这个函数就是置CRL的CNF为1,进入配置模式,对应上图注意事项中第二条
void RTC_ExitConfigMode(void);//退出配置模式,就是把CNF位清零
uint32_t RTC_GetCounter(void);//获取CNT计数器的值,读取时钟就是靠这个函数
void RTC_SetCounter(uint32_t CounterValue);//写入CNT计数器的值,设置时间是靠这个函数
void RTC_SetPrescaler(uint32_t PrescalerValue);//写入预分频器,这个值会写入到预分频器的PRL重装寄存器中
void RTC_SetAlarm(uint32_t AlarmValue);//写入闹钟值
uint32_t RTC_GetDivider(void);//读取预分频器中的DIV余数寄存器,余数寄存器是一个自减计数器,一般是为了得到更细致的时间,因为CNT计数间隔最短是1s,如果要更细的时间就得靠这个DIV余数寄存器来实现了
void RTC_WaitForLastTask(void);//等待上次操作完成,函数内容就是循环直到RTOFF状态位为1,对应上图注意事项的第三条
void RTC_WaitForSynchro(void);//等待同步,函数内容是清除RSF标志位,然后循环直到RSF为1,对应上图注意事项的第一条
FlagStatus RTC_GetFlagStatus(uint16_t RTC_FLAG);//获取状态标志位
void RTC_ClearFlag(uint16_t RTC_FLAG);//清除标志位
ITStatus RTC_GetITStatus(uint16_t RTC_IT);//获取中断标志位
void RTC_ClearITPendingBit(uint16_t RTC_IT);//清除标志位
接下来我们正式写程序,我们按照前面说的步骤来进行初始化:
#include “stm32f10x.h” // Device header
void MyRTC_Init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE);
PWR_BackupAccessCmd(ENABLE);
RCC_LSEConfig(RCC_LSE_ON);//启动LSE晶振
while (!RCC_GetFlagStatus(RCC_FLAG_LSERDY));//等待LSE准备好
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
RCC_RTCCLKCmd(ENABLE);
//接着,根据配置步骤,我们要调用两个等待函数,一个是等待同步,一个是等待上一次写入操作完成
RTC_WaitForSynchro();//等待同步
RTC_WaitForLastTask();//等待上一次操作完成,这两行代码是一个安全保障措施,防止因为始终不同步而产生bug
RTC_SetPrescaler(32768 – 1)//配置预分频器,目前分频器输入的时钟是LSE,频率是32.768KHz,进行32768分频就是1Hz了
//根据注意事项,写操作之后,这个值不是立刻生效的,我们最好等一下写操作完成
RTC_WaitForLastTask();//这个等待函数我们可以在每次写入操作之后调用,也可以在写入之前
//根据注意事项,必须要先设置CNF位进入配置模式才能写入PRL等寄存器,但这个SetPrescaler函数中在写入我们传入的参数之前
//库函数就已经调用了进入配置模式的代码,在最后也自带退出配置模式的代码,另外SetCounter和SetAlarm也是
RTC_SetCounter(1672588795);//需要一个32位的秒计数器,设置一个初始时间,这里使用北京时间2023-1-1 23:59:55
RTC_WaitForLastTask();
//初始化到这里后,CNT的值就会从这个秒数开始,以1s的时间间隔,不断自增,我们读取CNT就能获取时间了
}
写到这里我们先进行初步测试
#include “stm32f10x.h” // Device header
#include “Delay.h”
#include “OLED.h”
#include “MyRTC.h”
int main(void)
{
OLED_Init();
MyRTC_Init();
while (1)
{
OLED_ShowNum(1, 1, RTC_GetCounter(), 10);
}
}
接下来我们要做的使写两个函数,一个是设置时间,一个是读取时间
读取时间我们就把读到的秒数,转换为年月日时分秒放到一个全局数组里,设置时间我们就把全局数组的年月日时分秒转换为秒数,再写到CNT里
void MyRTC_SetTime(void)
{
time_t time_cnt;
struct tm time_data;
time_data.tm_year = MyRTC_Time[0] – 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];//将数组的数据填充到结构体里
time_cnt = mktime(&time_data);//日期时间到秒数的转换
RTC_SetCounter(time_cnt);//把指定的秒数写入到CNT中
RTC_WaitForLastTask();
}
主要就是三步,第一步把我们数组指定的时间,填充到struct tm结构体里
第二步,使用mktime得到秒数
第三步,将秒数写入到RTC的CNT中
接下来是读取时间的函数
void MyRTC_ReadTime(void)
{
time_t time_cnt;
struct tm time_data;
time_cnt = RTC_GetCounter();//读取CNT秒数
time_data = *localtime(&time_cnt);//参数是const time_t*,所以把time_cnt的地址传入,返回值是struct tm *
//所以要先取内容再赋值给time_data
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;
}
#include “stm32f10x.h” // Device header
#include <time.h>
uint16_t MyRTC_Time[] = {2024, 4, 7, 22, 25, 54};
void MyRTC_Init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE);
PWR_BackupAccessCmd(ENABLE);
RCC_LSEConfig(RCC_LSE_ON);//启动LSE晶振
while (!RCC_GetFlagStatus(RCC_FLAG_LSERDY));//等待LSE准备好
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
RCC_RTCCLKCmd(ENABLE);
//接着,根据配置步骤,我们要调用两个等待函数,一个是等待同步,一个是等待上一次写入操作完成
RTC_WaitForSynchro();//等待同步
RTC_WaitForLastTask();//等待上一次操作完成,这两行代码是一个安全保障措施,防止因为始终不同步而产生bug
RTC_SetPrescaler(32768 – 1);//配置预分频器,目前分频器输入的时钟是LSE,频率是32.768KHz,进行32768分频就是1Hz了
//根据注意事项,写操作之后,这个值不是立刻生效的,我们最好等一下写操作完成
RTC_WaitForLastTask();//这个等待函数我们可以在每次写入操作之后调用,也可以在写入之前
//根据注意事项,必须要先设置CNF位进入配置模式才能写入PRL等寄存器,但这个SetPrescaler函数中在写入我们传入的参数之前
//库函数就已经调用了进入配置模式的代码,在最后也自带退出配置模式的代码,另外SetCounter和SetAlarm也是
RTC_SetCounter(1672588795);//需要一个32位的秒计数器,设置一个初始时间,这里使用北京时间2023-1-1 23:59:55
RTC_WaitForLastTask();
//初始化到这里后,CNT的值就会从这个秒数开始,以1s的时间间隔,不断自增,我们读取CNT就能获取时间了
}
void MyRTC_SetTime(void)
{
time_t time_cnt;
struct tm time_data;
time_data.tm_year = MyRTC_Time[0] – 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];//将数组的数据填充到结构体里
time_cnt = mktime(&time_data);//日期时间到秒数的转换
RTC_SetCounter(time_cnt);//把指定的秒数写入到CNT中
RTC_WaitForLastTask();
}
void MyRTC_ReadTime(void)
{
time_t time_cnt;
struct tm time_data;
time_cnt = RTC_GetCounter();//读取CNT秒数
time_data = *localtime(&time_cnt);//参数是const time_t*,所以把time_cnt的地址传入,返回值是struct tm *
//所以要先取内容再赋值给time_data
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;
}
主函数:
#include “stm32f10x.h” // Device header
#include “Delay.h”
#include “OLED.h”
#include “MyRTC.h”
int main(void)
{
OLED_Init();
MyRTC_Init();
OLED_ShowString(1, 1, “Data:XXXX-XX-XX”);
OLED_ShowString(2, 1, “Time:XX:XX:XX”);
OLED_ShowString(3, 1, “CNT:”);
while (1)
{
MyRTC_ReadTime();
OLED_ShowNum(1, 6, MyRTC_Time[0], 4);//年
OLED_ShowNum(1, 11, MyRTC_Time[1], 2);//月
OLED_ShowNum(1, 14, MyRTC_Time[2], 2);//日
OLED_ShowNum(2, 6, MyRTC_Time[3], 2);//时
OLED_ShowNum(2, 9, MyRTC_Time[4], 2);//分
OLED_ShowNum(2, 12, MyRTC_Time[5], 2);//秒
OLED_ShowNum(3, 6, RTC_GetCounter(), 10);
}
}
如果想要表达北京时区的时间,需要在伦敦时间的基础上加上8小时的时差,不能够直接在结构体的小时变量里加,因为小时是24进一位的,我们可以直接在总的时间戳上加8小时对应的秒数就行了
time_cnt = RTC_GetCounter() + 8 * 60 * 60;
现在要解决一个问题,就是每次复位,时间都会重置,在程序中时间会重置的原因就是我们每次复位后都调用了初始化函数,在这个初始化函数里,我们自己手动给它的时间重置了,所以这个初始化代码我们要有判断地去执行,当系统断电,备用电池也断电了我们就执行这个初始化,当系统只是主电源断电,备用电池没断的话,我们就不需要执行了。要判断是否只有主电源断电,就需要用到我们之前学的BKP了,我们在BKP里随便写一个数据,如果上电检测这个数据没清零,就表示备用电池是存在的,RTC也可以继续运行,这样上电的时候就不用再初始化,重置时间了,如果上电检测数据清零了,就表示系统完全断电过,这样就需要重新初始化了
void MyRTC_Init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE);
PWR_BackupAccessCmd(ENABLE);
if (BKP_ReadBackupRegister(BKP_DR1) != 0xA5A5)//随便指定一个数据当标志位
{
RCC_LSEConfig(RCC_LSE_ON);
while (!RCC_GetFlagStatus(RCC_FLAG_LSERDY));
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
RCC_RTCCLKCmd(ENABLE);
RTC_WaitForSynchro();
RTC_WaitForLastTask();
RTC_SetPrescaler(32768 – 1);
RTC_WaitForLastTask();
RTC_SetCounter(1672588795);
RTC_WaitForLastTask();
BKP_WriteBackupRegister(BKP_DR1, 0xA5A5);//初始化之后DR1是A5A5,下次读到是这个数就说明初始化过了,并且中途备用电池没断电
}
else
{
RTC_WaitForSynchro();//防止意外
} RTC_WaitForLastTask();
}