声明:以下仅为本人的理解,不一定完全正确(勿喷),有不对的请指正,可以共同交流学习进步

众所周知,STM32HAL库的RTC有这个断电重新上电后时间正常但是丢失日期的bug, 这主要是因为HAL库处理时间是最终还是将时间转换为秒然后处理,而日期的处理主要是通过RTC_DateTypeDef结构体DateToUpdate变量进行处理。下面我们来分析下HAL_RTC_Init初始化函数

可以看到在RTC初始化函数中,简单粗暴的将这个日期数据处理的结构体复位了,而没有去读取时间戳将其转换成日期。

接下来分析HAL_RTC_SetDate日期设置和HAL_RTC_GetDate日期获取函数

可以看到无论是HAL库的设置日期和获取日期函数都是经过这个DateToUpdate变量的处理,那么一旦我们断电重新上电,那么HAL_RTC_Init函数获就将DateToUpdate变量复位,所以我们调用HAL_RTC_GetDate获取到的就不是你所设置的日期了。

所以我们可以将年月日时分秒格式数据转换成时间戳直接写入RTC_CNT寄存器,然后读取时也是直接读取寄存器值,再转换为年月日时分秒格式。这里可以使用<time.h>中的库函数解决, 以下是时间设置函数以及时间获取函数

void myRTC_setTime(uint16_t* time_data)
{
	uint32_t set_tim_stamp;
    struct tm set_tim_data;
	
	set_tim_data.tm_year = time_data[0] - 1900;
	set_tim_data.tm_mon  = time_data[1] - 1;
	set_tim_data.tm_mday = time_data[2];
	set_tim_data.tm_hour = time_data[3];
	set_tim_data.tm_min  = time_data[4];
	set_tim_data.tm_sec  = time_data[5];
	
	set_tim_stamp = mktime(&set_tim_data);
	
	// 等待上一次写操作完成
	while(!(hrtc.Instance->CRL & RTC_CRL_RTOFF));
	
	// 失能写保护
	__HAL_RTC_WRITEPROTECTION_DISABLE(&hrtc);
	
	// 将时间戳写入RTC_CNT寄存器
	WRITE_REG(hrtc.Instance->CNTH, (set_tim_stamp >> 16U));
    WRITE_REG(hrtc.Instance->CNTL, (set_tim_stamp & RTC_CNTL_RTC_CNT));
	
	// 写入完毕开启写保护
	__HAL_RTC_WRITEPROTECTION_ENABLE(&hrtc);
	
	// 等待写操作完成
	while(!(hrtc.Instance->CRL & RTC_CRL_RTOFF));
}

void myRTC_getTime(uint16_t* get_time_datas)
{
	uint32_t get_tim_stamp;
    struct tm get_tim_data = {0};
	
	// 从RTC_CNT寄存器获取RTC时间戳
    get_tim_stamp = RTC->CNTH; 	// 获取高16位
    get_tim_stamp <<= 16U;
    get_tim_stamp |= RTC->CNTL; // 获取低16位

	get_tim_data = *localtime(&get_tim_stamp);
	
    get_time_datas[0] = get_tim_data.tm_year + 1900;
    get_time_datas[1] = get_tim_data.tm_mon  + 1;
    get_time_datas[2] = get_tim_data.tm_mday;

    get_time_datas[3] = get_tim_data.tm_hour;
    get_time_datas[4] = get_tim_data.tm_min;
    get_time_datas[5] = get_tim_data.tm_sec;
}

这里函数传进来的参数是存放日期时间的数组,如下分别表示年月日时分秒

uint16_t time_data[6] = {2024, 12, 18, 10, 30, 0};

这里使用了时间戳转换的知识,大家可以自行搜索,这里不过多赘述。

一般情况下,我们在每一次RTC初始化后再设置日期时间,但这样每次上电后都会是原先所设置的日期时间,我们需要的是让时间沿着上次断电时的时间继续往下走,所以我们可以利用备份寄存器第一次设置时间时,写入一个特定值存到备份寄存器,后续每次上电都读取备份寄存器,如果读取的和这个值相等那么就可以不在设置时间,以下是代码

if(myRTC_read_BKP(RTC_BKP_DR1) != 0x66)			// 当备份寄存器的值为0x66时 时间不复位
{
		myRTC_setTime(time_data);
		myRTC_write_BKP(RTC_BKP_DR1, 0x66);
}

注意:如果你有设置闹钟,那么闹钟也需要使用这种方式写入和读取,否则调用库函数日期时间会不正确,这里我估计是HAL_RTC_SetAlarm和HAL_RTC_GetAlarm这两个库函数中的处理对日期有影响,但是没看出为啥,反正用这两个函数就乱了。下面给出闹钟设置和读取的代码:

