STM32入门教程(RTC实时时钟&BKP备份寄存器篇)
重要的内容写在前面:
- 该系列是以up主江协科技的STM32视频教程为基础写下去的,大部分内容都参考了老师的课件,对于一些个人认为比较重要但是老师仅口述的部分,笔者都有用文字的方式记录并标出了重点。
- 文中的图片基本都来源于老师的课件以及开发板和芯片的手册,粘贴过来是为了方便阅读。
- 如果有条件的可以先学习一些相关课程再去看STM32的教程,学起来会更加轻松(不太建议零基础开始直接STM32,听起来可能会有点困难,可以先学51单片机),相关课程有数字电路(强烈推荐先学数电,不然可能会有很多地方理解起来很困难)、模拟电路、计算机组成原理(像寄存器、存储器、中断等在这门课里有很详细的介绍)、计算机网络等。
- 如有错漏欢迎指出。
视频链接:[12-1] Unix时间戳_哔哩哔哩_bilibili
一、时间与时间戳
1、Unix时间戳
(1)Unix时间戳(Unix Timestamp)定义为从UTC/GMT的1970年1月1日0时0分0秒开始所经过的秒数,不考虑闰秒。
(2)时间戳存储在秒计数器中,秒计数器为32位/64位的整型变量。
(3)世界上所有时区的秒计数器相同,不同时区通过添加偏移来得到当地时间。

2、C语言的time.h模块
C语言的time.h模块提供了时间获取和时间戳转换的相关函数,可以方便地进行秒计数器、日期时间(是个结构体,其中的成员为年月日时分秒以及星期几等)和字符串之间的转换
|
函数 |
作用 |
|
time_t time(time_t*); |
获取系统时钟 |
|
struct tm* gmtime(const time_t*); |
秒计数器转换为日期时间(格林尼治时间) |
|
struct tm* localtime(const time_t*); |
秒计数器转换为日期时间(当地时间) |
|
time_t mktime(struct tm*); |
日期时间转换为秒计数器(当地时间) |
|
char* ctime(const time_t*); |
秒计数器转换为字符串(默认格式) |
|
char* asctime(const struct tm*); |
日期时间转换为字符串(默认格式) |
|
size_t strftime(char*, size_t, const char*, const struct tm*); |
日期时间转换为字符串(自定义格式) |

二、BKP(Backup Registers)备份寄存器
1、BKP概述
(1)BKP可用于存储用户应用程序数据、RTC时钟校准值等,当VDD(2.0~3.6V)电源被切断,它们仍然由VBAT(1.8~3.6V)维持供电,当系统在待机模式下被唤醒、或系统复位、或电源复位时,它们也不会被复位。
(2)TAMPER引脚检测到侵入事件后,硬件会自动将所有备份寄存器的内容清除(软件无法拦截),同时可以申请中断,在中断函数中可以继续清除其它存储器的数据并锁死设备,常用于预防数据被窃取等恶性事件。
(3)RTC引脚可以输出三类信号——RTC校准时钟(512Hz,用于外部校准32.768kHz晶振)、RTC闹钟脉冲或者秒脉冲(1Hz)。
(4)用户数据存储容量:20字节(中容量和小容量)/ 84字节(大容量和互联型)。
2、BKP的基本结构示意

三、RTC(Real Time Clock)实时时钟
1、RTC概述
(1)RTC是一个独立的定时器,可为系统提供时钟和日历的功能。
(2)RTC和时钟配置系统处于后备区域,系统复位时数据不清零,VDD(2.0~3.6V)断电后可借助VBAT(1.8~3.6V)供电继续走时。
(3)配备32位的可编程计数器,可对应Unix时间戳的秒计数器。
(4)配备20位的可编程预分频器,可适配不同频率的输入时钟。
(5)可选择三种RTC时钟源:
①HSE时钟除以128(通常为8MHz/128)。
②LSE振荡器时钟(通常为32.768KHz,一般都选用该时钟作为RTCCLK)。
③LSI振荡器时钟(40KHz)。
2、RTC框图

