第11周实验

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

阅读资料了解 STM32F103的RTC(实时时钟)原理,使用带SPI或IIC接口的OLED屏显模块实现以下功能:

  1. 读取STM32F103C8T6 内部的时钟(年月日时分秒),日历(星期x),1秒周期,通过串口输出到PC上位机,;

  2. 读取AHT20的温度和湿度,通过OLED,把年月份时分秒、日历和实时温度、湿度显示出来,2秒周期。

一、RTC主要介绍

RTC(Real Time Clock):实时时钟,是指可以像时钟一样输出实际时间的电子设备,一般会是集成电路,因此也称为时钟芯片。总之,RTC只是个能靠电池维持运行的32位定时器,并不像实时时钟芯片,读出来就是年月日。
RTC就只一个定时器而已,掉电之后所有信息都会丢失,因此我们需要找一个地方来存储这些信息,于是就找到了备份寄存器(BKP)。因为它掉电后仍然可以通过纽扣电池供电,继续工作,所以能时刻保存这些数据。
stm32F103使用外部晶体的32.768kHz的振荡器,产生一个1秒长的时间基准。
RTC 模块和时钟配置系统(RCC_BDCR 寄存器)是在后备区域,即在系统复位或从待机模式唤醒后 RTC 的设置和时间维持不变。但是在系统复位后,会自动禁止访问后备寄存器和 RTC,以防止对后备区域(BKP)的意外写操作。所以在要设置时间之前, 先要取消备份区域(BKP)写保护。
在这里插入图片描述
在初始化时候,DIV和PRL都会装载同样的数。RTCCLK频率为32.768kHz,来驱动DIV工作,DIV为自减寄存器。当DIV减少到0时,会产生一个TR_CLK信号,该信号会触发三件事情:①将PRL的数重装在到DIV中;②触发CNT计数器+1;③触发SECF,进而产生秒中断。我们会设置一个合理的预分频数,使得TR_CLK触发时间刚好是1s。ALR中存储的是我们设置的闹钟秒数,当CNT和ALR中的数据相同时,就会触发闹钟中断或者闹钟唤醒。(关于CNT和ALR中存放的数据,为时间戳。)

二、功能实现和主要代码

思考整个程序的流程,因为MCU有后备寄存器,其中的值会在电池供电的时候一直保持,我们可以在寄存器里写入一些标志,来在程序运行时判断RTC是首次运行,还是一直有电池供电运行,时间在继续计时。如果后备寄存器里没有我们想要的标志,说明RTC是首次运行,这个时候我们应该对时间计数器重新初始化,将日期和时间转为时间戳,写入计数器;如果后备寄存器里的值正是我们之前写入过的标志,则说明RTC一直在供电持续运行,此时读出计数器的值,转化成日期及时间。
而把Unix时间戳转换为时间,则不得不需要知道一些历法的知识,因为Unix时间不考虑闰秒,则只需要考虑闰年。把读出的秒数,按照每年(考虑闰年、平年)有多少秒,每月有多少秒,每天有多少秒,每小时有多少秒,每分钟多少秒,一一减掉,则可得出日期和时间。

在这里插入图片描述

1.判断闰年、平年
1582年以来公历的置闰规则:
普通闰年:公历年份是4的倍数,且不是100的倍数的,为闰年(如2004年、2020年等就是闰年)。
世纪闰年:公历年份是整百数的,必须是400的倍数才是闰年(如1900年不是闰年,2000年是闰年)。
1582年以前的惯例:四年一闰;如果公元A年的A(正数)能被4整除,那么它就是闰年;如果公元前B年的B(正数)除以4余1,那么它也是闰年。(1582年是神奇的一年,有兴趣的可以搜索一下1582年10月,看看发生了什么事。)

/**
  * @brief  Check whether the passed year is Leap or not.
  * @param  nYear  year to check
  * @retval 1: leap year
  *         0: not leap year
  */
static uint8_t RTC_IsLeapYear(uint16_t nYear)
{
  if ((nYear % 4U) != 0U)
  {
    return 0U;
  }

  if ((nYear % 100U) != 0U)
  {
    return 1U;
  }

  if ((nYear % 400U) == 0U)
  {
    return 1U;
  }
  else
  {
    return 0U;
  }
}

2.Unix时间戳转为UTC时间
该过程,以1970年1月1日00:00:00为开始时间,把32位寄存器的值,转化成日历时间,笔者根据程序总结的流程如下:
① 先计算较简单的weekday,起始时间是周四;因为不管平年还是闰年,不管哪个月份,每一周都是7天,算出寄存器值总共有多少天,天数模7的余数,经过简单计算,就是当前的weekday;
② 计算年份;如果天数大于1年,则根据该年平年或闰年,减去相应天数,年数递增,继续判断剩余天数包含的年数,直至剩余天数不足一年,退出循环;
③ 计算月份和日期;步骤②最后剩余的天数,已不满1年,逐月减去相应的天数,月份递增,注意如果遇到闰年二月,要减去29天,直至剩余天数不足一个月,退出循环;剩余的天数+1为日期;
④ 计算时、分、秒;时间戳模86400的余数,为不足整天的秒数s,s/3600,为小时,(s%3600)/60为分钟,s%60为秒,这部分的计算比较好理解。

typedef struct
{
	uint8_t Hours;
    uint8_t Minutes;
    uint8_t Seconds;
    uint8_t WeekDay;
    uint8_t Month;
    uint8_t Date;
    uint16_t Year;
}RTC_ts;