void myRTC_set_Alarm(uint8_t* alarm_time)
{
//	RTC_AlarmTypeDef sAlarm;
//	//sAlarm.Alarm = RTC_ALARM_A;         // 指定alarm ID

//	// 指定告警具体时间
//	sAlarm.AlarmTime.Hours = alarm_time[0];
//	sAlarm.AlarmTime.Minutes = alarm_time[1];
//	sAlarm.AlarmTime.Seconds = alarm_time[2];
//	HAL_RTC_SetAlarm_IT(&hrtc, &sAlarm, RTC_FORMAT_BIN);
	
	uint32_t alarm_counter = 0;
    uint32_t hour_s = 0, minute_s = 0, sec = 0;
	
	// 将闹钟时间转换为秒数
	hour_s   = alarm_time[0] * 3600U;		// 每小时3600秒
	minute_s = alarm_time[1] * 60U;
	sec	     = alarm_time[2];
	
	alarm_counter = hour_s + minute_s + sec;	// 总秒数
	
	// 等待上次写操作结束
	while ((hrtc.Instance->CRL & RTC_CRL_RTOFF) == (uint32_t)RESET);
	
	// 失能写保护
	__HAL_RTC_WRITEPROTECTION_DISABLE(&hrtc);
	
	// 将闹钟数据写入RTC闹钟寄存器
    WRITE_REG(hrtc.Instance->ALRH, (alarm_counter >> 16U));
    WRITE_REG(hrtc.Instance->ALRL, (alarm_counter & RTC_ALRL_RTC_ALR));
	
	// 写入完毕,打开写保护
	__HAL_RTC_WRITEPROTECTION_ENABLE(&hrtc);
	
	// 等待本次写操作结束
	while ((hrtc.Instance->CRL & RTC_CRL_RTOFF) == (uint32_t)RESET);
	
	/* 清闹钟中断标志位 */
    __HAL_RTC_ALARM_CLEAR_FLAG(&hrtc, RTC_FLAG_ALRAF);
	
	// 开启闹钟中断
	__HAL_RTC_ALARM_ENABLE_IT(&hrtc, RTC_IT_ALRA);

}

void myRTC_get_Alarm(uint8_t* alarm_time)
{
//    RTC_AlarmTypeDef sAlarm;
//    HAL_RTC_GetAlarm(&hrtc, &sAlarm, RTC_ALARM_A, RTC_FORMAT_BIN);

//    alarm_time[0] = sAlarm.AlarmTime.Hours;
//    alarm_time[1] = sAlarm.AlarmTime.Minutes;
//    alarm_time[2] = sAlarm.AlarmTime.Seconds;
	
	uint32_t read_alarm_counter = 0U;
	
	// 读出闹钟数据
	read_alarm_counter = RTC->ALRH; 	// 获取高16位
    read_alarm_counter <<= 16U;
    read_alarm_counter |= RTC->ALRL; 	// 获取低16位
	
	// 转换为标准格式(时分秒)
	alarm_time[0] = (uint8_t)((read_alarm_counter / 3600U) % 24U);
	if(read_alarm_counter % 3600U == 0)
	{
		alarm_time[1] = 0;
		alarm_time[2] = 0;
	}
	else
	{
		alarm_time[1] = (uint8_t)((read_alarm_counter % 3600U) / 60U);
		alarm_time[2] = (uint8_t)((read_alarm_counter % 3600U) % 60U);
	}
}

这里我将库函数部分注释掉了,因为一直在调试。这里我也开启闹钟中断了。

还有一个问题用这种方法之后,闹钟中断莫名奇妙的无法触发,但是时间日期,闹钟时间均正常。而我使用注释掉的库函数配置就能触发中断,但是日期却乱了。。。。

这里因为我的板子LSE不起振,我用的是LSI时钟,所以断电RTC不会继续走时,但是会保留断电前的时间。以下是全部代码

main.c

int main(void)
{
    HAL_Init();                         /* 初始化HAL库 */
    sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
	delay_init(72);						// 延时初始化 依据时钟频率为72mhz

	key_init();
	OLED_Init();
	serial_Init();
	led_init();
	myRTC_Init();
	
	uint16_t time_data[] = {2024, 12, 18, 10, 30, 0};
	uint8_t alarm_time[3] = {10, 30, 10};
	
	if(myRTC_read_BKP(RTC_BKP_DR1) != 0x66)			// 当备份寄存器的值为0x66时 时间不复位
	{
		myRTC_setTime(time_data);
		myRTC_write_BKP(RTC_BKP_DR1, 0x66);
	}
	myRTC_set_Alarm(alarm_time);
	
    while(1)
    {
        // 获取时间和闹钟
		myRTC_getTime(time_data);
		myRTC_get_Alarm(alarm_time);
		OLED_Show_RTC_Time_Alarm(time_data, alarm_time);	// 显示时间和闹钟时间
    }
}