(1)后备区域的电路在主电源掉电后可以使用备用电池维持工作。
(2)RTCCLK是经过选择器输入RTC的时钟,一般选择LSE振荡器时钟,进入RTC的时钟信号首先由预分频器进行分频。
(3)RTC的预分频器由两个寄存器组成,RTC_PRL是重装载寄存器(决定分频系数),RTC_DIV是余数寄存器(本质上是一个自减计数器),DIV负责对RTCCLK的时钟脉冲进行计数,当自减为0时会输出一个时钟脉冲到TR_CLK上,同时PRL将重装载值写进DIV中,以此往复达到分频的效果。
(4)RTC_CNT是秒计数器,TR_CLK每来一个时钟脉冲,计数器的值+1;理论上TE_CLK应该配置为每秒产生一个时钟脉冲。
(5)RTC_ALR是闹钟寄存器,它与RTC_CNT等长,用于设置闹钟,用户可以在ALR中写一个时间戳,当ALR与CNT的值相等时,会产生RTC_Alarm信号通往右侧的中断系统,同时还可以使STM32退出待机模式。
(6)RTC_Second是秒中断信号,每来一个时钟脉冲就会产生一个秒中断信号。
(7)RTC_Overflow是溢出中断信号,不过对于RTC_CNT而言,它存储的是32位的无符号数,到2106年才会发生计数溢出。(SECF、OWF、ALRF是中断标志位,SECIE、OWIE、ALRIE是中断使能位)
(8)读写RTC中的寄存器需要通过APB1总线(RTC是APB1总线上的设备)。
3、RTC的硬件电路

4、BKP和RTC操作的注意事项
(1)执行以下操作将使能对BKP和RTC的访问:
①设置RCC_APB1ENR的PWREN和BKPEN,使能PWR和BKP时钟。
②设置PWR_CR的DBP,使能对BKP和RTC的访问。(使用PWR_BackupAccessCmd函数)
(2)若在读取RTC寄存器时,RTC的APB1接口曾经处于禁止状态,则软件首先必须等待RTC_CRL寄存器中的RSF位(寄存器同步标志)被硬件置1。
(3)必须设置RTC_CRL寄存器中的CNF位,使RTC进入配置模式后,才能写入RTC_PRL、RTC_CNT、RTC_ALR寄存器。
(4)对RTC任何寄存器的写操作,都必须在前一次写操作结束后进行。可以通过查询RTC_CR寄存器中的RTOFF状态位,判断RTC寄存器是否处于更新中,。仅当RTOFF状态位是1时,才可以写入RTC寄存器。
四、示例程序
1、读写备份寄存器
(1)按照下图所示接好电路,并将OLED显示屏的项目文件夹复制一份作为模板使用。

