STM32 HAL库RTC复位丢失年月日的解决办法
- 0.前言
- 一、实现方式
- 1.CubeMX配置:
- 2.MX_RTC_Init()函数修改
- 2.编写手动解析函数
- 二、总结
参考文章:stm32f1 cubeMX RTC 掉电后日期丢失的问题
0.前言
最近在使用STM32F103做RTC实验时,发现RTC复位后时间正常,但是日期丢失的问题。这个问题在之前使用的标准库中没有遇到,说明是HAL的bug,经过对HAL_RTC_SetDate()和HAL_RTC_GetDate()的解析之后,发现在上电初始化时,HAL库直接简单粗暴的对日期时间戳进行了重置,导致无法读取。
经过多方查找,目前使用较多且较简单的方法是:直接将日期数据写入备份寄存器中,上电时重新进行获取。但是这种方法存在一个问题,假如掉电后经历了日期跨越,那么日期数据就不再准确。所以这里参考标准库的实现方式,直接将HAL库中的初始化过程注释掉,直接从RTC的时间戳寄存器中获取数据,然后手动进行解析。
一、实现方式
1.CubeMX配置:
直接使能RTC功能即可,日期可以不进行设置,后续手动进行设置。
2.MX_RTC_Init()函数修改
为了尽量保持CubeMX的生成格式,防止后续重新生成时自己的代码被覆盖,这里直接在MX_RTC_Init()函数中,使用宏定义注释掉HAL的日期初始化流程:
将添加的宏定义放在 USER CODE BEGIN 和 USER CODE END之间,即可保证重新生成时不被刷新。
2.编写手动解析函数
rtc.h:
在头文件中添加以下代码:
其中包括日历相关的结构体定义,以及日历的全局变量。手动初始化、设置日期、获取日期的相关函数。
rtc.c
在rtc的相关的功能代码中,添加以下代码段:
/* USER CODE BEGIN 0 */
_calendar_obj calendar; // 时钟结构体
// 月份数据表
const uint8_t table_week[12] = {0, 3, 3, 6, 1, 4, 6, 2, 5, 0, 3, 5}; // 月修正数据表
// 平年的月份日期表
const uint8_t mon_table[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};// 判断是否是闰年函数
// 月份 1 2 3 4 5 6 7 8 9 10 11 12
// 闰年 31 29 31 30 31 30 31 31 30 31 30 31
// 非闰年 31 28 31 30 31 30 31 31 30 31 30 31
// year:年份
// 返回值:该年份是不是闰年.1,是.0,不是
static uint8_t Is_Leap_Year(uint16_t year)
{if (year % 4 == 0) // 必须能被4整除{if (year % 100 == 0){if (year % 400 == 0)return 1; // 如果以00结尾,还要能被400整除elsereturn 0;}elsereturn 1;}elsereturn 0;
}// 获得现在是星期几
// 功能描述:输入公历日期得到星期(只允许1901-2099年)
// year,month,day:公历年月日
// 返回值:星期号
static uint8_t RTC_Get_Week(uint16_t year, uint8_t month, uint8_t day)
{uint16_t temp2;uint8_t yearH, yearL;yearH = year / 100;yearL = year % 100;// 如果为21世纪,年份数加100if (yearH > 19)yearL += 100;// 所过闰年数只算1900年之后的temp2 = yearL + yearL / 4;temp2 = temp2 % 7;temp2 = temp2 + day + table_week[month - 1];if (yearL % 4 == 0 && month < 3)temp2--;return (temp2 % 7);
}void RTC_Set(uint16_t syear, uint8_t smon, uint8_t sday, uint8_t hour, uint8_t min, uint8_t sec)
{uint16_t t;uint32_t seccount = 0;if (syear < 1970 || syear > 2099)return;for (t = 1970; t < syear; t++) // 把所有年份的秒钟相加{if (Is_Leap_Year(t))seccount += 31622400; // 闰年的秒钟数elseseccount += 31536000; // 平年的秒钟数}smon -= 1;for (t = 0; t < smon; t++) // 把前面月份的秒钟数相加{seccount += (uint32_t)mon_table[t] * 86400; // 月份秒钟数相加if (Is_Leap_Year(syear) && t == 1)seccount += 86400; // 闰年2月份增加一天的秒钟数}seccount += (uint32_t)(sday - 1) * 86400; // 把前面日期的秒钟数相加seccount += (uint32_t)hour * 3600; // 小时秒钟数seccount += (uint32_t)min * 60; // 分钟秒钟数seccount += sec; // 最后的秒钟加上去// 设置时钟RCC->APB1ENR |= 1 << 28; // 使能电源时钟RCC->APB1ENR |= 1 << 27; // 使能备份时钟PWR->CR |= 1 << 8; // 取消备份区写保护// 上面三步是必须的!RTC->CRL |= 1 << 4; // 允许配置RTC->CNTL = seccount & 0xffff;RTC->CNTH = seccount >> 16;RTC->CRL &= ~(1 << 4); // 配置更新while (!(RTC->CRL & (1 << 5))); // 等待RTC寄存器操作完成RTC_Get(); // 设置完之后更新一下数据
}void RTC_Get(void)
{static uint16_t daycnt = 0;uint32_t timecount = 0;uint32_t temp = 0;uint16_t temp1 = 0;timecount = RTC->CNTH; // 得到计数器中的值(秒钟数)timecount <<= 16;timecount += RTC->CNTL;temp = timecount / 86400; // 得到天数(秒钟数对应的)if (daycnt != temp) // 超过一天了{daycnt = temp;temp1 = 1970; // 从1970年开始while (temp >= 365){if (Is_Leap_Year(temp1)) // 是闰年{if (temp >= 366)temp -= 366; // 闰年的秒钟数elsebreak;}elsetemp -= 365; // 平年temp1++;}calendar.w_year = temp1; // 得到年份temp1 = 0;while (temp >= 28) // 超过了一个月{if (Is_Leap_Year(calendar.w_year) && temp1 == 1) // 当年是不是闰年/2月份{if (temp >= 29)temp -= 29; // 闰年的秒钟数elsebreak;}else{if (temp >= mon_table[temp1])temp -= mon_table[temp1]; // 平年elsebreak;}temp1++;}calendar.w_month = temp1 + 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); // 获取星期
}void rtc_init_user(void)
{//HAL_RTCEx_SetSecond_IT(&hrtc); // 秒中断使能,没有配置这个中断可以不加if (HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR1) != 0x5050) // 是否第一次配置{RTC_Set(2022, 3, 9, 20, 58, 0); // 设置日期和时间HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR1, 0x5050); // 标记已经初始化过了}RTC_Get(); // 更新时间
}
/* USER CODE END 0 */
然后在主程序中,即可设置和获取时间信息,并且掉电后不会丢失。
二、总结
HAL库RTC问题,主要就是在HAL_RTC_SetDate()和HAL_RTC_GetDate()这两个函数中,对日期相关的数据处理不当,粗暴的将日期的进位舍去了。修改的原理也很简单,获取到真实时间戳后手动解析即可,在笔者 的实现方式中,则主要对应RTC_Get()和RTC_Set()函数,这里笔者使用的函数还存在限制,对星期几的解析只能计算到2099年,其实这里还有更简单的方法,直接使用 time.h 中提供的时间戳处理方法,修改这两个函数进行日期的计算和解析,有兴趣的读者可以自行尝试(需要在Keil中使能MicroLib库)。不过肯定比通过日期备份的方式更加合理可靠。