实时时钟是一个独立的定时器。RTC模块拥有一组连续计数的计数器,在相应软件配置下,可提供时钟日历的功能。修改计数器的值可以重新设置系统当前的时间和日期。
RTC模块和时钟配置系统(RCC_BDCR寄存器)处于后备区域,即在系统复位或从待机模式唤醒后,RTC的设置和时间维持不变。
系统复位后,对后备寄存器和RTC的访问被禁止,这是为了防止对后备区域(BKP)的意外写操作。
执行以下操作将使能对后备寄存器和RTC的访问:
1、设置寄存器RCC_APB1ENR的PWREN和BKPEN位,使能电源和后备接口时钟
2、设置寄存器PWR_CR的DBP位,使能对后备寄存器和RTC的访问。
通过串口可以设置RTC时钟的日期和时间,设置好后,可以在数码管上显示设定的时间,同时日期和时间也会被实时发送到串口上进行显示;并且系统复位或者断电后,RTC的时钟依然运行
由STM32电源框图可以看到,当主电源VDD掉电后,通过VBAT脚为实时时钟(RTC)和备份寄存器提供电源
因为RTC时钟在系统断电后需要继续工作,就需要用电池连接到VBAT引脚上,给RTC提供电源,当电池被拔掉后,RTC也不能工作了
激活RTC
参数默认即可
选择外部32.768KHz的低速时钟源,直接给到RTC时钟
从时钟树可以看出,RTC时钟源还可以选择HSE的128分频和内部低速时钟LSI RC,那为什么要使用LSE外部低速时钟源呢?
因为HSE在系统断电后是不起振了,无法提供时钟源,而内部LSI随着芯片停止工作也不起振,还有原因是LSE比较精确
Public.c
因为使用到串口输入字符串来设置RTC的时钟,所以与printf函数重定向一样,将 getchar 的底层函数 fgetc 映射到物理串口,后面只需使用getchar()函数即可接收串口发来的信息
/*
* @name fgetc
* @brief fgetc映射到物理串口
* @param None
* @retval ch:已接收的字符
*/
int fgetc(FILE* f)
{
uint8_t ch = 0;
//通过查询方法等待接收
HAL_UART_Receive(&huart_debug,&ch,1,0xFFFF);
return ch;
}
MyRTC.c
设置RTC的日期和时间,日期就是年,月,日,星期,时间是时,分,秒,进入函数会首先判断RTC备份寄存器1的值,因为该寄存器不会被系统复位,断电复位或者待机模式唤醒复位,在设置好一次RTC的日期和时间后,往该寄存器里写入一个随机值,下次就会通过判断该寄存器的值,如果没被修改,则说明已经设置过日期和时间,不用再设置,如果值被修改,则进行设置日期和时间的操作
有两种方法可修改RTC的值,方法一:修改备份寄存器的值,通过触摸按键来实现;方法二:系统断电的时候拔掉VBAT的电池,RTC则停止工作,上电后会重新设置时间
/*
* @name Calendar_Set
* @brief 设置日历
* @param None
* @retval None
*/
static void Calendar_Set()
{
/*上电复位时,读取RTC备份寄存器1的值,如果为0xAAAA,表示已经设置了时间,不需要重新设置
注:0xAAAA是自己随便设定的值,该寄存器不会被系统复位,电源复位或从待机模式唤醒而复位,
所以该寄存器的值在断电后依然保持不变,判断该寄存器是否有预设定值,有就表示已经设置了时间
*/
if(HAL_RTCEx_BKUPRead(&hrtc,RTC_BKP_DR1) != 0xAAAA)
{
printf("开始设置RTC的日期和时间\r\n\r\n");
RTC_Date_Set(); //设置日期
RTC_Time_Set(); //设置时间
//设置完日期和时间后写RTC备份寄存器1的值,表示日期和时间已经设定
HAL_RTCEx_BKUPWrite(&hrtc,RTC_BKP_DR1,0xAAAA);
}
else
{
printf("RTC的日期和时间已经设置\r\n\r\n");
printf("重新设置的方法如下:\r\n");
printf("方法一:长按触摸按键1两秒以上\r\n");
printf("方法二:系统断电,同时拔掉RTC电池\r\n");
}
}
设置RTC日期
定义一个日期结构体变量,从串口接收年,月,日的数据后,调用函数HAL_RTC_SetDate将日期写入到结构体变量中;
设置RTC时间的函数类似,只是最大值不同,时钟是23,分钟是59,秒钟也是59
/*
* @name RTC_Date_Set
* @brief 设置RTC日期
* @param None
* @retval None
*/
static void RTC_Date_Set()
{
RTC_DateTypeDef RTC_DateStruct; //定义一个日期结构体变量
uint8_t SetValue;
printf("=============日期设置===========\r\n");
printf("请输入年份(00-99):20\r\n");
//等待串口设置
SetValue = 0xFF;
while(SetValue == 0xFF)
{
//对串口输入值进行校验后,再赋给SetValue,参数99表示年份的最大值
SetValue = Input_RTC_SetValue(99);
}
printf("年份被设置为:20%02u\r\n",SetValue);
RTC_DateStruct.Year = SetValue;
printf("请输入月份(1-12):\r\n");
SetValue = 0xFF;
while(SetValue == 0xFF)
{
SetValue = Input_RTC_SetValue(12);
if(SetValue == 0x00)
{
printf("月份不能设置为0,请重新输入月份:\r\n");
SetValue = 0xFF;
}
}
printf("月份被设置为:%02u\r\n",SetValue);
RTC_DateStruct.Month = SetValue;
printf("请输入日期(01-31):\r\n");
SetValue = 0xFF;
while(SetValue == 0xFF)
{
SetValue = Input_RTC_SetValue(31);
if(SetValue == 0x00)
{
printf("日期不能设置为0,请重新输入日期:\r\n");
SetValue = 0xFF;
}
}
printf("日期被设置为:%02u\r\n",SetValue);
RTC_DateStruct.Date = SetValue;
//设置日期——二进制数据格式
HAL_RTC_SetDate(&hrtc,&RTC_DateStruct,RTC_FORMAT_BIN);
}
stm32f1xx_hal_rtc.h
通过查看RTC的HAL库头文件可以看到,RTC日期结构体的年份的取值范围是0到99,所以不能直接设置如2022这样的数值,但串口打印年份时可加上2000,数码管显示同理
/**
* @brief RTC Date structure definition
*/
typedef struct
{
uint8_t WeekDay; /*!< Specifies the RTC Date WeekDay (not necessary for HAL_RTC_SetDate).
This parameter can be a value of @ref RTC_WeekDay_Definitions */
uint8_t Month; /*!< Specifies the RTC Date Month (in BCD format).
This parameter can be a value of @ref RTC_Month_Date_Definitions */
uint8_t Date; /*!< Specifies the RTC Date.
This parameter must be a number between Min_Data = 1 and Max_Data = 31 */
uint8_t Year; /*!< Specifies the RTC Date Year.
This parameter must be a number between Min_Data = 0 and Max_Data = 99 */
} RTC_DateTypeDef;
MyRTC.c
Input_RTC_SetValue函数是接收串口的设置值,并判断其有效性
/*
* @name Input_RTC_SetValue
* @brief 输入RTC设置值
* @param None
* @retval None
*/
static uint8_t Input_RTC_SetValue(uint8_t MaxValue)
{
uint8_t SetValue = 0; //返回值
uint8_t Value_Arr[2] = {0}; //串口接收缓存
uint8_t Index = 0;
//以等待方式从串口2接收两个字符
while (Index < 2)
{
//等待串口接收数据
Value_Arr[Index++] = getchar();
//校验数据有效性
if((Value_Arr[Index-1] < '0')||(Value_Arr[Index-1] > '9'))
{
printf("请输入0 到 9之间的数字-->\r\n");
Index--; //下标减1,重新接收
}
}
//接收到的两个字符转化为数值
SetValue = (Value_Arr[0]-'0')*10 +(Value_Arr[1]-'0');
//判断返回值有效性
if(SetValue > MaxValue)
{
printf("请输入 0 到 %d 之间的数字\r\n",MaxValue);
SetValue = 0xFF;
}
return SetValue;
}
只需调用HAL库的RTC获取日期和时间就能得到刚刚通过串口设置的值,通过指针,MyRTC.pRTC_DataStruct保存到文件开头定义的结构体变量中
/*
* @name Calendar_Get
* @brief 获取日历
* @param None
* @retval None
*/
static void Calendar_Get()
{
//获取当前日期
HAL_RTC_GetDate(&hrtc,MyRTC.pRTC_DataStruct,RTC_FORMAT_BIN);
//获取当前时间
HAL_RTC_GetTime(&hrtc,MyRTC.pRTC_TimeStruct,RTC_FORMAT_BIN);
}
System.c
主函数判断RTC的设置标志位RTC_Set_Flag,该标志位初始化为TRUE,所以一上电就会先进行RTC的日期和时间设置
/*
* @name Run
* @brief 系统运行
* @param None
* @retval None
*/
static void Run()
{
if(MyRTC.RTC_Set_Flag == TRUE)
{
MyRTC.RTC_Set_Flag = FALSE;
//设置RTC日期和时间
MyRTC.Calendar_Set();
}
//获取RTC日期和时间
MyRTC.Calendar_Get();
//显示RTC日期和时间
MyRTC.Calendar_Show();
//延时
HAL_Delay(1000);
}
分析框图,RTC的后备区域没有日期寄存器,断电恢复后,没法直接读取日期;如果需要读取断电恢复后日期更新值,有两种方法:
方法一:使用高级些的MCU,比如STM32F4,RTC自带日期寄存器。缺点:MCU成本贵
方法二:利用32位可编程计数器进行计算。具体方法是:假定计数器为0时,为一个起始日期,比如2000年1月1日;设定日期后,减去起始日期,换算成秒钟初始化给计数器;断电后,计数器值不重新装载,与设定日期对比,换算出当前日期。
缺点:
1、不能使用HAL库编程,因为这种方法是自己设计的,没有库函数
2、需要CPU大量计算,效率低;
3、RTC溢出与闹钟功能不可用。