BKP备份寄存器与RTC实时时钟(STM32标准库学习笔记)

在前一章节中,深入探讨了SPI通信外设的原理及其与W25Q64 Flash存储器的硬件连接与数据交互,重点实践了基于STM32标准库的硬件SPI配置与数据读写流程。该部分内容聚焦于外设驱动与外部设备通信,强调了时序控制与数据完整性保障。

本章将转向STM32微控制器中另一类关键功能模块------备份域(Backup Domain)中的备份寄存器(BKP)与实时时钟(RTC)。这两者共同构成了系统在低功耗模式下维持时间信息与关键配置数据的核心机制。与上一章的外设通信不同,本章内容更侧重于系统级时钟管理、断电状态下的数据保持能力以及时间基准的精确维护。

通过本章的学习,将掌握如何启用并配置RTC时钟源,设置初始时间与日期,利用中断实现定时唤醒,并借助备份寄存器在系统掉电后仍能保留用户设定的关键参数。这些功能广泛应用于智能仪表、工业监控、物联网节点等对时间精度和数据持久性有要求的应用场景。

从数据传输到时间守护,本系列笔记逐步构建起对STM32标准外设的全面理解,为后续复杂系统开发奠定坚实基础。

如果觉得有收获可以为我的文章点点小赞,这将激励我创作更好的内容!谢谢!

一、BKP备份寄存器与RTC实时时钟

1.1 Unix时间戳

Unix 时间戳(Unix Timestamp)定义为从UTC/GMT的1970年1月1日0时0分0秒开始所经过的秒数,不考虑闰秒

时间戳存储在一个秒计数器中,秒计数器为32位/64位的整型变量

世界上所有时区的秒计数器相同,不同时区通过添加偏移来得到当地时间

1.2 UTC/GMT

GMT(Greenwich Mean Time)格林尼治标准时间是一种以地球自转为基础的时间计量系统。它将地球自转一周的时间间隔等分为24小时,以此确定计时标准

UTC(Universal Time Coordinated)协调世界时是一种以原子钟为基础的时间计量系统。它规定铯133原子基态的两个超精细能级间在零磁场下跃迁辐射9,192,631,770周所持续的时间为1秒。当原子钟计时一天的时间与地球自转一周的时间相差超过0.9秒时,UTC会执行闰秒来保证其计时与地球自转的协调一致

1.3 时间戳转换

C语言的time.h模块提供了时间获取和时间戳转换的相关函数,可以方便地进行秒计数器、日期时间和字符串之间的转换

1.4 BKP简介

BKP(Backup Registers)备份寄存器

BKP可用于存储用户应用程序数据。当VDD(2.0~3.6V)电源被切断,他们仍然由VBAT(1.8~3.6V)维持供电。当系统在待机模式下被唤醒,或系统复位或电源复位时,他们也不会被复位

TAMPER引脚产生的侵入事件将所有备份寄存器内容清除

RTC引脚输出RTC校准时钟、RTC闹钟脉冲或者秒脉冲

存储RTC时钟校准寄存器

用户数据存储容量:

20字节(中容量和小容量)/ 84字节(大容量和互联型)

1.5 BKP基本结构

1.6 RTC简介

RTC(Real Time Clock)实时时钟

RTC是一个独立的定时器,可为系统提供时钟和日历的功能

RTC和时钟配置系统处于后备区域,系统复位时数据不清零,VDD(2.0~3.6V)断电后可借助VBAT(1.8~3.6V)供电继续走时

32位的可编程计数器,可对应Unix时间戳的秒计数器

20位的可编程预分频器,可适配不同频率的输入时钟

可选择三种RTC时钟源:

HSE时钟除以128(通常为8MHz/128)

LSE振荡器时钟(通常为32.768KHz)

LSI振荡器时钟(40KHz)

1.7 RTC框图

1.8 RTC基本结构

1.9 硬件电路

1.10 RTC操作注意事项

执行以下操作将使能对BKP和RTC的访问:

设置RCC_APB1ENR的PWREN和BKPEN,使能PWR和BKP时钟

设置PWR_CR的DBP,使能对BKP和RTC的访问

若在读取RTC寄存器时,RTC的APB1接口曾经处于禁止状态,则软件首先必须等待RTC_CRL寄存器中的RSF位(寄存器同步标志)被硬件置1

必须设置RTC_CRL寄存器中的CNF位,使RTC进入配置模式后,才能写入RTC_PRL、RTC_CNT、RTC_ALR寄存器

