STM32速成笔记(九)—RTC

简介: 本文详细介绍了RTC模块,介绍了STM32的RTC的特性,框图,配置步骤,并给出了详细的程序设计。最后,针对实际使用时可能遇到的问题给出了解决方法以及程序。


🎀 文章作者:二土电子
🐸 期待大家一起学习交流!


一、RTC简介

RTC(Real Time Clock)实时时钟,它是一个独立的定时器。RTC模块拥有一组连续计数的计数器,在相应软件配置下,可提供时钟日历的功能。修改计数器的值可以重新设置系统当前的时间和日期。

RTC模块和时钟配置都是在后备区域,无论单片机处于何种状态,只要保证后备区正常供电,RTC就会一直工作。

二、STM32的RTC

2.1 主要特性

  • 可编程的预分频系数:分频系数最高为2^20
  • 32位的可编程计数器,可用于较长时间段的测量
  • 可以选择以下三种RTC的时钟源
     ─ HSE时钟除以128
     ─ LSE振荡器时钟
     ─ LSI振荡器时钟
    
  • 3个专门的可屏蔽中断

     ─ 闹钟中断,用来产生一个软件可编程的闹钟中断
     ─ 秒中断,用来产生一个可编程的周期性中断信号(最长可达1秒)
     ─ 溢出中断,指示内部可编程计数器溢出并回转为0的状态
    

    2.2 RTC框图介绍

    2e2c25d03c584fcd5f73e23b900744ad_8effb97e534e484e95a20face785e0e4.png

  • 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程序配置

    5.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 获取当前年月日,时分秒

/*
 *==============================================================================
 *函数名称:RTC_Get_CurDate
 *函数功能:获取当前年月日,时分秒
 *输入参数:无
 *返回值:0:成功;1:失败
 *备  注:无
 *==============================================================================
 */
