目录
RTC实时时钟
功能框图
UNIX时间戳
初始化结构体
RTC时间结构体
RTC日期结构体
RTC闹钟结构体
进入和退出配置函数
实验环节1:显示日历
常规配置
RTC配置
测试环节
实验现象
实验环节2:闹钟
常规配置
RTC配置
测试环节
实验现象
RTC实时时钟
STM32的RTC外设,实质上是一个掉电后还继续运行的定时器。类似于通用定时器TIM外设,可以计时和触发中断。
掉电指的是电源VDD断开时为了RTC外设掉电继续运行,必须接上锂电池给STM32的RTC、备份发卡通过Vbat引脚供电。当主电源VDD有效时由VDD给RTC外设供电,当CDD掉电后,由Vbat给RTC外设供电。但无论由什么电源供电,RTC的数据都保存在属于RTC的备份域中,若主电源VDD和Vbat都掉电,则备份域保存的所有数据将丢失。备份域除了RTC模块的寄存器,还有42个16位的寄存器可以在VDD掉电时保存用户程序的数据,系统复位或电源复位时,数据也不会清空。
从RTC的定时器特性来说,是一个32位的递增计数器。时钟源有三种:HSE/128、LSI和LSE。
使用LSI或HSE/8时钟源,在主电源VDD掉电时,这两个时钟来源都会受到影响,因此无法保证RTC正常工作。因此RTC一般使用LSE。在设计中,频率通常为32.768kHz。
在主电源VDD有效并处于待机模式时,RTC还可以配置闹钟事件时STM32退出待机模式。
功能框图
浅灰色部分属于备份域,VDD掉电时可在Vbat的驱动下继续运行。包括了RTC分频器、计数器、闹钟控制器。
若VDD电源有效,RTC可以触发RTC_Second(秒中断)、RTC_Overflow(溢出事件)和RTC_Alarm(闹钟中断)。从结构图可以分析出,定时器溢出中断无法被配置为中断。
若STM32处于待机模式,可由闹钟事件或WKUP事件(外部唤醒事件,属于EXTI模块,不属于RTC)退出待机模式。
闹钟事件在计数器RTC_CNT的值等于闹钟寄存器RTC_ALR的值时触发。
在备份域中所有寄存器都是16位的,RTC控制相关的寄存器也不例外。它的计数器RTC_CNT的32位由RTC_CNTL和RTC_CNTH这两个寄存器组成。
在配置RTC模块的时钟时,通常把输入的32768Hz的RTCCLK进行32768分频得到实际驱动计数器的时钟TR_CLK = RTCCLK / 32768 =1Hz,计数周期为1s,计数器在TR_CLK的驱动下计数,即每秒计数器RTC_CNT的值+1。
由于备份域的存在,使得RTC核具有了完全独立于APB1接口的特性,也因此对RTC寄存器的访问要遵守一定的规则。
系统复位后,默认禁止访问后备寄存器和RTC,防止对后备区域(BKP)的意外写操作。执行以下操作使能对后备寄存器和RTC的访问:
设置RCC_APB1ENR:PWREN、BKPEN位来使能电源和后备接口时钟。
设置PWR_CR:DBP位使能对后备寄存器和RTC的访问。
设置后备寄存器为可写访问后,在第一次通过APB1接口访问RTC时,因为时钟频率的差异,所以必须等待APB1和RTC外设同步,确保被读取出来的RTC寄存器值是正确的。若在同步后,一直没有关闭APB1的RTC外设接口,就不需要再次同步了。
如果内核要对RTC寄存器进行任何写操作,在内核写出写指令后,RTC模块在3个RTCCLK时钟后才开始正式的写RTC寄存器操作。由于RTCCLK的频率比内核主频低得多,所以每次操作后都必须检查RTC关闭操作标志位RTOFF,当这个标志位被置1,写操作才正式完成。
当然,以上操作都具有对应的库函数,不需要具体的查阅寄存器。
UNIX时间戳
RTC_CNT是32位寄存器,可存储的最大值为2^32-1,即约等于136年。
如某个时刻读取计数器的值为2天的秒数,以2011.1.1 0:0:0时间置0计数器的,则可以算出是2011.1.3 0:0:0时间,计数器会在2011+136年左右溢出。定时器被置0的时间为计数元年,相对计时元年的秒数为时间戳(计数器的值)。
大多数操作系统都是利用时间戳和计时元年来计算当前时间的,有个标准:UNIX时间戳和UNIX计时元年。
UNIX计时元年被设置为格林威治时间1970.1.1 0:0:0时间。
在这个计时系统上,使用的是有符号的32位整形变量来保存UNIX时间戳,因此最高位表示符号,时间戳能显示的范围更小了,会在2038.1.19 3:14:07时间溢出。
网页上可搜:UNIX时间戳。可实时查看。
初始化结构体
STM32 HAL库对RTC控制提供了完善的函数。
typedef struct {
uint32_t AsynchPrediv; /* 配置RTC_CLK的异步分频因子(0x00~0x7F ) ,具体由RTC_PRER:PREDIV_A[6:0]配置 */
uint32_t OutPut; /* RTCEx输出通道设置,指定哪一路信号作为RTC的输出,禁止输出/闹钟A输出/闹钟B输出/唤醒输出 */
} RTC_InitTypeDef;
RTC时间结构体
用来设置初始时间,配置的是RTC时间寄存器RTC_TR。
typedef struct {
uint8_t Hours; /* 小时设置。12小时制式时,0~11;24小时制式时,0~23 */
uint8_t Minutes; /* 分钟设置,0~59 */
uint8_t Seconds; /* 秒设置,0~59 */
} RTC_TimeTypeDef;
RTC日期结构体
用来设置初始日期,配置的是RTC日期寄存器RTC_DR。
typedef struct {
uint8_t WeekDay; /* 星期几设置,1~7 */
uint8_t Month; /* 月份设置,1~12 */
uint8_t Date; /* 日期设置,1~31 */
uint8_t Year; /* 年份设置,0~99 */
} RTC_DateTypeDef;
RTC闹钟结构体
用来设置闹钟时间,设置的格式为[星期/日期]-[时]-[分]-[秒],4个字段,每个字段可以设置为有效或无效(MASK)。如果MASK掉[星期/日期]字段,则每天闹钟都会响。
typedef struct {
RTC_TimeTypeDef AlarmTime; /* 设定RTC时间寄存器的值:时/分/秒 */
uint32_t Alarm; /* RTC 闹钟选择:闹钟A、闹钟B */
} RTC_AlarmTypeDef;
进入和退出配置函数
/**
* @brief 进入 RTC 配置模式 .
* @param None
* @retval None
*/
void RTC_EnterConfigMode(void)
{
/* 设置 CNF 位进入配置模式 */
RTC->CRL |= RTC_CRL_CNF;
}
/*
* @brief 退出 RTC 配置模式 .
* @param None
* @retval None
*/
void RTC_ExitConfigMode(void)
{
/* 清空 CNF 位退出配置模式 */
RTC->CRL &= (uint16_t)~((uint16_t)RTC_CRL_CNF);
}
实验环节1:显示日历
常规配置
USART1:带中断,支持printf输出。
RTC配置
RTC_HandleTypeDef hrtc;
/* RTC init function */
void MX_RTC_Init(void)
{
RTC_TimeTypeDef sTime = {0};
RTC_DateTypeDef DateToUpdate = {0};
/* USER CODE BEGIN RTC_Init 1 */
/* 判断是否首次上电 */
if (HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR1) != 0x5050)
{
HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR1, 0x5050); // 插入BKP数值判断
/* USER CODE END RTC_Init 1 */
/** Initialize RTC Only
*/
hrtc.Instance = RTC;
hrtc.Init.AsynchPrediv = RTC_AUTO_1_SECOND;
hrtc.Init.OutPut = RTC_OUTPUTSOURCE_NONE;
if (HAL_RTC_Init(&hrtc) != HAL_OK)
{
Error_Handler();
}
/** Initialize RTC and set the Time and Date
*/
sTime.Hours = 0x0;
sTime.Minutes = 0x0;
sTime.Seconds = 0x0;
if (HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BCD) != HAL_OK)
{
Error_Handler();
}
DateToUpdate.WeekDay = RTC_WEEKDAY_MONDAY;
DateToUpdate.Month = RTC_MONTH_JANUARY;
DateToUpdate.Date = 0x1;
DateToUpdate.Year = 0x0;
if (HAL_RTC_SetDate(&hrtc, &DateToUpdate, RTC_FORMAT_BCD) != HAL_OK)
{
Error_Handler();
}
/* USER CODE BEGIN RTC_Init 2 */
}
/* USER CODE END RTC_Init 2 */
}
void HAL_RTC_MspInit(RTC_HandleTypeDef *rtcHandle)
{
if (rtcHandle->Instance == RTC)
{
HAL_PWR_EnableBkUpAccess(); // 取消BKP区域写保护,才能进行时间保存和计时
/* Enable BKP CLK enable for backup registers */
__HAL_RCC_BKP_CLK_ENABLE(); // 开启BKP时钟
/* RTC clock enable */
__HAL_RCC_RTC_ENABLE();
}
}
void HAL_RTC_MspDeInit(RTC_HandleTypeDef *rtcHandle)
{
if (rtcHandle->Instance == RTC)
{
__HAL_RCC_RTC_DISABLE();
}
}
测试环节
#include "string.h"
uint8_t RxBuffer[20];
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart == &huart1)
{
}
}
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart == &huart1)
{
// A5 5A 00 01 01 00 00 00
if(RxBuffer[0]==0xA5 && RxBuffer[1]==0x5A)
{
RTC_DateTypeDef RtcDate;
RTC_TimeTypeDef RtcTime;
RtcTime.Hours = RxBuffer[5];
RtcTime.Minutes = RxBuffer[6];
RtcTime.Seconds = RxBuffer[7];
if (HAL_RTC_SetTime(&hrtc, &RtcTime, RTC_FORMAT_BCD) != HAL_OK)
{
Error_Handler();
}
// 星期内部自动校正
RtcDate.WeekDay = RTC_WEEKDAY_MONDAY;
RtcDate.Month = RxBuffer[3];
RtcDate.Date = RxBuffer[4];
RtcDate.Year = RxBuffer[2];
if (HAL_RTC_SetDate(&hrtc, &RtcDate, RTC_FORMAT_BCD) != HAL_OK)
{
Error_Handler();
}
memset(RxBuffer, 0, sizeof(RxBuffer));
HAL_UART_Receive_IT(&huart1, (uint8_t *)&RxBuffer, 8);
}
}
}
void test(void)
{
RTC_DateTypeDef RtcDate;
RTC_TimeTypeDef RtcTime;
HAL_UART_Receive_IT(&huart1, (uint8_t *)&RxBuffer, 8);
while(1)
{
HAL_RTC_GetTime(&hrtc, &RtcTime, RTC_FORMAT_BIN); // 读出时间值
HAL_RTC_GetDate(&hrtc, &RtcDate, RTC_FORMAT_BIN); // 一定要先读时间后读日期,这样才能校正星期参数
printf("实时时间:%04d-%02d-%02d %02d:%02d:%02d 星期:%2d\r\n", 2000+RtcDate.Year, RtcDate.Month,
RtcDate.Date, RtcTime.Hours, RtcTime.Minutes, RtcTime.Seconds, RtcDate.WeekDay);//显示日期时间
HAL_Delay(1000);
}
}
实验现象
实验环节2:闹钟
常规配置
USART1:带中断,支持printf输出。
蜂鸣器配置。
RTC配置
RTC_HandleTypeDef hrtc;
void MX_RTC_Init(void)
{
RTC_TimeTypeDef sTime = {0};
RTC_DateTypeDef DateToUpdate = {0};
RTC_AlarmTypeDef sAlarm = {0};
/* USER CODE BEGIN RTC_Init 1 */
/* 判断是否首次上电 */
if (HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR1) != 0x5050)
{
HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR1, 0x5050); // 插入BKP数值判断
/* USER CODE END RTC_Init 1 */
/** Initialize RTC Only
*/
hrtc.Instance = RTC;
hrtc.Init.AsynchPrediv = RTC_AUTO_1_SECOND;
hrtc.Init.OutPut = RTC_OUTPUTSOURCE_ALARM;
if (HAL_RTC_Init(&hrtc) != HAL_OK)
{
Error_Handler();
}
/** Initialize RTC and set the Time and Date
*/
sTime.Hours = 0x0;
sTime.Minutes = 0x0;
sTime.Seconds = 0x0;
if (HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BCD) != HAL_OK)
{
Error_Handler();
}
DateToUpdate.WeekDay = RTC_WEEKDAY_MONDAY;
DateToUpdate.Month = RTC_MONTH_JANUARY;
DateToUpdate.Date = 0x1;
DateToUpdate.Year = 0x0;
if (HAL_RTC_SetDate(&hrtc, &DateToUpdate, RTC_FORMAT_BCD) != HAL_OK)
{
Error_Handler();
}
/** Enable the Alarm A
*/
sAlarm.AlarmTime.Hours = 0x0;
sAlarm.AlarmTime.Minutes = 0x1;
sAlarm.AlarmTime.Seconds = 0x0;
sAlarm.Alarm = RTC_ALARM_A;
if (HAL_RTC_SetAlarm_IT(&hrtc, &sAlarm, RTC_FORMAT_BCD) != HAL_OK)
{
Error_Handler();
}
/* USER CODE BEGIN RTC_Init 2 */
}
/* USER CODE END RTC_Init 2 */
}
void HAL_RTC_MspInit(RTC_HandleTypeDef *rtcHandle)
{
if (rtcHandle->Instance == RTC)
{
HAL_PWR_EnableBkUpAccess();
/* Enable BKP CLK enable for backup registers */
__HAL_RCC_BKP_CLK_ENABLE();
/* RTC clock enable */
__HAL_RCC_RTC_ENABLE();
/* RTC interrupt Init */
HAL_NVIC_SetPriority(RTC_Alarm_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(RTC_Alarm_IRQn);
}
}
void HAL_RTC_MspDeInit(RTC_HandleTypeDef *rtcHandle)
{
if (rtcHandle->Instance == RTC)
{
__HAL_RCC_RTC_DISABLE();
/* RTC interrupt Deinit */
HAL_NVIC_DisableIRQ(RTC_Alarm_IRQn);
}
}
测试环节
#include "string.h"
uint8_t RxBuffer[20];
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart == &huart1)
{
}
}
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart == &huart1)
{
// A5 5A 00 01 01 00 00 00
if(RxBuffer[0]==0xA5 && RxBuffer[1]==0x5A)
{
RTC_DateTypeDef RtcDate;
RTC_TimeTypeDef RtcTime;
RTC_AlarmTypeDef RtcAlarm;
RtcTime.Hours = RxBuffer[5];
RtcTime.Minutes = RxBuffer[6];
RtcTime.Seconds = RxBuffer[7];
if (HAL_RTC_SetTime(&hrtc, &RtcTime, RTC_FORMAT_BCD) != HAL_OK)
{
Error_Handler();
}
// 星期内部自动校正
RtcDate.WeekDay = RTC_WEEKDAY_MONDAY;
RtcDate.Month = RxBuffer[3];
RtcDate.Date = RxBuffer[4];
RtcDate.Year = RxBuffer[2];
if (HAL_RTC_SetDate(&hrtc, &RtcDate, RTC_FORMAT_BCD) != HAL_OK)
{
Error_Handler();
}
RtcAlarm.AlarmTime.Hours = RxBuffer[5];
RtcAlarm.AlarmTime.Minutes = RxBuffer[6];
RtcAlarm.AlarmTime.Seconds = RxBuffer[7] + 0x10;
RtcAlarm.Alarm = RTC_ALARM_A;
if (HAL_RTC_SetAlarm_IT(&hrtc, &RtcAlarm, RTC_FORMAT_BCD) != HAL_OK)
{
Error_Handler();
}
memset(RxBuffer, 0, sizeof(RxBuffer));
HAL_UART_Receive_IT(&huart1, (uint8_t *)&RxBuffer, 8);
}
}
}
void HAL_RTC_AlarmAEventCallback(RTC_HandleTypeDef *hrtc)
{
HAL_GPIO_TogglePin(LED_G_GPIO_Port, LED_G_Pin);
}
void test(void)
{
RTC_DateTypeDef RtcDate;
RTC_TimeTypeDef RtcTime;
初始化
HAL_UART_Receive_IT(&huart1, (uint8_t *)&RxBuffer, 8);
while(1)
{
HAL_RTC_GetTime(&hrtc, &RtcTime, RTC_FORMAT_BIN); // 读出时间值
HAL_RTC_GetDate(&hrtc, &RtcDate, RTC_FORMAT_BIN); // 一定要先读时间后读日期,这样才能校正星期参数
printf("实时时间:%04d-%02d-%02d %02d:%02d:%02d 星期:%2d\r\n", 2000+RtcDate.Year, RtcDate.Month,
RtcDate.Date, RtcTime.Hours, RtcTime.Minutes, RtcTime.Seconds, RtcDate.WeekDay);//显示日期时间
HAL_Delay(1000);
}
}
实验现象
上电运行,LED默认灭。一分钟后LED亮绿灯。
通过串口调试助手发送A55A231101000000,10秒后LED灭。