对RTC任何寄存器的写操作,都必须在前一次写操作结束后进行。可以通过查询RTC_CR寄存器中的RTOFF状态位,判断RTC寄存器是否处于更新中。仅当RTOFF状态位是1时,才可以写入RTC寄存器

1.11 实例一 12.1.1 读写备份寄存器

BKP的初始化步骤

第一步,开启PWR和BKP的时钟 ;

第二步,使用PWR的一个函数,使能对BKP和RTC的访问 ;

随后,便可以使用库函数写入或者读取BKP

可能用到的BKP库函数:

//恢复缺省配置/手动清空BKP所有的数据寄存器

void BKP_DeInit(void);

//配置TAMPER侵入检测功能:配置TAMPER引脚的有效电平

void BKP_TamperPinLevelConfig(uint16_t BKP_TamperPinLevel);

//配置TAMPER侵入检测功能:是否开启侵入检测功能

void BKP_TamperPinCmd(FunctionalState NewState);

//是否开启BKP的中断

void BKP_ITConfig(FunctionalState NewState);

//配置时钟输出功能

void BKP_RTCOutputConfig(uint16_t BKP_RTCOutputSource);

//设置RTC校准值

void BKP_SetRTCCalibrationValue(uint8_t CalibrationValue);

//写备份寄存器

void BKP_WriteBackupRegister(uint16_t BKP_DR, uint16_t Data);

//读备份寄存器

uint16_t BKP_ReadBackupRegister(uint16_t BKP_DR);

//标志位相关函数

FlagStatus BKP_GetFlagStatus(void);

void BKP_ClearFlag(void);

ITStatus BKP_GetITStatus(void);

void BKP_ClearITPendingBit(void);

可能用到的PWR库函数:

//PWR_BackupAccessCmd,备份寄存器访问使能

void PWR_BackupAccessCmd(FunctionalState NewState);

main .c ↓

-----------------------------

cpp 复制代码
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "Key.h"
uint8_t KeyNum;					//定义用于接收按键键码的变量
uint16_t ArrayWrite[] = {0x1234,0x5678};	//定义要写入数据的测试数组
uint16_t ArrayRead[2];						//定义要读取数据的测试数组
int main(void)
{
/模块初始化/
OLED_Init();
Key_Init();
/*显示静态字符串*/
OLED_ShowString(1,1,"W:");
OLED_ShowString(2,1,"R:");

/*开启时钟*/	
RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP,ENABLE);		//开启PWR的时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR,ENABLE);		//开启BKP的时钟

/*备份寄存器访问使能*/
PWR_BackupAccessCmd(ENABLE);							//使用PWR开启对备份寄存器的访问

while(1)
{
	KeyNum = Key_GetNum();		//获取按键键码
	
	if(KeyNum == 1)						//按键1按下
	{
		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);		//
	}
	
	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);				//

}
}

-----------------------------

1.12 实例二 12.2.1 实时时钟

RTC的初始化的配置流程

第一步,开启PWR和BKP的时钟,使能BKP和RTC的访问;

第二步,启动RTC的时钟,使用LSE作为系统时钟,所以,要使用RCC模块里的函数,开启LSE的时钟(LSE的时钟为了省电是默认关闭的,需手动开启);

第三步,选择RTCCLK时钟源,配置RTCCLK数据选择器,指定LSE为RTCCLK;

第四步,根据RTC的使用注意事项,需要调用两个等待的函数,一个是 等待同步 ,另一个是 等待上一步操作完成 ;

第五步,配置预分频器,给PRL重装寄存器一个合适的分频值,以确保输出给计数器的频率是1Hz;

第六步,配置CNT的值,给RTC一个初始时间。如果需要闹钟,可以配置闹钟值,需要中断的话,可以配置中断这部分;

可能用到的 RCC 库函数:

//RCC_LSEConfig配置LSE外部低速时钟

void RCC_LSEConfig(uint8_t RCC_LSE);

//RCC_LSICmd配置LSI内部低速时钟

void RCC_LSICmd(FunctionalState NewState);

//RCC_RTCCLKConfig RTCCLK配置

void RCC_RTCCLKConfig(uint32_t RCC_RTCCLKSource);

//RCC_RTCCLKCmd 启动RTCCLK

void RCC_RTCCLKCmd(FunctionalState NewState);

//RCC_GetFlagStatus获取标志位

FlagStatus RCC_GetFlagStatus(uint8_t RCC_FLAG);

可能用到的 RTC 库函数:

