打印
[应用相关]

STM32 HAL库RTC复位丢失年月日的解决办法

[复制链接]
149|1
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
tpgf|  楼主 | 2024-3-28 12:23 | 只看该作者 |只看大图 回帖奖励 |倒序浏览 |阅读模式
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之间,即可保证重新生成时不被刷新。

3.解析
3.1编写手动解析函数
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整除
            else
                return 0;
        }
        else
            return 1;
    }
    else
        return 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世纪,年份数加100
    if (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; // 闰年的秒钟数
        else
            seccount += 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; // 闰年的秒钟数
                else
                    break;
            }
            else
                temp -= 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; // 闰年的秒钟数
                else
                    break;
            }
            else
            {
                if (temp >= mon_table[temp1])
                    temp -= mon_table[temp1]; // 平年
                else
                    break;
            }
            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 */




然后在主程序中,即可设置和获取时间信息,并且掉电后不会丢失。





3.2使用time.h中的方式自动解析
首先在keil中使能MicroLib库:



然后使用time.h中的相关函数,对时间戳进行解析:
rtc.h:



rtc.c:

/* USER CODE BEGIN 0 */
#include "time.h"

/* 年 月 日 时 分 秒 星期:0-Sunday*/
uint16_t date_info[] = {2024, 3, 10, 15, 0, 0, 0};

void MyRTC_SetTime(uint16_t *time_info)
{
    time_t time_stamp;
    struct tm time_date;

    // 初始化结构体
    time_date.tm_year = time_info[0] - 1900;
    time_date.tm_mon = time_info[1] - 1;
    time_date.tm_mday = time_info[2];
    time_date.tm_hour = time_info[3];
    time_date.tm_min = time_info[4];
    time_date.tm_sec = time_info[5];
    // 转换成时间戳
    time_stamp = mktime(&time_date);

    // 设置RTC时钟, 实测注释掉也没问题
    // __HAL_RCC_PWR_CLK_ENABLE();
    // __HAL_RCC_BKP_CLK_ENABLE();
    // HAL_PWR_EnableBkUpAccess();
    /* Disable the write protection for RTC registers */
    __HAL_RTC_WRITEPROTECTION_DISABLE(&hrtc);
    /* Set RTC COUNTER MSB word */
    WRITE_REG(hrtc.Instance->CNTH, (time_stamp >> 16U));
    /* Set RTC COUNTER LSB word */
    WRITE_REG(hrtc.Instance->CNTL, (time_stamp & RTC_CNTL_RTC_CNT));
    /* Enable the write protection for RTC registers */
    __HAL_RTC_WRITEPROTECTION_ENABLE(&hrtc);
    /* Wait for synchro */
    while ((hrtc.Instance->CRL & RTC_CRL_RTOFF) == (uint32_t)RESET);

    // 更新一次数据
    MyRTC_GetTime();
}

void MyRTC_GetTime(void)
{
    time_t time_stamp;
    struct tm time_date;

    // 获取RTC时间戳
    time_stamp = RTC->CNTH; // 获取高16位
    time_stamp <<= 16;
    time_stamp += RTC->CNTL; // 获取低16位
    // 解析成结构体信息,存入全局变量
    time_date = *localtime(&time_stamp);
    date_info[0] = time_date.tm_year + 1900;
    date_info[1] = time_date.tm_mon + 1;
    date_info[2] = time_date.tm_mday;
    date_info[3] = time_date.tm_hour;
    date_info[4] = time_date.tm_min;
    date_info[5] = time_date.tm_sec;
    date_info[6] = time_date.tm_wday;
}

void rtc_init_user(void)
{
    if (HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR1) != 0x5050) // 是否第一次配置
    {
        MyRTC_SetTime(date_info);                  // 设置日期和时间
        HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR1, 0x5050); // 标记已经初始化过了
    }
    MyRTC_GetTime(); // 更新时间
}
/* USER CODE END 0 */





二、总结
  HAL库RTC问题,主要就是在HAL_RTC_SetDate()和HAL_RTC_GetDate()这两个函数中,对日期相关的数据处理不当,粗暴的将日期的进位舍去了。修改的原理也很简单,获取到真实时间戳后手动解析即可,在笔者的实现方式中,则主要对应RTC_Get()和RTC_Set()函数,这里笔者使用的函数还存在限制,对星期几的解析只能计算到2099年,其实这里还有更简单的方法,直接使用 time.h 中提供的时间戳处理方法, (2024-3-10实现time.h中的方式解析)修改这两个函数进行日期的计算和解析,有兴趣的读者可以自行尝试 (需要在Keil中使能MicroLib库)。不过肯定比通过日期备份的方式更加合理可靠。
————————————————

                            版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

原文链接:https://blog.csdn.net/weixin_45682654/article/details/136591610

使用特权

评论回复
沙发
理想阳| | 2024-3-28 15:04 | 只看该作者
解决一个复位RTC没了,这么麻烦呀。

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

1360

主题

13960

帖子

8

粉丝