RTC框图
实时时钟(Real-time clock: RTC)是一个独立的计时器。RTC提供一组连续运行的计数器,可以与合适的软件一起使用,以提供时钟日历功能。可以写入计数器值以设置系统的当前时间/日期。
可以选择以下三种作为RTC时钟源:
- HSE时钟进行128分频
- LSE振荡器时钟
- LSI振荡器时钟
有关时钟的更多信息,参考STM32时钟树
RTC核心部分由2个部分构成:
- RTC分频器由
RTC_PRL
、RTC_DIV
构成:RTC_PRL
是预分频装载寄存器,用来保存RTC预分频器的周期计数值。RTC_DIV
是预分频器计数寄存器(只读)。在TR_CLK的每个周期里,RTC预分频器中计数器的值都会被重新设置为RTC_PRL寄存器的值。用户可通过读取RTC_DIV寄存器,以获得预分频计数器的当前值,而不停止分频计数器的工作,从而获得精确的时间测量。当RTC_DIV的值等于RTC_PRL的值,可生成最长为1s的时间基准TR_CLK
- RTC32位可编程计数器由
RTC_CNT
、RTC_ALR
构成RTC_CNT
是32位计数寄存器,存放RTC当前计数值,计数的速率取决于TR_CLK
。分为两个16位寄存器RTC_CNTH
和RTC_CNTL
RTC_ALR
是闹钟(alarm)寄存器,当可编程计数器(RTC_CNT)的值与RTC_ALR中的32位值相等时,触发一个闹钟事件,并且产生RTC闹钟中断。
系统复位后,对后备寄存器和RTC的访问被禁止,这是为了防止对后备区域(BKP)的意外写操作。执行以下操作使能对(Backup)后备寄存器和RTC的访问:
- 通过设置RCC_APB1ENR寄存器的PWREN位和BKPEN位,来开启POWER和BACKUP的时钟
- 通过设置电源控制寄存器(PWR_CR)的DBP位,允许对RTC和Backup寄存器的访问
PWR
电源控制(Power control:PWR),电源框图如下。STM32的工作电压(VDD)为2.0~3.6V。通过内置的电压调节器提供所需的1.8V电源。 当主电源VDD掉电后,可通过VBAT脚为实时时钟(RTC)和备份寄存器提供电源。
备份域(Backup domain)有以下几个部分构成:
- 频率为32K的LSE振荡器
- BKP(备份)寄存器
- RCC_BDCR(备份域控制)寄存器
- RTC
BKP
备份寄存器(Backup Register)是42个16位的寄存器,可用来存储84个字节的用户应用程序数据。他们处在备份域里,当VDD电源被切断,他们仍然由VBAT维持供电,数据不会丢失。当系统在待机模式下被唤醒,或系统复位或电源复位时,他们也不会被复位。BKP可用于RTC校准功能。
RTC显示实时时间
通过串口配置修改将RTC计数寄存器修改为当前时间,开启RTC秒中断(Second Interrupt),1S产生一次RTC中断,并通过串口助手实时显示时间。备份寄存器在这个例程中的作用就是检查RTC是否被配置了。由于使用的最小系统板VBAT引脚没有接电池,所以需要单独供电,断电后BKP的内容会被清空。
整体流程图
通过串口修改RTC外设寄存器的值设为当前时间,配置完成后单片机向串口助手发送数据,使用串口助手显示当前时间。BKP在本例程中的作用就是用于检测RTC是否被配置,通过向BKP_DR1寄存器写入一个任意的值,来判断RTC是否被配置。整体流程图如下:
flowchart TD id0["配置NVIC"] --> id["上电检测BKP寄存器"] --> id1["BKP寄存器是否为0x1234?"] -->|NO| id2["配置RTC并向BKP寄存器写入0x1234"] --> id4["通过串口助手设置RTC的时间"] --> id6 id1 -->|YES| id3["等待RTC的寄存器同步"] --> id5["使能RTC秒中断"] --> id6["清除复位标志位"] --> id7["向串口显示当前时间"]void MyRTC_Init(void)
{
NVIC_Configuration();
if(BKP_ReadBackupRegister(BKP_DR1) != 0x1234)
{
printf("\r\n\n RTC not yet configured....");
RTC_Configuration(); //配置RTC
printf("\r\n RTC configured....");
Time_Adjust();
BKP_WriteBackupRegister(BKP_DR1, 0x1234);
}
else
{
/* 检查电源复位标志位是否被设置 */
if (RCC_GetFlagStatus(RCC_FLAG_PORRST) != RESET)
{
printf("\r\n\n Power On Reset occurred....");
}
/* 检查引脚复位标志位是否被设置 */
else if (RCC_GetFlagStatus(RCC_FLAG_PINRST) != RESET)
{
printf("\r\n\n External Reset occurred....");
}
printf("\r\n No need to configure RTC....");
/* 等待RTC同步 */
RTC_WaitForSynchro();
/* 使能RTC秒中断 */
RTC_ITConfig(RTC_IT_SEC, ENABLE);
/* 等待RTC寄存器上的最后一次写入操作完成 */
RTC_WaitForLastTask();
}
/* 清除复位标志位 */
RCC_ClearFlag();
}
配置RTC
系统复位后,执行以下操作才能实现对(Backup)备份寄存器和RTC的访问:
- 开启PWR、BKP时钟
- 允许访问BACKUP寄存器、RTC外设
{ signal: [
{ name: "RTCCLK", wave: "p..........", period: 2 },
{ name: "RTC_DIV", wave: "=.=.=.=.=.=.=.=.=.=.=.", data: ["0x00", "0x03", "0x02", "0x01", "0x00","0x03", "0x02", "0x01", "0x00", "0x03", "0x02"] },
{ name: "TR_CLK", wave: "0.1.0.....1.0.....1.0.."},
{ name: "RTC_Second", wave: "0.1.0.....1.0.....1.0.."},
{ name: "RTC_CNT", wave: "=...=.......=.......=...", data: ["FFFFFFFD", "FFFFFFFFE", "FFFFFFFFF", "00000000"] },
{ name: "RTC_Overflow", wave:"0.................1.0.."}
],
head:{
text:'RTC_PRL=0x03,计数溢出的波形图'
}
}
- RTCCLK的时钟是来自LSE振荡器的时钟,\(f_{LSE}\)=32.728KHz。由参考手册可得\(f_{TR\_CLK}=\frac{f_{RTCCLK}}{RTC\_PRL[19:0]+1}\),时间基准TR_CLK最大可计时1S,即RTC_Second的时钟周期最长可为1S。要获得1S的时间基准,RTC_PRL寄存器应该写入32767。
- RTC_DIV存放分频计数器当前值,当减为0时,在TR_CLK的上升沿时发生重装载(reload),RTC_DIV的值被重装载为RTC_PRL的值
- RTC_CNT寄存器的值在每个TR_CLK的时钟周期下,值都会递增。RTC_CNT寄存器用于存放系统的当前时间
- RTC_Overflow:当32位的RTC_CNT寄存器计数满了之后,在下个TR_CLK上升沿时,产生一次溢出信号
RTC配置程序代码:
void RTC_Configuration(void)
{
/* 开启PWR、BKP时钟 */
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);
/* 允许访问Backup和RTC寄存器 */
PWR_BackupAccessCmd(ENABLE);
/* 复位BKP */
BKP_DeInit();
/* 使能LSE */
RCC_LSEConfig(RCC_LSE_ON);
/* 等待LSE准备好 */
while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET)
{
}
/* 选择LSE做为RTC时钟源 */
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
/* 使能RTC时钟 */
RCC_RTCCLKCmd(ENABLE);
/* 等待RTC寄存器同步 */
RTC_WaitForSynchro();
/* RTC写寄存器操作必须要这段语句,等待上次对RTC寄存器的写操作完成 */
RTC_WaitForLastTask();
/* 每秒产生一次中断 */
RTC_ITConfig(RTC_IT_SEC, ENABLE);
/* 等待上次对RTC寄存器的写操作完成 */
RTC_WaitForLastTask();
/* 设置预分频 */
RTC_SetPrescaler(32767); //RTC时钟周期 = (32.768KHz) / (32767 + 1) = 1Hz = 1s
/* 等待上次对RTC寄存器的写操作完成 */
RTC_WaitForLastTask();
}
串口修改RTC
通过串口设置RTC计数寄存器的值,printf()
向串口助手发送信息,USART_Scanf()
函数接收来自串口助手的消息。USART的数据帧使用8位有效数据。
串口修改RTC程序代码:
/**
* @brief 调整时间
* @param none
* @retval none
*/
void Time_Adjust(void)
{
/* 等待上次对RTC寄存器的写操作完成 */
RTC_WaitForLastTask();
/* 设置RTC计数器的值 */
RTC_SetCounter(Time_Regulate());
/* 等待上次对RTC寄存器的写操作完成 */
RTC_WaitForLastTask();
}
/**
* @brief 通过串口设置时间
* @param none
* @retval 用秒表示的时间
*/
uint32_t Time_Regulate(void)
{
uint32_t Tmp_HH = 0xFF, Tmp_MM = 0xFF, Tmp_SS = 0xFF;
printf("\r\n==============Time Settings=====================================");
printf("\r\n Please Set Hours");
while(Tmp_HH == 0xFF)
{
Tmp_HH = USART_Scanf(23);
}
printf(": %d", Tmp_HH);
printf("\r\n Please Set Minutes");
while(Tmp_MM == 0xFF)
{
Tmp_MM = USART_Scanf(59);
}
printf(": %d", Tmp_MM);
printf("\r\n Please Set Seconds");
while(Tmp_SS == 0xFF)
{
Tmp_SS = USART_Scanf(59);
}
printf(": %d", Tmp_SS);
/* 将输入的值转换为Counter CNT */
return (Tmp_HH * 60 * 60 + Tmp_MM * 60 + Tmp_SS);
}
/**
* @brief 获取来自串口的数据
* @param value 允许串口输入的最大值
* @retval 转换后的串口的数据
*/
uint8_t USART_Scanf(uint32_t value)
{
uint32_t index = 0;
uint8_t rev[2] = {0, 0};
while(index < 2)
{
/* 等待RXNE = 1 */
while(USART_GetFlagStatus(USART1, USART_FLAG_RXNE) == RESET)
{
}
rev[index++] = USART_ReceiveData(USART1);
if(rev[index - 1] < 0x30 || rev[index - 1] > 0x39)
{
printf("\n\rPlease enter valid number between 0 and 9");
index--;
}
}
index = ((rev[0] - 0x30) * 10) + (rev[1] - 0x30); //获取输入的值
/* 检验 */
if(index > value)
{
printf("\n\rPlease enter valid number between 0 and %d", value);
return 0xFF;
}
return index;
}
显示实时时间
/**
* @brief 将时间按照指定格式显示
* @param value RTC计数寄存器的值
* @retval none
*/
void Time_Display(uint32_t value)
{
/* 当前时间为23:59:59时,复位RTC */
if(RTC_GetCounter() == 0x0001517F)
{
RTC_SetCounter(0x00);
/* Waits until last write operation on RTC registers has finished. */
RTC_WaitForLastTask();
}
uint32_t Tmp_HH = 0x00, Tmp_MM = 0x00, Tmp_SS = 0x00;
Tmp_HH = value / 3600; //小时
Tmp_MM = (value % 3600) / 60; //分钟
Tmp_SS = (value % 3600) % 60; //秒
printf("Time: %0.2d:%0.2d:%0.2d\r", Tmp_HH, Tmp_MM, Tmp_SS);
}
/**
* @brief 显示实时时间
* @param none
* @retval none
*/
void Time_Show(void)
{
while(1)
{
if(time_display == 1)
{
Time_Display(RTC_GetCounter());
time_display = 0; //通过RTC中断来置1
}
}
}
RTC中断处理函数
void RTC_IRQHandler(void)
{
if(RTC_GetITStatus(RTC_IT_SEC) != RESET)
{
time_display = 1;
RTC_ClearITPendingBit(RTC_IT_SEC);
/* Waits until last write operation on RTC registers has finished. */
RTC_WaitForLastTask();
}
}
主函数
int main(void)
{
COM_Init(); //串口初始化,采样数据
MyRTC_Init(); //初始化RTC
Time_Show(); //在无限循环中显示时间
}
演示结果
使用串口助手手动调节RTC来显示当前时间