myRTC.c

RTC_HandleTypeDef hrtc;

void myRTC_Init(void)
{
    // 使能电源时钟 BKP时钟
    __HAL_RCC_PWR_CLK_ENABLE();
    __HAL_RCC_BKP_CLK_ENABLE();
    HAL_PWR_EnableBkUpAccess();         // 使能访问bkp和rtc寄存器

    hrtc.Instance = RTC;
    hrtc.Init.AsynchPrediv = 40000 - 1;             // 分频
    hrtc.Init.OutPut = RTC_OUTPUTSOURCE_NONE;       // 忽略RTC入侵功能
    HAL_RTC_Init(&hrtc);
	
}

void HAL_RTC_MspInit(RTC_HandleTypeDef *hrtc)
{
    __HAL_RCC_RTC_ENABLE();

    RCC_OscInitTypeDef  RCC_OscInitStruct;
    RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_LSI;      // 振荡器选择LSI
    RCC_OscInitStruct.LSIState = RCC_LSI_ON;                        // LSI状态开启
    RCC_OscInitStruct.PLL.PLLState = RCC_PLL_NONE;                  // 不使用PLL
    HAL_RCC_OscConfig(&RCC_OscInitStruct);        // 初始化振荡器

    RCC_PeriphCLKInitTypeDef  PeriphClkInit;
    PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_RTC;        // 外设时钟配置为RTC
    PeriphClkInit.RTCClockSelection = RCC_RTCCLKSOURCE_LSI;        // 选择RTC时钟源为LSI
    HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit);       

	// 为闹钟设置中断
    HAL_NVIC_SetPriority(RTC_Alarm_IRQn, 1, 1);
    HAL_NVIC_EnableIRQ(RTC_Alarm_IRQn);

}

// RTC闹钟中断服务函数
void RTC_Alarm_IRQHandler(void)
{
	if (__HAL_RTC_ALARM_GET_IT_SOURCE(&hrtc, RTC_IT_ALRA))
	{
		if(__HAL_RTC_ALARM_GET_FLAG(&hrtc, RTC_CRL_ALRF) != (uint32_t)RESET)
		{
			led_on(LED1);
			
			/* 清闹钟溢出中断标志位 */
			__HAL_RTC_ALARM_CLEAR_FLAG(&hrtc, RTC_CRL_ALRF);
		}    
	}
	
	/* 清闹钟外部触发中断标志位 */
	__HAL_RTC_ALARM_EXTI_CLEAR_FLAG();
}

uint16_t myRTC_read_BKP(uint32_t BackupRegister)
{
    return HAL_RTCEx_BKUPRead(&hrtc, BackupRegister);
}

void myRTC_write_BKP(uint32_t BackupRegister, uint16_t data)
{
    HAL_RTCEx_BKUPWrite(&hrtc, BackupRegister, data);
}

void myRTC_setTime(uint16_t* time_data)
{
	uint32_t set_tim_stamp;
    struct tm set_tim_data;
	
	set_tim_data.tm_year = time_data[0] - 1900;
	set_tim_data.tm_mon  = time_data[1] - 1;
	set_tim_data.tm_mday = time_data[2];
	set_tim_data.tm_hour = time_data[3];
	set_tim_data.tm_min  = time_data[4];
	set_tim_data.tm_sec  = time_data[5];
	
	set_tim_stamp = mktime(&set_tim_data);
	
	// 等待上一次写操作完成
	while(!(hrtc.Instance->CRL & RTC_CRL_RTOFF));
	
	// 失能写保护
	__HAL_RTC_WRITEPROTECTION_DISABLE(&hrtc);
	
	// 将时间戳写入RTC_CNT寄存器
	WRITE_REG(hrtc.Instance->CNTH, (set_tim_stamp >> 16U));
    WRITE_REG(hrtc.Instance->CNTL, (set_tim_stamp & RTC_CNTL_RTC_CNT));
	
	// 写入完毕开启写保护
	__HAL_RTC_WRITEPROTECTION_ENABLE(&hrtc);
	
	// 等待写操作完成
	while(!(hrtc.Instance->CRL & RTC_CRL_RTOFF));
}