(2)在stm32f10x_bkp.h文件中有BKP相关的函数。
[1] BKP_DeInit函数:将备份域(Backup Domain)的所有寄存器复位为默认值(即硬件复位后的状态),可以用于清空BKP的所有数据。
函数原型:void BKP_DeInit(void);
参数解释:无参数
返回值:无返回值
[2] BKP_TamperPinLevelConfig函数:配置侵入检测引脚(Tamper Pin,即PC13)的有效电平,即设定当TAMPER出现何种电平状态时,会触发一个侵入检测事件。
函数原型:void BKP_TamperPinLevelConfig(uint16_t BKP_TamperPinLevel);
参数解释:
uint16_t BKP_TamperPinLevel:BKP_TamperPinLevel_Hig表示当检测到上升沿时触发侵入事件,BKP_TamperPinLevel_Low当检测到下降沿时触发侵入事件
返回值:无返回值
[3] BKP_TamperPinCmd函数:使能或禁用侵入检测引脚的侵入检测功能。
函数原型:void BKP_TamperPinCmd(FunctionalState NewState);
参数解释:
FunctionalState NewState:ENABLE表示使能侵入检测功能,DISABLE表示禁用侵入检测功能,PC13引脚可用于普通I/O或其它复用功能(如RTC时钟输出)
返回值:无返回值
[4] BKP_ITConfig函数:使能或禁用侵入检测引脚的中断功能。
函数原型:void BKP_ITConfig(FunctionalState NewState);
参数解释:
FunctionalState NewState:ENABLE表示使能侵入检测中断,DISABLE表示禁用侵入检测中断
返回值:无返回值
[5] BKP_RTCOutputConfig函数:选择在侵入检测引脚上输出的RTC时钟源。
函数原型:void BKP_RTCOutputConfig(uint16_t BKP_RTCOutputSource);
参数解释:
uint16_t BKP_RTCOutputSource:指定要在侵入检测引脚上输出的RTC时钟源类型
返回值:无返回值
[6] BKP_SetRTCCalibrationValue函数:设置RTC时钟的校准值,以调整RTC的走时精度。
函数原型:void BKP_SetRTCCalibrationValue(uint8_t CalibrationValue);
参数解释:
uint8_t CalibrationValue:RTC时钟校准值,指定在每2^20个时钟脉冲中被忽略的脉冲数量,0表示不进行校准
返回值:无返回值
[7] BKP_WriteBackupRegister函数:向指定的备份数据寄存器BKP写入用户数据。
函数原型:void BKP_WriteBackupRegister(uint16_t BKP_DR, uint16_t Data);
参数解释:
uint16_t BKP_DR:指定要写入的备份数据寄存器
uint16_t Data:要写入备份寄存器的用户数据
返回值:无返回值
[8] BKP_ReadBackupRegister函数:从指定的备份数据寄存器BKP中读取用户数据。
函数原型:uint16_t BKP_ReadBackupRegister(uint16_t BKP_DR);
参数解释:
uint16_t BKP_DR:指定要读取的备份数据寄存器
返回值:指定备份数据寄存器中存储的16位数据
[9] BKP_GetFlagStatus函数:检查侵入检测事件的标志位是否被置位,即判断是否发生过侵入检测事件。
函数原型:FlagStatus BKP_GetFlagStatus(void);
参数解释:无参数
返回值:SET表示侵入检测事件标志位已被置位——表示发生过侵入检测事件,RESET表示侵入检测事件标志位未被置位——表示未发生过侵入检测事件
[10] BKP_ClearFlag函数:清除侵入检测引脚事件的待处理标志位。
函数原型:void BKP_ClearFlag(void);
参数解释:无参数
返回值:无返回值
[11] BKP_GetITStatus函数:检查侵入检测引脚的中断是否已经发生,即判断侵入检测中断请求是否处于待处理状态。
函数原型:ITStatus BKP_GetITStatus(void);
参数解释:无参数
返回值:SET表示侵入检测中断已发生且已被使能——存在有效的中断请求,RESET表示侵入检测中断未发生,或虽已发生但中断未被使能
[12] BKP_ClearITPendingBit函数:清除侵入检测引脚的中断待处理位。
函数原型:void BKP_ClearITPendingBit(void);
参数解释:无参数
返回值:无返回值
(3)在main.c文件中粘贴以下代码,然后编译,将程序下载到开发板中并进行调试(主要是开发板掉电后VBAT引脚通电与不通电的区别,观察两种情况下BKP中的数据是否会被重置)。
#include "stm32f10x.h" // Device headerCmd
#include "OLED.h"
#include "Key.h"
uint16_t ArrayWrite[] = {0x1234, 0x5678};
uint16_t ArrayRead[2];
uint8_t KeyNum;
int main()
{
OLED_Init();
Key_Init();
OLED_ShowString(1, 1, "W:");
OLED_ShowString(2, 1, "R:");
//使能PWR和BKP时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE);
//使能对BKP和RTC的访问
PWR_BackupAccessCmd(ENABLE);
while(1)
{
KeyNum = Key_GetNum();
if(KeyNum == 1) //按下按键1,ArrayWrite中的数据自增并写入BKP
{
ArrayWrite[0]++;
ArrayWrite[1]++;
BKP_WriteBackupRegister(BKP_DR1, ArrayWrite[0]);
BKP_WriteBackupRegister(BKP_DR2, ArrayWrite[1]);
OLED_ShowHexNum(1,3,ArrayWrite[0],4);
OLED_ShowHexNum(1,8,ArrayWrite[1],4);
}
//读取BKP中的数据
ArrayRead[0] = BKP_ReadBackupRegister(BKP_DR1);
ArrayRead[1] = BKP_ReadBackupRegister(BKP_DR2);
OLED_ShowHexNum(2,3,ArrayRead[0],4);
OLED_ShowHexNum(2,8,ArrayRead[1],4);
}
}
2、实时时钟应用
(1)按照下图所示接好电路,并将OLED显示屏的项目文件夹复制一份作为模板使用。

