STM32后备区域详解:读写BKP备份寄存器与RTC实时时钟的应用指南
目录
STM32后备区域:读写BKP备份寄存器与使用RTC实时时钟详解
1 什么是STM32的后备区域
分割线*
2.1 BKP备份寄存器简介
2.2 BKP备份寄存器基本结构
2.3 BKP外设头文件 bkp.h介绍
2.4 读写 BKP备份寄存器 操作步骤
2.5 编写 读写备份寄存器
5.1 文件介绍
main.c
分割线*
3.1 什么是Unix时间戳
3.2 C语言中时间戳转换函数
2.1 C语言提供的time.h函数介绍
2.2 时间戳转换图
3.3 RTC简介
3.4 RTC时钟选择
3.5 RTC框图
3.6 RTC基本框图
3.7 RTC硬件电路
3.8 RTC头文件介绍
3.9 使用 RTC实时时钟 操作步骤
3.10 编写RTC实时时钟显示年月日时分秒
3.10.1 文件介绍
MyRTC.c
MyRTC.h
main.c
STM32后备区域:读写BKP备份寄存器与使用RTC实时时钟详解
1 什么是STM32的后备区域
STM32 的后备区域是芯片内部的一个特殊区域。
特点和作用:
后备区域有什么:
分割线*
下面开始BKP部分
2.1 BKP备份寄存器简介
BKP(Backup Registers)备份寄存器(后备寄存器)
它位于后备区域。
BKP可用于存储数据。
存储特性:
- 当VDD(2.0~3.6V)电源(主电源)被切断,后备区域仍然由VBAT备用电源(1.8~3.6V)维持供电。
- 并且系统在待机模式下被唤醒,或系统复位或电源复位时,他们也不会被复位
- 但是如果备用电源VBAT和主电源VCC都断电了,就会清除数据,因为BKP本质是RAM存储器。掉电丢失数据
STM32的TAMPER引脚
他可以产生的侵入事件可以将所有备份寄存器内容清除
- TAMPER是用于引入检测信号(可以是或上升沿/下降沿)的,当发生入侵时,将清除BKP所有内容,并申请中断。
- 并且他是由备用电源供电,主电源断电后侵入检测仍然有效,以保证数据安全
BKP的RTC引脚可以输出RTC校准时钟、RTC闹钟脉冲或者秒脉冲
BKP存储RTC时钟校准寄存器
STM32后备区域的供电特性:
- 当VDD主电源掉电时,后备区域仍然可以由VBAT的备用电池供电。
- 当VDD主电源上电时,后备区域供电会自动从VBAT切换到VDD。
BKP数据存储容量:
- 20字节(中容量和小容量)/ 84字节(大容量和互联型)
手册建议:
- 如果没有外部电池,建议VBAT引脚接到VDD,就是VBAT和主电源接到一起,并且再连接一个100nf的滤波电容
2.2 BKP备份寄存器基本结构
其中橙色部分就是后备区域,BKP处于后备区域。但后备区域不止有BKP。
STM32后备区域的特性:
数据寄存器:
TAMPER引脚:
时钟输出:
2.3 BKP外设头文件 bkp.h介绍
void BKP_DeInit(void);
void BKP_TamperPinLevelConfig(uint16_t BKP_TamperPinLevel);
void BKP_TamperPinCmd(FunctionalState NewState);
void BKP_ITConfig(FunctionalState NewState);
void BKP_RTCOutputConfig(uint16_t BKP_RTCOutputSource);
void BKP_SetRTCCalibrationValue(uint8_t CalibrationValue);
void BKP_WriteBackupRegister(uint16_t BKP_DR, uint16_t Data);
写BKP备份寄存器
uint16_t BKP_ReadBackupRegister(uint16_t BKP_DR);
读BKP寄存器
FlagStatus BKP_GetFlagStatus(void);
查看标志位
void BKP_ClearFlag(void);
清除标志位
ITStatus BKP_GetITStatus(void);
查看中断标志位
void BKP_ClearITPendingBit(void);
清除中断标志位
pwr.h中也有一个函数是需要用到的
void PWR_BackupAccessCmd(FunctionalState NewState);
备份寄存器访问使能(其实是设置BDP位)
2.4 读写 BKP备份寄存器 操作步骤
- 开启PWR和BKP的时钟
- PWR的开启函数为
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
它的目的是开启后备电源的时钟(可以理解为开启后备电源VBAT)。 - BKP的开启函数
RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE);
他的目的是开启BKP外设的时钟,可以看到他们都是APB1总线下的。 - 使能备份区域的访问
- 因为RTC实时时钟和BKP备份寄存器,都处于备份区域中。在STM32中,想访问RTC和BKP,就要先开启备份区域的访问权限。它在PwR.h中 函数:
PWR_BackupAccessCmd(ENABLE);
- 读写操作
- 写入:
BKP_WriteBackupRegister(BKP_DR1,Data);
- 读出:
Data = BKP_ReadBackupRegister(BKP_DR1);
2.5 编写 读写备份寄存器
5.1 文件介绍
main.c
#include "stm32f10x.h" // Device header
#include "oled.h"
#include "Delay.h"
#include "Key.h"
/**
* 函 数:STM32 BKP备份寄存器的读写测试
* 参 数:无
* 返 回 值:无
* 注意事项:无
*/
uint16_t WriteArr[] = {0x0000, 0x0001};
uint16_t ReadArr[2] = { 0 };
int main()
{
OLED_Init();//初始化OLED;
KEY_Init();//初始化按键
//使能时钟电源和后备接口时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR,ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP,ENABLE);
//使能备份访问控制
PWR_BackupAccessCmd(ENABLE);
OLED_ShowString(1,1,"W:");
OLED_ShowString(2,1,"R:");
while(1)
{
if(KEY_Get() == 1)
{
//写入BKP
BKP_WriteBackupRegister(BKP_DR1,WriteArr[0]);
BKP_WriteBackupRegister(BKP_DR2,WriteArr[1]);
//显示写入的值
OLED_ShowHexNum(1,3,WriteArr[0],4);
OLED_ShowHexNum(1,8,WriteArr[1],4);
WriteArr[0]++;
WriteArr[1]++;
}
//读取BKP
ReadArr[0] = BKP_ReadBackupRegister(BKP_DR1);
ReadArr[1] = BKP_ReadBackupRegister(BKP_DR2);
//显示读取的值
OLED_ShowHexNum(2,3,ReadArr[0],4);
OLED_ShowHexNum(2,8,ReadArr[1],4);
}
}
分割线*
下面开始RTC部分
3.1 什么是Unix时间戳
GMT(Greenwich Mean Time)格林尼治标准时间是一种以地球自转为基础的时间计量系统。它将地球自转一周的时间间隔等分为24小时,以此确定计时标准 (不过他不精准,因为地球自转是越来越慢的。所以又有了如下的规定)
(格林尼治是伦敦的一个区)
UTC(Universal Time Coordinated)协调世界时是一种以原子钟为基础的时间计量系统。它规定铯133原子基态的两个超精细能级间在零磁场下跃迁辐射9,192,631,770周所持续的时间为1秒。当原子钟计时一天的时间与地球自转一周的时间相差超过0.9秒时,UTC会执行闰秒来保证其计时与地球自转的协调一致
我们时间戳所说的1970年1月1日0时0分0秒。指的是伦敦伦敦时间的0时0秒。 其他的位置可以分为24个时区。每偏差一个时区,相应的时间就要加或减一个小时
3.2 C语言中时间戳转换函数
2.1 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*); | 日期时间转换为字符串(自定义格式) |
2.2 时间戳转换图
这张图就清晰了显示了各个函数的作用:其实就是在各种数据类型之间进行转换。
-
秒计数器数据类型:time_t,其实就是一个32或64位的有符号的整形数据。也就是64位的秒计数器(如果不特别声明,默认为64。)
-
日期时间数据类型:struct tm,这时一个结构体类型。成员如下:
- 秒、
- 分、
- 时、
- 月的几日、
- 月份、(需要+1偏移量)
- 年份(需要+1900偏移量)、
- 周某开始的星期几、从1月1日开始的第几天、
- 是否使用夏令时 (是为了鼓励夏天时早睡早起节约用电设计的,目前个别国家在使用)
字符串型数据类型:char*,用来指向一个表示时间的字符串
所以,在转换时,要根据函数的返回值,来进行相应赋值。比如struct tm* gmtime(const time_t*);
的返回值为Struct tm的指针类型。那么在赋值给自己定义的Struct tm xxx 结构体时,就要先用*取值,才能正确赋值
等等以此类推,需要根据不同的类型进行转换,
(time_t 整形、Struct tm 结构体 Char* 字符指针..)
3.3 RTC简介
RTC (Real Time Clock):实时时钟。
- HSE时钟除以128(通常为8MHz/128)
- LSE振荡器时钟(通常为32.768KHz)
- LSI振荡器时钟(40KHz)
3.4 RTC时钟选择
时钟信号解释
HSE = 高速外部时钟信号
HSI = 高速内部时钟信号
LSI = 低速内部时钟信号
LSE = 低速外部时钟信号
H (High):高速,L (Low):低速,E (External):外部,I (Internal):内部
时钟选择
- 因为LSE外部低速时钟,才可以通过VBAT备用电池供电,
- 所以在主电源断电情况下LSE可以继续震荡,实现RTC主电源掉电继续走时的功能。
- 并且他的频率为32.768KHZ。经过2^15分频之后每次自然溢出时刚好为1s,也就是1HZ。
3.5 RTC框图
灰色填充区域均是后备区域。
可编程预分频器:
分频和计数:
RTC_CNT:
闹钟寄存器RTC_ALR:
中断信号:
中断标志位和中断输出控制
APB1总线和APB1接口:
3.6 RTC基本框图
时钟来源配置:
时钟预分频:
闹钟设定:
中断信号触发:
程序配置步骤:
**配置数据选择器:**选择RTCCLK时钟来源。
**配置重装寄存器:**选择分频系数。
配置32位计数器:
配置中断:
3.7 RTC硬件电路
- 备用电池供电
- **简单连接(左侧):**使用一个3V的电池B1直接连接到VBAT和GND。这样设计简单,但是电源冗余不高。
- **推荐连接(中间):**使用两个3V的电池B2和B3通过两个二极管D1和D2连接到VBAT和GND。这样设计增加了电源的可靠性,因为如果一个电池失效,另一个电池还能提供电源。电容C3(0.1uF)用于滤波,稳定电压。
- 外部低速晶振
- **晶振部分(中间):**使用一个32.768kHz的晶振(X1)连接到两个10pF的电容(C1和C2),并接地。这部分电路提供了一个稳定的时钟信号,通常用于RTC(实时时钟)功能。
- **连接到STM32单片机(右侧):**OSC32_IN和OSC32_OUT分别连接到STM32单片机的PC14和PC15引脚。
- STM32单片机连接
- **供电和地(右侧):**VDD和VSS分别是电源和地,VDD连接到电源正极,VSS连接到地。VBAT连接到备用电池供电部分的输出。
- **时钟信号(右侧):**PC14和PC15分别连接到外部低速晶振的OSC32_IN和OSC32_OUT。
- **其他引脚(右侧):**图中列出了STM32F103C8T6单片机的引脚配置,包括PA0到PA15,PB0到PB15等。这些引脚可以根据具体应用进行配置。
3.8 RTC头文件介绍
void RTC_ITConfig(uint16_t RTC_IT, FunctionalState NewState);
RTC_IT
和状态 NewState
,实现对 RTC 中断的使能或失能控制。void RTC_EnterConfigMode(void);
void RTC_ExitConfigMode(void);
uint32_t RTC_GetCounter(void);
void RTC_SetCounter(uint32_t CounterValue);
CounterValue
。void RTC_SetPrescaler(uint32_t PrescalerValue);
PrescalerValue
。void RTC_SetAlarm(uint32_t AlarmValue);
AlarmValue
。uint32_t RTC_GetDivider(void);
void RTC_WaitForLastTask(void);
void RTC_WaitForSynchro(void);
RTC_CNT
、RTC_ALR
和 RTC_PRL
)与 APB 时钟完成同步。FlagStatus RTC_GetFlagStatus(uint16_t RTC_FLAG);
RTC_FLAG
,返回其状态。void RTC_ClearFlag(uint16_t RTC_FLAG);
ITStatus RTC_GetITStatus(uint16_t RTC_IT);
RTC_IT
,判断中断是否发生。void RTC_ClearITPendingBit(uint16_t RTC_IT);
3.9 使用 RTC实时时钟 操作步骤
RTC使用时,一般都是和BKP一起使用的:
因为RTC在初始化时,需要配置其32位的时间戳计数器。
但每次复位或者主电源上电后,在执行主程序时,会重新初始化RTC的时间戳计数器。这就导致了RTC实时时钟本来能在备份电源的供给下计时,但是上电后又给我重新初始化覆盖了。导致不实时了!
所以需要在初始化程序中判断是否需要设置32位的时间戳计数器。
一般的方法是,在第一次初始化RTC时,顺便在BKP备份寄存器中写入特定值。在下一次初始化RTC时,对BKP备份寄存器的值进行判断。如果之前没有写入,那么就初始化。否则不初始化。
所以使用RTC实时时钟操作步骤如下:
执行以下操作将使能对BKP和RTC的访问:
-
开启PWR和BKP的时钟
- PWR的开启函数为
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
开启后备电源的时钟 - BKP的开启函数
RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE);
开启BKP外设的时钟
使能备份区域的访问
函数:PWR_BackupAccessCmd(ENABLE);
判断是否需要初始化RTC见代码部分
开启LSE时钟
- 开启LSE时钟
RCC_LSEConfig(RCC_LSE_ON);
- 等待LSE时钟开启完毕
while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) != SET);
- 选择RTCCLK时钟为LSE
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
- 使能RTCCLK时钟
RCC_RTCCLKCmd(ENABLE);
等待时钟同步
-
因为可能在恢复主电源之后,APB1总线刚刚恢复震荡频率,但是RTCCLK需要经过外部震荡源分频后才能有一次输出。如果直接读取,会导致读取不准确。 所以需要等待RTCCLK产生上升沿来激活更新一下时间戳计数器,这时APB1直接读取。才是准确的。 所以软件读取时必须等待RTCCLK来一个上升沿,使RTC_CRL寄存器中的**RSF位(寄存器同步标志)被硬件置1。**这时再读取RTC_CRL寄存器。才能正确读取。
函数:
RTC_WaitForSynchro();
(等待时钟同步)
等待写入完成
-
对RTC任何寄存器的写操作,都必须在前一次写操作结束后进行。
-
可以通过查询RTC_CR寄存器中的RTOFF状态位,判断RTC寄存器是否处于更新中。 当RTOFF状态位是1时,才可以写入RTC寄存器
函数:
RTC_WaitForLastTask();
设置RTC预分频器
- 设置预分频器
RTC_SetPrescaler(32768 - 1);
- 等待写入完成
RTC_WaitForLastTask();
写入前其实是需要设置RTC_CRL寄存器中的CNF位 (函数会帮我们自动完成,所以不需要)
- 在写入时,必须设置RTC_CRL寄存器中的CNF位, 使RTC进入配置模式后,才能写入RTC_PRL(预分频器)、RTC_CNT(时间戳计数器)、RTC_ALR(闹钟寄存器)寄存器
设置CNT时间戳计数器时间
(利用C语言中time.h来转换时间戳并写入,详见代码)
在BKP备份寄存器中写入特定数据。为下次复位或仅主电源断电后上电时判断是否初始化打下基础
3.10 编写RTC实时时钟显示年月日时分秒
3.10.1 文件介绍
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
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, "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(); //RTC读取时间,最新的时间存储到MyRTC_Time数组中
OLED_ShowNum(1, 6, MyRTC_Time[0], 4); //显示MyRTC_Time数组中的时间值,年
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); //显示32位的秒计数器
OLED_ShowNum(4, 6, RTC_GetDivider(), 10); //显示余数寄存器
}
}
作者:L_Z_J_I