一、RTC简介
RTC(Real Time Clock)实时时钟,它是一个独立的定时器。RTC模块拥有一组连续计数的计数器,在相应软件配置下,可提供时钟日历的功能。修改计数器的值可以重新设置系统当前的时间和日期。
RTC模块和时钟配置都是在后备区域,无论单片机处于何种状态,只要保证后备区正常供电,RTC就会一直工作。
二、STM32的RTC
2.1 主要特性
• 可编程的预分频系数:分频系数最高为2^20
• 32位的可编程计数器,可用于较长时间段的测量
• 可以选择以下三种RTC的时钟源 ─ HSE时钟除以128 ─ LSE振荡器时钟 ─ LSI振荡器时钟
• 3个专门的可屏蔽中断 ─ 闹钟中断,用来产生一个软件可编程的闹钟中断 ─ 秒中断,用来产生一个可编程的周期性中断信号(最长可达1秒) ─ 溢出中断,指示内部可编程计数器溢出并回转为0的状态
22.2 RTC框图介绍
RTC框图
• RTCCLK通常选择低功耗32.768kHz外部晶振(LSE)
• RTC预分频器通常设置为32768,LES时钟经过RTC预分频器,输入频率变为1Hz,也就是1秒
• RTC_CNT输入时钟为1Hz时,1s加1次
• RTC_ALR是用来做闹钟的,RTC_CNT的值会与RTC_ALR的值进行比较,二者相等时,会产生闹钟中断
三、访问后备区域步骤
STM32系统复位之后,对后备寄存器和RTC的访问被禁止,这是为了防止对后备区域(BKP)的意外写操作。执行以下操作,可以访问后备区域寄存器
• 设置寄存器RCC_APB1ENR的PWREN和BKPEN位,使能电源和后备接口时钟
• 设置寄存器PWR_CR的DBP位,使能对后备寄存器和RTC的访问
完成上面的设置之后,就可以操作后备寄存器。第一次通过APB1总线访问RTC时,需要等待APB1和RTC同步,确保读取出来的RTC的寄存器值是正确的。如果同步之后,一直没有关闭APB1和RTC外设接口,就不需要再同步了。
如果内核需要对RTC寄存器写入数据,在内核发送指令后,RTC会在3个RTCCLK时钟之后,开始写入数据。每次写入时,必须要检查RTC关闭操作标志位RTOFF是否置1来判断是否写操作完成。
四、RTC配置步骤
• 使能电源时钟和后备域时钟,开启RTC后备寄存器写访问
• 复位备份区域,开启外部低速振荡器(LSE)
• 选择RTC时钟,并使能
• 设置RTC的分频系数,配置RTC时钟
• 更新配置,设置RTC中断分组
• 编写RTC中断服务函数
五、RTC程序配置
55.1 RTC结构体定义
- // RTC结构体
- typedef struct
- {
- // 时分秒
- u8 hour;
- u8 min;
- u8 sec;
-
- // 年月日周
- u16 w_year;
- u8 w_month;
- u8 w_date;
- u8 week;
- }_calendar;
复制代码
5.2 RTC初始化函数
- /*
- *==============================================================================
- *函数名称:RTC_Init
- *函数功能:初始化RTC
- *输入参数:无
- *返回值:0:成功;1:失败
- *备 注:无
- *==============================================================================
- */
- u8 RTC_Init (void)
- {
- u8 temp=0; // 超时监控变量
- // 结构体定义
- NVIC_InitTypeDef NVIC_InitStructure;
-
- // 使能PWR和BKP外设时钟
- RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);
- PWR_BackupAccessCmd(ENABLE); // 使能后备寄存器访问
-
- // 检测是否是第一次配置RTC
- // 配置时会想RTC寄存器写入0xA0A0,如果读出的数据不是0xA0A0,认为是第一次配置RTC
- if (BKP_ReadBackupRegister(BKP_DR1) != 0xA0A0)
- {
- BKP_DeInit(); // 复位备份区域
- RCC_LSEConfig(RCC_LSE_ON); // 设置外部低速晶振(LSE),使用外设低速晶振
-
- // 等待低速晶振就绪
- while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET&&temp<250)
- {
- temp++;
- delay_ms(10);
- }
- // 初始化时钟失败,晶振有问题
- if(temp>=250)
- {
- return 1;
- }
-
- RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); // 设置RTC时钟(RTCCLK),选择LSE作为RTC时钟
- RCC_RTCCLKCmd(ENABLE); // 使能RTC时钟
- RTC_WaitForLastTask(); // 等待最近一次对RTC寄存器的写操作完成
-
- RTC_WaitForSynchro(); // 等待RTC寄存器同步
- RTC_ITConfig(RTC_IT_SEC, ENABLE); // 使能RTC秒中断
- RTC_WaitForLastTask(); // 等待最近一次对RTC寄存器的写操作完成
-
- RTC_EnterConfigMode(); // 允许配置
- RTC_SetPrescaler(32767); // 设置RTC预分频的值
- RTC_WaitForLastTask(); // 等待最近一次对RTC寄存器的写操作完成
-
- RTC_Set_Date(2023,6,26,11,15,00); // 设置初始时间
- RTC_ExitConfigMode(); // 退出配置模式
- BKP_WriteBackupRegister(BKP_DR1, 0XA0A0); // 向指定的后备寄存器中写入用户程序数据
- }
- // 系统继续计时
- else
- {
- RTC_WaitForSynchro(); // 等待最近一次对RTC寄存器的写操作完成
- RTC_ITConfig(RTC_IT_SEC, ENABLE); // 使能RTC秒中断
- RTC_WaitForLastTask(); // 等待最近一次对RTC寄存器的写操作完成
- }
-
- // 配置RTC中断分组
- NVIC_InitStructure.NVIC_IRQChannel = RTC_IRQn; // RTC全局中断
- NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; // 先占优先级1位,从优先级3位
- NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; // 先占优先级0位,从优先级4位
- NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; // 使能该通道中断
- NVIC_Init(&NVIC_InitStructure); // 根据NVIC_InitStruct中指定的参数初始化外设NVIC寄存器
-
- RTC_Get_CurDate(); // 获取当前时间
- return 0; // 配置成功
- }
复制代码
初始化函数使用时,可以用while等待初始化成功,但是需要增加一个超时检测,这里简单给出一个写法,如果1s内,RTC没有初始化成功,直接跳过
- u32 tempVar = 0; // 初始化RTC时的超时计数变量
-
- while (RTC_Init() && tempVar < 100) // RTC初始化
- {
- delay_ms (10);
- // 10ms自加1
- tempVar = tempVar + 1;
- }
复制代码
5.3 设置年月日,时分秒
- /*
- *==============================================================================
- *函数名称:RTC_Set_Date
- *函数功能:设置RTC的年月日,时分秒
- *输入参数:无
- *返回值:0:成功;1:失败
- *备 注:时间范围为1970年到2099年,可修改
- *==============================================================================
- */
- u8 RTC_Set_Date (u16 syear,u8 smon,u8 sday,u8 hour,u8 min,u8 sec)
- {
- u16 t;
- u32 seccount=0;
-
- // 判断是否为合法年份
- if(syear < 1970 || syear > 2099)
- {
- return 1;
- }
-
- for(t = 1970;t < syear;t ++) // 把所有年份的秒钟相加
- {
- // 闰年的秒钟数
- if(Is_Leap_Year(t))
- {
- seccount += 31622400;
- }
- // 平年的秒钟数
- else
- {
- seccount += 31536000;
- }
- }
-
- smon -= 1;
-
- for(t = 0;t < smon;t ++) // 把前面月份的秒钟数相加
- {
- seccount += (u32)mon_table[t] * 86400; // 月份秒钟数相加
- // 闰年2月份增加一天的秒钟数
- if(Is_Leap_Year(syear) && t == 1)
- {
- seccount += 86400;
- }
- }
- seccount += (u32)(sday-1) * 86400; // 把前面日期的秒钟数相加
-
- seccount += (u32)hour * 3600; // 小时秒钟数
-
- seccount += (u32)min*60; // 分钟秒钟数
-
- seccount += sec; // 最后的秒钟加上去
- RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE); // 使能PWR和BKP外设时钟
- PWR_BackupAccessCmd(ENABLE); // 使能RTC和后备寄存器访问
-
- RTC_SetCounter(seccount); // 设置RTC计数器的值
- RTC_WaitForLastTask(); // 等待最近一次对RTC寄存器的写操作完成
- return 0;
- }
复制代码
5.4 判断闰年函数
- /*
- *==============================================================================
- *函数名称:Is_Leap_Year
- *函数功能:判断输入年份是否为闰年
- *输入参数:无
- *返回值:0:不是闰年;1:是闰年
- *备 注:四年一闰;百年不闰,四百年再闰
- *==============================================================================
- */
- u8 Is_Leap_Year (u16 year)
- {
- // 是否能被4整除
- if(year % 4 == 0)
- {
- // 是否能被100整除
- if(year % 100 == 0)
- {
- // 如果以00结尾,还要能被400整除
- if(year % 400 == 0)
- {
- return 1;
- }
- // 是100的倍数,但是不是400的倍数
- else
- {
- return 0;
- }
- }
- // 是4的倍数,不是100的倍数
- else
- {
- return 1;
- }
- }
- // 不是4的倍数
- else
- {
- return 0;
- }
- }
复制代码
5.5 获取当前年月日,时分秒
5.6 获取星期几
该函数设计是根据蔡勒公式设计,程序如下
- /*
- *==============================================================================
- *函数名称:RTC_Get_Week
- *函数功能:获取当前是星期几
- *输入参数:year:当前年;month:当前月;day:当前日
- *返回值:星期几
- *备 注:无
- *==============================================================================
- */
- u8 RTC_Get_Week (u16 year,u8 month,u8 day)
- {
- u16 temp;
- u8 yearH,yearL;
-
- yearH = year / 100;
- yearL = year % 100;
-
- // 如果为21世纪,年份数加100
- if (yearH > 19)
- {
- yearL += 100;
- }
-
- // 所过闰年数只算1900年之后的
- temp = yearL + yearL / 4;
- temp = temp % 7;
- temp = temp + day + table_week[month - 1];
-
- if (yearL % 4 == 0 && month < 3)
- {
- temp --;
- }
-
- return(temp % 7);
- }
复制代码
5.7 中断服务函数
- /*
- *==============================================================================
- *函数名称:RTC_IRQHandler
- *函数功能:RTC中断服务函数
- *输入参数:无
- *返回值:无
- *备 注:更新时间
- *==============================================================================
- */
- void RTC_IRQHandler(void)
- {
- // 秒中断
- if (RTC_GetITStatus(RTC_IT_SEC) != RESET)
- {
- RTC_Get_CurDate(); // 获取当前时间
- // 串口打印当前时间
- printf("RTC Time:%d-%d-%d %d:%d:%d Week:%d\n",calendar.w_year,calendar.w_month,calendar.w_date,
- calendar.hour,calendar.min,calendar.sec,calendar.week);
- }
- RTC_ClearITPendingBit(RTC_IT_SEC | RTC_IT_OW); //清除秒中断标志位
- RTC_WaitForLastTask(); // 等待最近一次对RTC寄存器的写操作完成
- }
复制代码
六、拓展
在实际使用时,通常会通过网络授时,也就是利用WIFI模块连接网络,请求API获得初始时间。但是可能会存在些许差异。比如请求API后,获得的时间为2023.06.26.14:48:00。实际单片机解析出时间时已经过去了几秒或者十几秒,或者其他问题导致了实际解析出时间后已经与实际值有差距。此时就需要对时间进行矫正。博主在实际应用时差了14s,这里贴一下当时的矫正程序。可能大家用不到,这里只是觉得思考的过程有意思,所以贴出来分享一下。
- void RTC_Time_Correct(void) // 开机时间校正
- {
- // 加14秒不满1分钟
- if (gTimeSec < 46)
- {
- gTimeSec = gTimeSec + 14;
- }
- // 加14秒满1分钟
- else if (gTimeSec >= 46)
- {
- gTimeSec = gTimeSec + 14 - 60;
-
- // 分钟数需要加1
- // 加1分钟不满1小时
- if (gTimeMin < 59)
- {
- gTimeMin = gTimeMin + 1;
- }
- // 分钟数加1满1小时
- else if (gTimeMin == 59)
- {
- gTimeMin = 0;
-
- // 小时数需要加1
- // 加1小时不满1天
- if (gTimeHour < 23)
- {
- gTimeHour = gTimeHour + 1;
- }
- // 加1小时满1天
- else if (gTimeHour == 23)
- {
- gTimeHour = 0;
- // 天数需要加1
- // 天数小于28直接加1
- if (gTimeDay < 28)
- {
- gTimeDay = gTimeDay + 1;
- }
- // 天数等于28
- else if (gTimeDay == 28)
- {
- // 当前为二月
- if (gTimeMon == 2)
- {
- // 闰年
- if (Is_Leap_Year(gTimeYear))
- {
- gTimeDay = gTimeDay + 1;
- }
- // 当前为2月且不是闰年
- else
- {
- gTimeDay = 0; // 天数置零
- gTimeMon = gTimeMon + 1; // 月份加1
- }
- }
- }
- // 天数等于30
- else if (gTimeDay == 30)
- {
- // 当前月份只有30天
- if (gTimeMon == 2 || gTimeMon == 4 || gTimeMon == 6 || gTimeMon == 9
- || gTimeMon == 11)
- {
- gTimeDay = 0;
- gTimeMon = gTimeMon + 1;
- }
- // 当前月份有31天
- else
- {
- gTimeDay = gTimeDay + 1;
- }
- }
- // 天数等于31
- else if (gTimeDay == 31)
- {
- gTimeDay = 0;
-
- // 加1月不满1年
- if (gTimeMon != 12)
- {
- gTimeMon = gTimeMon + 1;
- }
- else
- {
- gTimeMon = 1;
- gTimeYear = gTimeYear + 1;
- }
- }
- }
- }
- }
- }
复制代码
转载自: 二土电子
如有侵权请联系删除
|