void myRTC_getTime(uint16_t* get_time_datas)
{
	uint32_t get_tim_stamp;
    struct tm get_tim_data = {0};
	
	// 从RTC_CNT寄存器获取RTC时间戳
    get_tim_stamp = RTC->CNTH; 	// 获取高16位
    get_tim_stamp <<= 16U;
    get_tim_stamp |= RTC->CNTL; // 获取低16位

	get_tim_data = *localtime(&get_tim_stamp);
	
    get_time_datas[0] = get_tim_data.tm_year + 1900;
    get_time_datas[1] = get_tim_data.tm_mon  + 1;
    get_time_datas[2] = get_tim_data.tm_mday;

    get_time_datas[3] = get_tim_data.tm_hour;
    get_time_datas[4] = get_tim_data.tm_min;
    get_time_datas[5] = get_tim_data.tm_sec;
}


void myRTC_set_Alarm(uint8_t* alarm_time)
{
//	RTC_AlarmTypeDef sAlarm;
//	//sAlarm.Alarm = RTC_ALARM_A;         // 指定alarm ID

//	// 指定告警具体时间
//	sAlarm.AlarmTime.Hours = alarm_time[0];
//	sAlarm.AlarmTime.Minutes = alarm_time[1];
//	sAlarm.AlarmTime.Seconds = alarm_time[2];
//	HAL_RTC_SetAlarm_IT(&hrtc, &sAlarm, RTC_FORMAT_BIN);
	
	uint32_t alarm_counter = 0;
    uint32_t hour_s = 0, minute_s = 0, sec = 0;
	
	// 将闹钟时间转换为秒数
	hour_s   = alarm_time[0] * 3600U;		// 每小时3600秒
	minute_s = alarm_time[1] * 60U;
	sec	     = alarm_time[2];
	
	alarm_counter = hour_s + minute_s + sec;	// 总秒数
	
	// 等待上次写操作结束
	while ((hrtc.Instance->CRL & RTC_CRL_RTOFF) == (uint32_t)RESET);
	
	// 失能写保护
	__HAL_RTC_WRITEPROTECTION_DISABLE(&hrtc);
	
	// 将闹钟数据写入RTC闹钟寄存器
    WRITE_REG(hrtc.Instance->ALRH, (alarm_counter >> 16U));
    WRITE_REG(hrtc.Instance->ALRL, (alarm_counter & RTC_ALRL_RTC_ALR));
	
	// 写入完毕,打开写保护
	__HAL_RTC_WRITEPROTECTION_ENABLE(&hrtc);
	
	// 等待本次写操作结束
	while ((hrtc.Instance->CRL & RTC_CRL_RTOFF) == (uint32_t)RESET);
	
	/* 清闹钟中断标志位 */
    __HAL_RTC_ALARM_CLEAR_FLAG(&hrtc, RTC_FLAG_ALRAF);
	
	// 开启闹钟中断
	__HAL_RTC_ALARM_ENABLE_IT(&hrtc, RTC_IT_ALRA);

}

void myRTC_get_Alarm(uint8_t* alarm_time)
{
//    RTC_AlarmTypeDef sAlarm;
//    HAL_RTC_GetAlarm(&hrtc, &sAlarm, RTC_ALARM_A, RTC_FORMAT_BIN);

//    alarm_time[0] = sAlarm.AlarmTime.Hours;
//    alarm_time[1] = sAlarm.AlarmTime.Minutes;
//    alarm_time[2] = sAlarm.AlarmTime.Seconds;
	
	uint32_t read_alarm_counter = 0U;
	
	// 读出闹钟数据
	read_alarm_counter = RTC->ALRH; 	// 获取高16位
    read_alarm_counter <<= 16U;
    read_alarm_counter |= RTC->ALRL; 	// 获取低16位
	
	// 转换为标准格式(时分秒)
	alarm_time[0] = (uint8_t)((read_alarm_counter / 3600U) % 24U);
	if(read_alarm_counter % 3600U == 0)
	{
		alarm_time[1] = 0;
		alarm_time[2] = 0;
	}
	else
	{
		alarm_time[1] = (uint8_t)((read_alarm_counter % 3600U) / 60U);
		alarm_time[2] = (uint8_t)((read_alarm_counter % 3600U) % 60U);
	}
}

最后看看实际效果

时间日期都是正常走,就是到了设置的闹钟时间就触发不了中断,分析了HAL_RTC_SetAlarm_IT这个函数的中断配置部分,照着它的流程配置还是没用。。

有大佬会可以给分析下,感谢。。。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