//平年每月的天数
const uint8_t u8DayNumTab[12] = {31,28,31,30,31,30,31,31,30,31,30,31};
/*******************************************************************************
  * 函数名:UnixToUTC
  * 功  能:Unix时间戳转为UTC时间
  * 参  数:u32TimeStamp:Unix时间戳
  * 返回值:UTC时间,RTC_ts结构体格式
  * 说  明:无
*******************************************************************************/
RTC_ts UnixToUTC(uint32_t u32TimeStamp)
{
	RTC_ts sTemp;
	sTemp.Year = 1970;
	sTemp.Month = RTC_MONTH_JANUARY;
	sTemp.Date = 1;
	sTemp.Hours = 0;
	sTemp.Minutes = 0;
	sTemp.Seconds = 0;
	sTemp.WeekDay = RTC_WEEKDAY_THURSDAY;
	
	uint32_t temp1 = 0;//天数
	uint32_t temp2 = 1970;//年份
	temp1 = u32TimeStamp / 86400;//计算出天数
	
	sTemp.WeekDay = (uint8_t)((temp1 % 7) + RTC_WEEKDAY_THURSDAY);//计算weekday,1970年1月1日为周四
	if (sTemp.WeekDay > RTC_WEEKDAY_SATURDAY)
	{
		sTemp.WeekDay -= 7;
	}
	
	if (temp1 > 0)
	{ 
		while (temp1 >= 365)//天数还大于1年,减去一年的天数,剩余的天数继续判断
		{
			if (IsLeapYear(temp2) == 1)//闰年
			{
				if (temp1 >= 366)
				{
					temp1 -= 366;
					temp2++;
				}else
				{
					break;//不足一年,跳出循环
				}		
			}else//平年
			{
				temp1 -= 365;
				temp2++;
			}
		}
		sTemp.Year = (uint16_t)temp2;//年份
		
		temp2 = RTC_MONTH_JANUARY;//月份,从1月份开始
		while (temp1 >= 28)//去掉整年/整月,剩余的天数
		{
			if ((IsLeapYear(sTemp.Year) == 1) && (temp2 == RTC_MONTH_FEBRUARY))//当年是闰年且是二月
			{
				if (temp1 >= 29)
				{
					temp1 -= 29;
				}else
				{
					break;
				}
			}else//平年
			{
				if (temp1 >= u8DayNumTab[temp2 - 1])//剩余天数比1个月大
				{
					temp1 -= u8DayNumTab[temp2 - 1];
				}else
				{
					break;
				}
			}
			temp2++;
		}
		sTemp.Month = (uint8_t)temp2;//月份
		sTemp.Date = (uint8_t)(temp1 + 1);//日期		
	}
	temp1 = (u32TimeStamp % 86400);//去掉整天,剩余的秒数
	sTemp.Hours = (uint8_t)(temp1 / 3600);//计算出小时数
	sTemp.Minutes = (uint8_t)((temp1 % 3600) / 60);//计算出分钟数
	sTemp.Seconds = (uint8_t)(temp1 % 60);//计算出秒数
	return sTemp;
}

3.UTC时间转为Unix时间戳
这一部分,与“2.Unix时间戳转为UTC时间”是相反的过程,将日期和时间,转换为1970-1-1 00:00:00以来的秒数,但这个过程要简单一些,流程如下:
① 计算整年的秒数;以1970年为起始年,逐年增加一年的秒数,直至UTC时间年的前一年,需要注意是平年还是闰年;
② 计算整月的秒数;逐月增加一个月的秒数,直至UTC时间月的前一个月,如果有闰月,需要多加1天;
③ 计算整天、整小时、整分钟的秒数,加上剩余的UTC时间秒的秒数,以上所有数值的和,即为Unix时间戳;

/*******************************************************************************
  * 函数名:UTCToUnix
  * 功  能:UTC时间转为Unix时间戳
  * 参  数:UTC时间,RTC_ts结构体格式
  * 返回值:Unix时间戳
  * 说  明:标准UTC时间转为Unix时间戳,时间范围1970~2100年
*******************************************************************************/
uint32_t UTCToUnix(RTC_ts sTime)
{
	uint32_t u32TimeStamp = 0;
	uint16_t i;
	if ((sTime.Year < 1970) || (sTime.Year > 2100))
	{
		return 0;
	}
	for (i = 1970; i < sTime.Year; i++)//计算整年的秒数
	{
		if (IsLeapYear(i) == 1)
		{
			u32TimeStamp += (86400 * 366);
		}else
		{
			u32TimeStamp += (86400 * 365);
		}
	}
	for (i = 0; i < (sTime.Month - 1); i++)//增加整月的秒数
	{
		u32TimeStamp += ((uint32_t)u8DayNumTab[i] * 86400);
	}
	if ((IsLeapYear(sTime.Year) == 1) && ((sTime.Month  - 1) >= RTC_MONTH_FEBRUARY))//当年是闰年且月份超过2月
	{
		u32TimeStamp += 86400;//多加1天
	}
	u32TimeStamp += (((uint32_t)sTime.Date - 1) * 86400);//增加整天的秒数	
	u32TimeStamp += ((uint32_t)sTime.Hours * 3600);//增加小时的秒数
	u32TimeStamp += ((uint32_t)sTime.Minutes * 60);//增加分钟的秒数
	u32TimeStamp += (uint32_t)sTime.Seconds;//增加剩余秒数
	return u32TimeStamp;
}

在这里插入图片描述

总结

STM32F103系列的RTC模块,没有年月日及时间寄存器,只有一个32位计数器,每1秒加1,没有年月日及时间寄存器,需要软件配合,才能实现日历、时钟功能;
由于只有计数功能,可以任意时间作为基准时间(即开始时间),用UTC时间戳(以1970年1月1日00:00:00为开始时间),可以使程序通用性更强;
以上UTCToUnix和UnixToUTC两个函数是以标准UTC时间为基础的,实际北京时间为UTC+8,相差8个小时,如果需要计算北京时间,需要加8个小时。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值