RTC简介
PART 01MM32的 RTC(实时时钟)是一个独立的定时器,通过配置,可以让它准确地每秒/每毫秒中断一次。RTC 模块拥有一组连续 计数的 计数器,在相应软件配置下,可提供时钟日历的功能。修改计数器的值可以重新设置系统当前的时间和日期。
RTC 模块和时钟配置系统(RCC_BDCR 寄存器)处于后备区域,即在系统复位或从待机模式、停机模式唤醒后,RTC 的设置和时间维持不变。
系统复位后,禁止访问后备寄存器和 RTC,防止对后备区域(BKP)的意外写操作。执行以下操作使能对后备寄存器和 RTC 的访问:
▪ 设置寄存器 RCC_APB1ENR 的 PWREN 和 BKPEN 位来使能 电源和后备 接口 时钟。
▪ 设置寄存器 PWR_CR 的 DBP 位使能对后备寄存器和 RTC 的访问。
当我们需要在掉电之后,又需要 RTC 时钟正常运行的话, 单片机的 VBAT脚需外接 3.3V 的纽扣 电池。当系统掉电之后 VBAT 给 RTC 工作提供供电,RTC 中的数据都会保持在后备寄存器当中,因此能够时刻保存数据。
RTC
主要特征
PART 02
RTC的可编程计数器是32位,该计数器会根据一定的 频率进行计数。这个频率就是RTC的时钟输入频率,在MM32的用户手册上的时钟树可以得知RTC的时钟源可以是以下三种:
▪ HSE时钟除以128
▪ LSE 振荡器时钟
▪ LSI振荡器时钟
RTC有3个专门的屏蔽中断:
▪ 闹钟中断,用来产生一个软件可编程的闹钟中断
▪ 秒/毫秒中断,用来产生一个可编程的周期性中断信号
▪ 溢出中断,指示内部可编程计数器溢出并返回0的状态
RTC
功能描述
PART 03
RTC既然是定时器,那么它一定有一个计数器,这个计数器必须有时钟来源。我们知道家用时钟一般最小单位是秒,为了得到精确的时钟,需要选择一个高精度的LSE,还要匹配最佳的电容,这样才能保证时钟的精确。常用的LSE是32.768KHz,经过32768分频后得到1秒的时钟信号。RTC的计数器就可以每隔1秒向上计数。
如果使能了RTC秒中断,那么计数器每记一次数就会产生中断,这样软件就可以将RTC计数器的值读出来,经过计算得到相应的时间显示到屏幕上。
RTC 还有一个闹钟中断功能,顾名思义,就是到了设定的时间后产生中断。此功能常被用到低功耗模式的唤醒场合,MCU进入低功耗模式,需要每隔一段时间需要唤醒,那么可以采用RTC闹钟的唤醒功能。
RT-Thread
移植
PART 04
RT-Thread的移植讲解在前面的微课堂(第121讲)已经详细介绍,有不懂的小伙伴可以翻看前面的讲解,在此就不再赘述。
主要是创建两个线程,分别为 LED_Task和RTC_Task。创建一个信号量sem,RTC秒中断释放信号量,RTC_Task线程获取信号量后打印时间。
在LED_Task线程,对LED每隔500ms进行翻转一次;
在RTC_Task线程,通过获取信号量的方式来确定时间已经改变并通过串口打印出来,在RTC秒中断里面释放信号量。
实验
描述
PART 05
实验描述
| 利用 MM32 在基于RT-Thread实时操作系统下RTC 实现一个简易的电子时钟。在串口助手中打印时间。
显示格式为 “年-月-日 时:分:秒 星期”
当时间计数为:23:59:59 时将刷新为:00:00:00。
| 硬件连接
| VBAT 引脚需外接电池,外部接32.768KHz晶振和匹配电容。
|
代码
描述
PART 06
01
main 函数分析
#include "led.h"
#include "delay.h"
#include "sys.h"
#include "rtc.h"
#include "uart.h"
#include
struct rt_semaphore sem; //定义信号量
#define THREAD_STACK_SIZE 1024
#define THREAD_PRIORITY 5
/* 线程1控制块及线程栈 */
static struct rt_thread LED_thread;
ALIGN(4)
static rt_uint8_t LED_stack[THREAD_STACK_SIZE];
/* 线程2控制块及线程栈 */
static struct rt_thread RTC_thread;
ALIGN(4)
static rt_uint8_t RTC_stack[THREAD_STACK_SIZE];
int main(void)
{
rt_err_t result;
LED_Init(); //初始化与LED连接的硬件接口
uart_initwBaudRate(115200); //初始化串口
printf("MM32 Dev Board\r\n");
printf("RTC TEST\r\n");
printf("@MindMotion\r\n");
/* 初始化静态信号量,初始值是0 */
rt_sem_init(&sem, "sem", 0, RT_IPC_FLAG_FIFO);//初始化信号量
/* 初始化线程1 */
result = rt_thread_init(&LED_thread, "t1", /* 线程名:t1 */
LED_Task, RT_NULL, /* 线程的入口是LED_Task */
&LED_stack[0], sizeof(LED_stack), /* 线程栈是LED_stack */
THREAD_PRIORITY, 10);
if (result == RT_EOK) /* 如果返回正确,启动线程1 */
rt_thread_startup(&LED_thread);
/* 初始化线程2 */
result = rt_thread_init(&RTC_thread, "t2", /* 线程名:t2 */
RTC_Task, RT_NULL, /* 线程的入口是RTC_Task */
&RTC_stack[0], sizeof(RTC_stack), /* 线程栈是RTC_stack */
THREAD_PRIORITY - 1, 10);
if (result == RT_EOK) /* 如果返回正确,启动线程2 */
rt_thread_startup(&RTC_thread);
return 0;
}
因为我们在实验中需要用到串口,所以调用 uart_initwBaudRate ()函数将串口配置好。RTC线程要与RTC_IRQHandler秒中断做同步,需要一个信号量sem,并在main函数初始化一下信号量sem。下面就是新建2个线程,一个是用来翻转LED,另一个用来打印RTC时间信息。
02
RTC和LED线程分析
/* 线程1入口 */
static void LED_Task(void* parameter)
{
while (1)
{
GPIOA->ODR ^= GPIO_Pin_8; //LED翻转
rt_thread_delay(500);
} /* 死循环 */
}
/* 线程2入口 */
static void RTC_Task(void* parameter)
{
char str[20];
while(RTC_Init()) //RTC初始化 ,一定要初始化成功
{
printf("RTC ERROR! \r\n");
rt_thread_delay(800);
printf("RTC Trying...\r\n");
}
while (1)
{
rt_sem_take(&sem, RT_WAITING_FOREVER);//永久等待信号量
sprintf(str,"%02d",calendar.w_year);
printf("%s-",str); //打印年份
sprintf(str,"%02d",calendar.w_month);
printf("%s-",str); //打印月份
sprintf(str,"%02d",calendar.w_date);
printf("%s ",str); //打印日期
sprintf(str,"%02d",calendar.hour);
printf("%s:",str); //打印小时
sprintf(str,"%02d",calendar.min);
printf("%s:",str); //打印分钟
sprintf(str,"%02d",calendar.sec);
printf("%s ",str); //打印秒钟
switch(calendar.week) //打印星期
{
case 0:
printf("Sunday \r\n");
break;
case 1:
printf("Monday \r\n");
break;
case 2:
printf("Tuesday \r\n");
break;
case 3:
printf("Wednesday\r\n");
break;
case 4:
printf("Thursday \r\n");
break;
case 5:
printf("Friday \r\n");
break;
case 6:
printf("Saturday \r\n");
break;
}
}
}
//RTC时钟中断
//每秒触发一次
//extern u16 tcnt;
void RTC_IRQHandler(void)
{
if (RTC_GetITStatus(RTC_IT_SEC) != RESET)//秒钟中断
{
RTC_Get();//更新时间
rt_sem_release(&sem); //释放信号量
}
if(RTC_GetITStatus(RTC_IT_ALR)!= RESET)//闹钟中断
{
RTC_ClearITPendingBit(RTC_IT_ALR); //清闹钟中断
}
RTC_ClearITPendingBit(RTC_IT_SEC|RTC_IT_OW); //清闹钟中断
RTC_WaitForLastTask();
}
LED_Task线程就做了一件事,那就是对PA8脚进行翻转,从了达到LED闪烁的效果,LED亮灭各500ms的时间。
RTC_Task线程,进入该线程的第一件事就是初始化RTC,在后面详细讲解RTC的初始化。初始化通过后,就开始永久等待信号量被RTC秒中断释放,信号量一旦释放,表示秒增加了,此时就开始更新并打印出时间,rt_sem_take(&sem, RT_WAITING_FOREVER);这个是等待信号量被释放,不然线程不会往下执行。在RTC秒中断里面有rt_sem_release(&sem);对信号量进行释放。
03
RTC_Init()代码分析
//实时时钟配置
//初始化RTC时钟,同时检测时钟是否工作正常
//BKP->DR1用于保存是否第一次配置的设置
//返回0:正常
//其他:错误代码
u8 RTC_Init(void)
{
//检查是不是第一次配置时钟
u8 temp=0;
//使能PWR和BKP外设时钟
RCC_APB1PeriphclockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);
//使能后备寄存器访问
PWR_BackupAccessCmd(ENABLE);
//从指定的后备寄存器中读出数据:读出了与写入的指定数据不相乎
if (BKP_ReadBackupRegister(BKP_DR1) != 0x5050)
{
BKP_DeInit(); //复位备份区域
RCC_LSEConfig(RCC_LSE_ON); //设置外部低速晶振(LSE),使用外设低速晶振
//检查指定的RCC标志位设置与否,等待低速晶振就绪
while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET)
{
temp++;
rt_thread_delay(10);
if(temp>=250)
return 1;//初始化时钟失败,晶振有问题
}
//设置RTC时钟(RTCCLK),选择LSE作为RTC时钟
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
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_WaitForSynchro(); //等待最近一次对RTC寄存器的写操作完成
RTC_Set(2020,10,29,10,42,55); //设置时间
RTC_ExitConfigMode(); //退出配置模式
BKP_WriteBackupRegister(BKP_DR1, 0X5050);//向指定的后备寄存器中写入用户程序数据
}
else//系统继续计时
{
RTC_WaitForSynchro(); //等待最近一次对RTC寄存器的写操作完成
RTC_ITConfig(RTC_IT_SEC, ENABLE); //使能RTC秒中断
RTC_WaitForLastTask(); //等待最近一次对RTC寄存器的写操作完成
}
RTC_NVIC_Config();//RCT中断分组设置
RTC_Get();//更新时间
return 0; //ok
}
在RTC_Init()函数里面 if 部分首先读取备份寄存器里面的值,看看备份寄存器里面的值是否正确(如果 RTC 曾经被设置过的话,备份寄存器里面的值为 0x5050)或判断这是不是第一次对 RTC 编程。如果这两种情况有任何一种发生的话,则初始化 RTC,并往串口助手打印出相应的调试信息。初始化好 RTC 之后,调用函数 RTC_Set ();(在 rtc.c 中实现)初始化时间值。
初始化完后,RTC 时钟就运行起来了。设置好时间后,把 0x5050这个值写入 RTC 的备份寄存器,这样当我们下一次上电时就不用重新输入 RTC 里面的时间值了。
如果 RTC 值曾经被设置过,则进入 else部分。else部分不需要重新设置 RTC 里面的时间值。
一切就绪之后,在main函数里面将我们的时间显示在电脑的串口助手上。然后在串口助手上显示出来,效果图如下:
以前我们想实现日历功能时还需通过MCU控制时钟 芯片,如 DS1302 或DS12C887等,而现在我们只需要用MCU内部的一个定时器就能搞定。通过用户编程只用 RTC这个定时器就能实现我们需要的超级日历功能。
0
|
|
|
|