STM32入门教程(RTC实时时钟&BKP备份寄存器篇)

重要的内容写在前面:

  1. 该系列是以up主江协科技的STM32视频教程为基础写下去的,大部分内容都参考了老师的课件,对于一些个人认为比较重要但是老师仅口述的部分,笔者都有用文字的方式记录并标出了重点。
  2. 文中的图片基本都来源于老师的课件以及开发板和芯片的手册,粘贴过来是为了方便阅读。
  3. 如果有条件的可以先学习一些相关课程再去看STM32的教程,学起来会更加轻松(不太建议零基础开始直接STM32,听起来可能会有点困难,可以先学51单片机),相关课程有数字电路(强烈推荐先学数电,不然可能会有很多地方理解起来很困难)、模拟电路、计算机组成原理(像寄存器、存储器、中断等在这门课里有很详细的介绍)、计算机网络等。
  4. 如有错漏欢迎指出。

视频链接:[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可用于存储用户应用程序数据,当VDD(2.0~3.6V)电源被切断,它们仍然由VBAT(1.8~3.6V)维持供电,当系统在待机模式下被唤醒、或系统复位、或电源复位时,它们也不会被复位

(2)TAMPER引脚产生的侵入事件会将所有备份寄存器的内容清除,同时还会申请中断,在中断函数中可以继续清除其它存储器的数据以及锁死设备,可用于预防数据被窃取等恶性事件。

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

(4)存储RTC时钟校准寄存器。

(5)用户数据存储容量: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退出待机模式。(RTC_ALR的值不会自己改变,需要用户设定)

(6)RTC_Second是秒中断信号,每有一个时钟脉冲就会产生一个秒中断信号;RTC_Overflow是溢出中断信号,不过对于RTC_CNT而言,它存储的是32位的无符号数,到2016年才会发生计数溢出。(SECF、OWF、ALRF是中断标志位,SECIE、OWIE、ALRIE是中断使能位)

(7)读写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函数:恢复缺省配置,可以用于清空BKP的所有数据。

[2]BKP_TamperPinLevelConfig函数:配置TAMPER是高电平触发还是低电平触发(侵入检测功能)。

[3]BKP_TamperPinCmd函数:选择是否开启侵入检测功能。

[4]BKP_ITConfig函数:选择是否开启中断。

[5]BKP_RTCOutputConfig函数:配置时钟输出功能,选择输出的时钟源。

[6]BKP_SetRTCCalibrationValue函数:设置RTC校准值。

[7]BKP_WriteBackupRegister函数:写备份寄存器。

[8]BKP_ReadBackupRegister函数:读备份寄存器。

[9]BKP_GetFlagStatus函数:获取标志位。

[10]BKP_ClearFlag函数:清除标志位。

[11]BKP_GetITStatus函数:在中断函数中获取标志位。

[12]BKP_ClearITPendingBit函数:在中断函数中清除标志位。

(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函数:配置LES外部低速时钟。

[2]RCC_LSICmd函数:配置LSI内部低速时钟。

[3]RCC_RTCCLKConfig函数:配置RTCCLK的数据选择器,选择RTCCLK的时钟源。

[4]RCC_RTCCLKCmd函数:允许RTCCLK选择的时钟进入RTC。

[5]RCC_GetFlagStatus函数:获取标志位。

[6]RCC_ClearFlag函数:清除标志位。

(3)在stm32f10x_rtc.h文件中有RTC模块相关的函数。

[1]RTC_ITConfig函数:配置中断输出。

[2]RTC_EnterConfigMode函数:RTC进入配置模式(置CRL寄存器的CNF位为1)。

[3]RTC_ExitConfigMode函数:RTC退出配置模式(置CRL寄存器的CNF位01)。

[4]RTC_GetCounter函数:读CNT计数器(获取时间)。

[5]RTC_SetCounter函数:写CNT计数器(设置时间)。

[6]RTC_SetPrescaler函数:写预分频器的PRL重装寄存器,也就是设置分频系数。

[7]RTC_SetAlarm函数:写闹钟寄存器。

[8]RTC_GetDivider函数:读预分频器的DIV余数寄存器。

[9]RTC_WaitForLastTask函数:等待上次操作完成(等待RTOFF=1)。

[10]RTC_WaitForSynchro函数:等待同步(等待REF=1)。

[11]RTC_GetFlagStatus函数:获取标志位。

[12]RTC_ClearFlag函数:清除标志位。

[13]RTC_GetITStatus函数:在中断函数中获取标志位。

[14]RTC_ClearITPendingBit函数:在中断函数中清除标志位。

(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);
	}
}
  • 24
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
STM32F407的RTC实时时钟可以通过以下步骤进行设置: 1. 打开PWR和BKP外设时钟。 2. 配置RTC时钟,使其与LSE低速外部晶振同步。 3. 配置RTC预分频器和计数器,以确定RTC时钟的更新频率。 4. 配置RTC时钟日历,包括时间和日期。 5. 使能RTC时钟和RTC中断,以便在时间更新时进行相应处理。 下面是一个简单的示例代码,用于初始化RTC实时时钟: ```c RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE); // 使能PWR外设时钟 PWR_BackupAccessCmd(ENABLE); // 允许修改RTC寄存器 // 选择LSE作为RTC时钟源 RCC_LSEConfig(RCC_LSE_ON); while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET); // 使能RTC外设时钟 RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); RCC_RTCCLKCmd(ENABLE); // 配置RTC预分频器和计数器 RTC_InitStructure.RTC_AsynchPrediv = 0x7F; // 异步预分频器值 RTC_InitStructure.RTC_SynchPrediv = 0xFF; // 同步预分频器值 RTC_InitStructure.RTC_HourFormat = RTC_HourFormat_24; // 24小时格式 RTC_Init(&RTC_InitStructure); // 配置RTC时钟日历 RTC_DateTypeDef RTC_DateStructure; RTC_TimeTypeDef RTC_TimeStructure; RTC_DateStructure.RTC_Year = 0x21; // 年份 RTC_DateStructure.RTC_Month = RTC_Month_November; // 月份 RTC_DateStructure.RTC_Date = 0x01; // 日 RTC_DateStructure.RTC_WeekDay = RTC_Weekday_Sunday; // 星期几 RTC_SetDate(RTC_Format_BCD, &RTC_DateStructure); RTC_TimeStructure.RTC_Hours = 0x10; // 小时 RTC_TimeStructure.RTC_Minutes = 0x30; // 分钟 RTC_TimeStructure.RTC_Seconds = 0x00; // 秒 RTC_SetTime(RTC_Format_BCD, &RTC_TimeStructure); // 使能RTC中断 RTC_ITConfig(RTC_IT_SEC, ENABLE); NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannel = RTC_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); // 使能RTC时钟 RTC_WaitForSynchro(); RTC_Cmd(ENABLE); ``` 在上述代码中,我们首先打开PWR和BKP外设时钟,然后配置RTC时钟使用LSE低速外部晶振。接下来,我们设置RTC的异步预分频器和同步预分频器,以确定RTC时钟的更新频率。然后,我们配置RTC时钟日历,包括时间和日期。最后,我们使能RTC中断,以便在时间更新时进行相应处理,并启用RTC时钟。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Zevalin爱灰灰

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值