(2)在stm32f10x_rcc.h文件中有几个本例相关的函数。
[1] RCC_LSEConfig函数:配置外部低速晶振(LSE,Low Speed External)的使能、禁用或旁路模式。
函数原型:void RCC_LSEConfig(uint32_t RCC_LSE);
参数解释:
uint32_t RCC_LSE:指定LSE的新状态,RCC_LSE_OFF表示禁用LSE振荡器,RCC_LSE_ON表示开启LSE振荡器,RCC_LSE_BYPASS表示使用外部时钟源代替外部晶振
返回值:无返回值
[2] RCC_LSICmd函数:使能或禁用内部低速时钟(LSI,Low Speed Internal)振荡器。
函数原型:void RCC_LSICmd(FunctionalState NewState);
参数解释:
FunctionalState NewState:ENABLE表示使能LSI振荡器,DISABLE表示禁用LSI振荡器
返回值:无返回值
[3] RCC_RTCCLKConfig函数:选择RTC(实时时钟)的时钟源,即配置RTCCLK的数据选择器,决定RTC模块使用哪个时钟信号作为其计时基准。
函数原型:void RCC_RTCCLKConfig(uint32_t RCC_RTCCLKSource);
参数解释:
uint32_t RCC_RTCCLKSource:RCC_RTCCLKSource_LSE表示选择LSE,RCC_RTCCLKSource_LSI表示选择LSI,RCC_RTCCLKSource_HSE_Div128表示选择HSE经128分频后的时钟作为RTC时钟源
返回值:无返回值
[4] RCC_RTCCLKCmd函数:使能或禁用RTC(实时时钟)模块的时钟。
函数原型:void RCC_RTCCLKCmd(FunctionalState NewState);
参数解释:
FunctionalState NewState:ENABLE 表示使能RTC时钟,DISABLE表示禁用RTC时钟
返回值:无返回值
[5] RCC_GetFlagStatus函数:检查RCC模块中指定的状态标志位是否被置位,从而判断相应的时钟状态、复位事件等是否已发生。
函数原型:FlagStatus RCC_GetFlagStatus(uint8_t RCC_FLAG);
参数解释:
uint8_t RCC_FLAG:指定要检查的状态标志位
返回值:SET表示指定的标志位被置位(事件已发生/状态已就绪),RESET表示指定的标志位未被置位(事件未发生/状态未就绪)
[6] RCC_ClearFlag函数:清除RCC模块中的复位标志位。
函数原型:void RCC_ClearFlag(void);
参数解释:无参数
返回值:无返回值
(3)在stm32f10x_rtc.h文件中有RTC模块相关的函数。
[1] RTC_ITConfig函数:使能或禁用指定的RTC中断源。
函数原型:void RTC_ITConfig(uint16_t RTC_IT, FunctionalState NewState);
参数解释:
uint16_t RTC_IT:指定要使能或禁用的RTC中断源
FunctionalState NewState:指定中断源的新状态,ENABLE表示使能指定的中断源,DISABLE表示禁用指定的中断源
返回值:无返回值
[2] RTC_EnterConfigMode函数:将RTC外设切换到配置模式(置CRL寄存器的CNF位为1)。
函数原型:void RTC_EnterConfigMode(void);
参数解释:无参数
返回值:无返回值
[3] RTC_ExitConfigMode函数:将RTC外设从配置模式切换回正常运行模式(置CRL寄存器的CNF位为0)。
函数原型:void RTC_ExitConfigMode(void);
参数解释:无参数
返回值:无返回值
[4] RTC_GetCounter函数:读取RTC 32位计数器的当前值。
函数原型:uint32_t RTC_GetCounter(void);
参数解释:无参数
返回值:RTC计数器的当前值,代表从起始时刻开始累积的秒数
[5] RTC_SetCounter函数:设置RTC 32位计数器的值。
函数原型:void RTC_SetCounter(uint32_t Counter);
参数解释:
uint32_t Counter:要写入RTC计数器的初始值
返回值:无返回值
[6] RTC_SetPrescaler函数:设置RTC预分频寄存器的值,从而决定RTC计数器的更新频率。
函数原型:void RTC_SetPrescaler(uint32_t PrescalerValue);
参数解释:
uint32_t PrescalerValue:要写入RTC预分频寄存器的值
返回值:无返回值
[7] RTC_SetAlarm函数:设置RTC闹钟寄存器的值。
函数原型:void RTC_SetAlarm(uint32_t AlarmValue);
参数解释:
uint32_t AlarmValue:要写入RTC闹钟寄存器的值
返回值:无返回值
[8] RTC_GetDivider函数:读取RTC预分频器余数寄存器(RTC_DIV)的当前值。
函数原型:uint32_t RTC_GetDivider(void);
参数解释:无参数
返回值:RTC预分频器余数寄存器的当前值,即RTC_DIV的当前计数值
[9] RTC_WaitForLastTask函数:等待最近一次对RTC寄存器的写操作完成(等待RTOFF=1)。
函数原型:void RTC_WaitForLastTask(void);
参数解释:无参数
返回值:无返回值
[10] RTC_WaitForSynchro函数:等待RTC寄存器与APB时钟同步完成(等待RSF=1)。
函数原型:ErrorStatus RTC_WaitForSynchro(void);
参数解释:无参数
返回值:SUCCESS表示同步成功,ERROR表示同步失败
[11] RTC_GetFlagStatus函数:检查指定的RTC状态标志位是否被置位。
函数原型:FlagStatus RTC_GetFlagStatus(uint32_t RTC_FLAG);
参数解释:
uint32_t RTC_FLAG:指定要检查的RTC标志位
返回值:SET表示指定的标志位被置位,RESET表示指定的标志位未被置位
[12] RTC_ClearFlag函数:清除指定的RTC待处理标志位。
函数原型:void RTC_ClearFlag(uint32_t RTC_FLAG);
参数解释:
uint32_t RTC_FLAG:指定要清除的RTC标志位
返回值:无返回值
[13] RTC_GetITStatus函数:检查指定的RTC中断是否已经发生。
函数原型:ITStatus RTC_GetITStatus(uint16_t RTC_IT);
参数解释:
uint16_t RTC_IT:指定要检查的RTC中断源
返回值:SET表示指定的RTC中断已发生,RESET表示指定的RTC中断未发生
[14] RTC_ClearITPendingBit函数:清除指定RTC中断的待处理标志位。
函数原型:void RTC_ClearITPendingBit(uint16_t RTC_IT);
参数解释:
uint16_t RTC_IT:指定要清除的中断待处理位
返回值:无返回值
(3)在项目的System组中添加MyRTC.h文件和MyRTC.c文件,用于封装RTC模块的代码。
①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
②MyRTC.c文件:
#include "stm32f10x.h" // Device header
#include <time.h>
uint16_t MyRTC_Time[] = {2023, 1, 1, 23, 59, 55}; //2023年1月1日23:59:55
void MyRTC_SetTime(void);
void MyRTC_Init(void)
{
//使能PWR和BKP时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE);
//使能对BKP和RTC的访问
PWR_BackupAccessCmd(ENABLE);
if(BKP_ReadBackupRegister(BKP_DR1) != 0xA5A5) //RTC只需初始化一遍即可(关机后再开机,计时不会被重置)
{
//开启LSE的时钟
RCC_LSEConfig(RCC_LSE_ON);
//等待LES时钟开启
while(RCC_GetFlagStatus(RCC_FLAG_LSERDY) != SET);
//配置RTCCLK的数据选择器,指定LSE为时钟源
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
//允许RTCCLK选择的时钟进入RTC
RCC_RTCCLKCmd(ENABLE);
//等待同步,等待前一次写操作结束
RTC_WaitForLastTask();
RTC_WaitForSynchro();
//配置预分频器,输出1Hz的时钟(RTC_SetPrescaler函数中有使RTC进入配置模式的过程)
RTC_SetPrescaler(32768 - 1); //32.768KHz / 32768 = 1Hz
RTC_WaitForLastTask(); //等待前一次写操作结束
//给RTC一个初始时间
MyRTC_SetTime();
//第一次初始化RTC时给BKP写值,如果程序复位,初始化函数会再执行一遍
//而程序复位时BKP不会被重置,可以依据BKP中的值是否为A5A5判断是否需要初始化RTC
BKP_WriteBackupRegister(BKP_DR1, 0xA5A5);
}
else
{
RTC_WaitForLastTask();
RTC_WaitForSynchro();
}
}
void MyRTC_SetTime(void)
{
time_t time_cnt; //秒计数器数据类型
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;
//日期时间结构体转换为伦敦时间的时间戳,再调整为北京时间的时间戳
//(这步只是为了CNT使用得到北京时间的时间戳,即使使用伦敦的时间戳也不影响最终结果)
RTC_SetCounter(time_cnt); //写CNT(设置时间)
RTC_WaitForLastTask(); //等待前一次写操作结束
}
void MyRTC_ReadTime(void)
{
time_t time_cnt; //秒计数器数据类型
struct tm time_date; //日期时间数据类型
time_cnt = RTC_GetCounter() + 8 * 60 * 60;
//当前时间戳是北京时间的时间戳,进行转换时需要换回伦敦时间的时间戳
//(这步只是为了CNT使用北京时间的时间戳,即使使用伦敦的时间戳也不影响最终结果)
time_date = *localtime(&time_cnt); //时间戳转换为日期时间结构体
//读取日期时间结构体中的时间参数
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;
}
(3)在main.c文件中粘贴以下代码,然后编译,将程序下载到开发板中并进行调试(主要是开发板掉电后VBAT引脚通电与不通电的区别,观察两种情况下RTC的计时是否被重置)。
#include "stm32f10x.h" // Device headerCmd
#include "OLED.h"
#include "MyRTC.h"
int main()
{
OLED_Init();
MyRTC_Init();
OLED_ShowString(1, 1, "Date:XXXX-XX-XX");
OLED_ShowString(2, 1, "Time:XX-XX-XX");
OLED_ShowString(3, 1, "CNT:");
OLED_ShowString(4, 1, "DIV:");
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);
OLED_ShowNum(4,6,RTC_GetDivider(),10);
}
}更多推荐

所有评论(0)