//RTC_ITConfig配置中断输出

void RTC_ITConfig(uint16_t RTC_IT, FunctionalState NewState);

//RTC_EnterConfigMode进入配置模式

void RTC_EnterConfigMode(void);

//RTC_ExitConfigMode退出配置模式

void RTC_ExitConfigMode(void);

//RTC_GetCounter获取CNT计数器的值(用来读取时钟)

uint32_t RTC_GetCounter(void);

//RTC_SetCounter写入CNT计数器的值(设置时间)

void RTC_SetCounter(uint32_t CounterValue);

//RTC_SetPrescaler写入预分频器(用来配置预分频器的分频系数)

void RTC_SetPrescaler(uint32_t PrescalerValue);

//RTC_SetAlarm写入闹钟值

void RTC_SetAlarm(uint32_t AlarmValue);

// RTC _ GetDivider读取预分频器中的DIV余数寄存器 (获取比1秒更短的时间)

uint32_t RTC_GetDivider(void);

//RTC_WaitForLastTask等待上次操作完成

void RTC_WaitForLastTask(void);

// RTC _ WaitForSynchro等待同步

void RTC_WaitForSynchro(void);

//标志位相关的函数

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);

在Keil软件里,time.c头文件里面gmtime与localtime是没有区别的,而且在stm32的环境下gmtime与localtime 始终将时间戳转换为0时区(伦敦时间) 的日期时间, mktime也始终是0时区 ,如果要换时区需要手动添加偏移,所以本程序的做法是弃用gmtime,只用localtime,来实现秒计数器转换日期时间的功能

使用利用time.c头文件设置RTC时间的流程

第一步,把我们数组指定的时间,填充到struct tm结构体里;

第二步,使用mktime函数,得到秒数;

第三步,将秒数写入到RTC的CNT中,完成RTC时间的设置。

MyRTC .c ↓

-----------------------------

cpp 复制代码
#include "stm32f10x.h"                  // Device header
#include <time.h>
uint16_t MyRTC_Time[] = {2026,11,1,11,3,55};	//定义全局的时间数组,数组内容分别为年、月、日、时、分、秒
void MyRTC_SetTime(void);				//函数声明
/**
函    数:RTC初始化
参    数:无
返 回 值:无
*/
void MyRTC_Init(void)
{
/开启时钟/	
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR,ENABLE);		//开启PWR的时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP,ENABLE);		//开启BKP的时钟
/备份寄存器访问使能/
PWR_BackupAccessCmd(ENABLE);							//使用PWR开启对备份寄存器的访问
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;			//定义秒计数器数据类型
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) - 86060;	//调用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_data;  	//定义日期时间数据类型
time_cnt = RTC_GetCounter() + 86060;			//读取RTC的CNT,获取当前的秒计数器
//+ 8 * 60 * 60为东八区的时区调整
time_data = *localtime(&time_cnt);					//使用localtime函数,将秒计数器转换为日期时间格式
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 ;
}
//如果LSE无法起振导致程序卡死在初始化函数中
//可将初始化函数替换为下述代码,使用LSI当作RTCCLK
//LSI无法由备用电源供电,故主电源掉电时,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_LSICmd(ENABLE);
	while (RCC_GetFlagStatus(RCC_FLAG_LSIRDY) != SET);
	
	RCC_RTCCLKConfig(RCC_RTCCLKSource_LSI);
	RCC_RTCCLKCmd(ENABLE);
	
	RTC_WaitForSynchro();
	RTC_WaitForLastTask();
	
	RTC_SetPrescaler(40000 - 1);
	RTC_WaitForLastTask();
	
	MyRTC_SetTime();
	
	BKP_WriteBackupRegister(BKP_DR1, 0xA5A5);
}
else
{
	RCC_LSICmd(ENABLE);				//即使不是第一次配置,也需要再次开启LSI时钟
	while (RCC_GetFlagStatus(RCC_FLAG_LSIRDY) != SET);
	
	RCC_RTCCLKConfig(RCC_RTCCLKSource_LSI);
	RCC_RTCCLKCmd(ENABLE);
	
	RTC_WaitForSynchro();
	RTC_WaitForLastTask();
}
}*/

-----------------------------

main .c ↓

-----------------------------

cpp 复制代码
#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,"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,8,   MyRTC_Time[3],2);			//时
	OLED_ShowNum(2,11,  MyRTC_Time[4],2);			//分
	OLED_ShowNum(2,14,  MyRTC_Time[5],2);			//秒

	OLED_ShowNum(3,6,RTC_GetCounter(),10);	        //显示32位的秒计数器
	OLED_ShowNum(4,6,RTC_GetDivider(),10);	        //显示余数寄存器
	
	
}
}