u8 RTC_Get_CurDate (void)
{
   
   
    // 存储上一次的总天数值,用来监测时间变化是否超过一天
    static u16 daycnt = 0;
    u32 timecount = 0; 
    // 临时计算变量
    u32 temp = 0;
    u16 temp1 = 0;

    timecount = RTC_GetCounter();   // 获取当前总秒数

     temp = timecount / 86400;   // 得到总天数

    // 超过一天了
    if(daycnt != temp)
    {
   
   
        daycnt = temp;   // 更新当前总天数值

        temp1 = 1970;   // 从1970年开始,计算当前年份
        while(temp >= 365)
        {
   
   
            // 是闰年
            if(Is_Leap_Year(temp1))
            {
   
   
                // 已经过完了366天
                if(temp >= 366)
                {
   
   
                    temp -= 366;   // 闰年的天数
                }
                // 刚过完365天,当前是第366天
                else
                {
   
   
                    temp1 ++;   // 年份加1
                    break;
                }
            }
            // 是平年
            else
            {
   
   
                temp -= 365;   // 平年的天数
            }
            temp1 ++;   // 年份加1
        }

        calendar.w_year = temp1;   // 得到年份
        temp1=0;   // 清零临时计算变量

        // 此时temp为小于一年的天数,开始计算当前月份
        while(temp >= 28)   // 超过了一个月
        {
   
   
            // 当年是闰年的2月份
            if(Is_Leap_Year(calendar.w_year) && temp1 == 1)
            {
   
   
                // 是闰年的二月份且天数大于等于29天
                if(temp >= 29)
                {
   
   
                    temp -= 29;   // 闰年的2月份天数
                }
                // 是闰年的2月份,天数小于闰年2月份天数
                else
                {
   
   
                    break;
                }
            }
            // 是平年
            else 
            {
   
   
                // 查询月份天数表
                if(temp >= mon_table[temp1])
                {
   
   
                    // 超过当月天数,减去
                    temp -= mon_table[temp1];
                }
                else
                {
   
   
                    break;
                }
            }
            temp1 ++;   // 月份加1
        }
        // 加1是因为月份表索引是0~11
        calendar.w_month = temp1 + 1;   // 得到月份
        // 当前日期为已经过去的天数加1
        calendar.w_date = temp + 1;   // 得到日期 
    }
    temp = timecount % 86400;   // 得到秒钟数          
    calendar.hour = temp / 3600;   // 小时
    calendar.min = (temp % 3600) / 60;   // 分钟    
    calendar.sec = (temp % 3600) % 60;   // 秒钟
    calendar.week = RTC_Get_Week(calendar.w_year,calendar.w_month,calendar.w_date);   // 获取星期

    return 0;
}

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;
                    }
                }
            }
        }
    }
}
相关文章
|
5月前
|
API 芯片
STM32 使用HAL库调试内部RTC经验总结
STM32 使用HAL库调试内部RTC经验总结
227 1
|
4月前
|
存储 物联网 芯片
STM32速成笔记(十四)—串口IAP
本文介绍了什么是IAP,IAP有什么作用,如何实现IAP。最后,给出了IAP的实现程序。
84 0
STM32速成笔记(十四)—串口IAP
|
4月前
|
芯片 内存技术
STM32速成笔记(十三)—低功耗模式
本文介绍了三种STM32低功耗模式的进入和退出方法,针对待机唤醒给出了程序设计。
108 0
STM32速成笔记(十三)—低功耗模式
|
4月前
|
存储 芯片 内存技术
STM32速成笔记(十二)—Flash闪存
本文简单介绍了什么是Flash。针对STM32F1的Flash做了详细介绍,介绍了操作Flash的步骤,并且给出了程序设计。最后,介绍了一些注意事项。
29 0
STM32速成笔记(十二)—Flash闪存
|
4月前
|
存储 芯片
STM32速成笔记(十一)—EEPROM(AT24C02)
本文详细介绍了什么是AT24C02,介绍了它的引脚,读/写时序,给出了应用实例和详细的程序设计。最后,简单介绍了AT24C02的应用场景。
128 0
STM32速成笔记(十一)—EEPROM(AT24C02)
|
4月前
STM32速成笔记(十)—IWDG
本文详细介绍了什么是IWDG,STM32的IWDG特性,框图和配置步骤。此外,给出了STM32的IWDG配置程序。通过一个简单的应用实例,展示了IWDG的配置和使用方法。
26 0
STM32速成笔记(十)—IWDG
|
4月前
|
存储 Perl
STM32速成笔记(八)—DMA
本文介绍了DMA的概念,用途。对于STM32F103ZET6的DMA做出了详细地介绍,给出了DMA配置步骤。最后,以配置DMA搬运ADC转换结果为例,给出了DMA的配置和使用方法。
71 0
STM32速成笔记(八)—DMA
|
4月前
|
存储 传感器
STM32速成笔记(七)—ADC
本文介绍了ADC的概念,用途,针对STM32的ADC做出了详细介绍,给出了配置步骤,配置程序。通过一个简单的小项目展示了ADC的配置和使用方法。此外,还针对如何利用定时器触发AD转换,如何采集交流信号,如何计算交流信号有效值进行了介绍,并给出了程序设计。
70 0
STM32速成笔记(七)—ADC
|
4月前
STM32速成笔记(六)—定时器
本文介绍了定时器的概念,作用。针对STM32F1的通用定时器做了详细介绍。此外,介绍了PWM的概念,用途以及STM32F1的PWM,给出了PWM频率的计算方法。最后通过介绍利用定时器的更新中断和PWM这两种方法实现呼吸灯,展示了定时器和PWM的配置步骤,并给出了详细的程序设计。另外,介绍了利用定时器实现按键长短按的检测方法。
97 0
STM32速成笔记(六)—定时器
|
4月前
|
芯片
STM32速成笔记(五)—串口通信
本文介绍了串口通信的概念,用途以及一些相关概念。介绍了如何进行printf重定向,如何根据接收到的特定信息,执行特定操作。此外,本文以通过上位机发送特殊指令控制LED亮灭的小项目,给出了详细的配置方法和程序设计。
107 0
STM32速成笔记(五)—串口通信