-----------------------------

二、总结

本文围绕 STM32 的 BKP 备份寄存器与 RTC 实时时钟展开,从时间的基本概念出发,逐步过渡到外设原理与工程实践,主要内容可以归纳为以下几个方面:

  • 时间基础:Unix 时间戳以 1970 年 1 月 1 日 0 时 0 分 0 秒为起点,用秒计数器表示时间,全球所有时区共用同一个秒计数器,通过添加时区偏移得到当地时间;UTC 以原子钟为基础,GMT 以地球自转为基础,二者在精度和闰秒处理上存在差异。
  • 时间戳转换 :借助 C 语言 time.h 中的 mktime、localtime 等函数,可以在秒计数器、struct tm 日期时间结构和字符串之间相互转换,这是 RTC 读写时间的关键工具。
  • BKP 备份寄存器:BKP 用于存储用户应用程序数据,在主电源 VDD 掉电后可由 VBAT 继续供电,系统复位、待机唤醒时数据不会丢失;TAMPER 引脚侵入事件会清除全部备份寄存器内容;中/小容量器件提供 20 字节、大容量和互联型提供 84 字节的存储空间。
  • RTC 实时时钟:RTC 是一个独立的定时器,位于后备区域,可提供时钟和日历功能;其 32 位可编程计数器对应 Unix 时间戳的秒计数器,20 位可编程预分频器用于适配不同频率的输入时钟,时钟源可选 HSE/128、LSE(32.768KHz)和 LSI(40KHz)。
  • 操作要点:使用 BKP 和 RTC 前必须先使能 PWR 和 BKP 时钟,并通过 PWR_CR 的 DBP 位开启访问;读取寄存器前需等待 RSF 同步标志置位,写入前需进入配置模式并等待 RTOFF 状态位为 1,这些细节是保证读写可靠的前提。
  • 工程实践:实例一演示了通过按键写入和读取备份寄存器,验证掉电不丢失的特性;实例二则完整实现了 RTC 的初始化、时间设置与读取显示,并利用备份寄存器中的标志位判断是否为首次配置,避免每次上电都重新设置时间。

在实际开发中,还需要特别注意时区问题:在 Keil 与 STM32 环境下,gmtime 与 localtime 均按 0 时区(伦敦时间)处理,mktime 同样以 0 时区为基准,因此需要在读写时手动加上或减去东八区的偏移量(8 × 60 × 60 秒),才能得到正确的本地时间。此外,若 LSE 晶振无法起振导致程序卡死在初始化阶段,可改用 LSI 作为 RTCCLK,但 LSI 无法由备用电源供电,主电源掉电时 RTC 走时会暂停,需要根据实际场景权衡取舍。

总体而言,BKP 与 RTC 的组合为嵌入式系统提供了一套低功耗、掉电可保持的时间与数据存储方案。掌握其时钟使能、访问使能、同步等待和配置模式等操作规范,再结合 time.h 的时间转换函数,就能在 STM32 项目中稳定地实现日历时钟、定时闹钟以及关键数据的掉电保存功能。

都看到这了点点赞吧!求求了!

相关推荐
陈飞EXCEL3 小时前
北京杰飞 IT 旗下妙码生花少儿Python编程上线两大学习系统
开发语言·python·学习
SLAM必须dunk3 小时前
Agent入门学习一
学习
Zw-awa3 小时前
串口命令如何从字符串长成协议:帧头、长度、校验和与状态机
stm32
瑞码空间3 小时前
JSP与Servlet核心开发与CRUD实战笔记
java·笔记·servlet·jsp
La Pulga4 小时前
【FreeRTOS源码学习笔记】链表的实现
数据结构·笔记·stm32·mcu·链表·嵌入式·freertos
承渊政道4 小时前
Linux网络学习【从NAT与代理服务理解网络转发、探索内网穿透原理、再到认识DNS与ICMP、最后动手部署并测试内网穿透】
linux·网络·学习·内网穿透·dns·nat
栉甜5 小时前
Vue2学习内容(上)
前端·javascript·vue.js·学习
shangjian0075 小时前
K8S学习-基础组件
学习·容器·kubernetes
lifallen6 小时前
ub-mode skill:软件工程原则
人工智能·学习·ai·软件工程·ai编程