stm32零星笔记(一)——sysTick滴答计时器、RTC实时时钟

目录

  • 什么是sysTick、RTC
  • 关于时钟树
  • 功能
    • 延时
      • 阻塞延时
      • 非阻塞延时的一种近似实现
    • 秒中断
    • 日历与时间
      • RTC(Real Time Clock,实时时钟)
      • 日期掉电保持

什么是sysTick、RTC

sysTick,System Tick Clock,系统滴答计时器,这是一个内嵌在NVIC的内核外设,一般被配置成1ms计数。
RTC,Real Time Clock,实时时钟,这是
从名字可以看出,他的作用与定时器非常类似,事实上这就是一个具有自动重载和溢出中断功能的24位系统节拍计时器,因此很多人都会有这样的疑惑,**stm32有多个外部定时器,为什么还要有systick?**作者在这里总结了以下几个原因。

  • systick是内嵌在内核的,因此所有基于Cortex-M3内核的MCU都可以使用该定时器,大大提高了可移植性;而不同单片机的外部定时器,其寄存器地址和可配置参数往往是不同的,每次移植都需要重新配置定时器。
  • systick被广泛应用于RTOS或者类似需要调度的应用中。在单片机中,并行任务往往是由调度器在串行任务中模拟实现的,可以这样理解,每个进程在执行到一定阶段会调用一次调度器,一次来实现任务切换,但如果在执行到调用调度器前任务出错导致卡死;而sysTick是独立工作的,即使在进入单步调试的时候,sysTick也不会停止工作,大大降低了系统奔溃的可能性。
  • sysTick可以在主电源断电的情况下继续工作,相当于万年历的功能。

关于时钟树

stm32零星笔记(一)——sysTick滴答计时器、RTC实时时钟_第1张图片
上图是从stm32f103c8t6数据手册中找到的时钟树局部,可以看出RTC支持三个时钟源。即LSI、LSE、HSE。

  • LSI,即Low Speed Internal,内部低速时钟,使用该时钟源的优点是可以剩下一个外部晶振,缺点是并不精准。
  • LSE,即Low Speed External,外部低速时钟,一般选用32.768kHz的晶振,是最精准的时钟源。

外部晶振一般为32.768kHz,这是因为32768正好是215,在数字电路中一般分频系数为2n 较容易实现,因此使用32.768kHz的晶振很容易分频得到1ms
注意!如果需使用万年历功能,在主电源掉电时使用后备电源供电时,保持时间准确,则必须使用LSE时钟源

  • HSE,High Speed External,高速外部时钟,将外部高速晶振经过128分频后提供时钟源给RTC。

功能

延时

阻塞延时

虽然很多人一听到RTC都会想到其时钟功能,但作者认为其被使用最多的还是延时。

/**
  * @brief Provides a tick value in millisecond.
  * @note  This function is declared as __weak to be overwritten in case of other
  *       implementations in user file.
  * @retval tick value
  */
__weak uint32_t HAL_GetTick(void)
{
  return uwTick;
}
/**
  * @brief This function provides minimum delay (in milliseconds) based
  *        on variable incremented.
  * @note In the default implementation , SysTick timer is the source of time base.
  *       It is used to generate interrupts at regular time intervals where uwTick
  *       is incremented.
  * @note This function is declared as __weak to be overwritten in case of other
  *       implementations in user file.
  * @param Delay specifies the delay time length, in milliseconds.
  * @retval None
  */
__weak void HAL_Delay(uint32_t Delay)
{
  uint32_t tickstart = HAL_GetTick();
  uint32_t wait = Delay;

  /* Add a freq to guarantee minimum wait */
  if (wait < HAL_MAX_DELAY)
  {
    wait += (uint32_t)(uwTickFreq);
  }

  while ((HAL_GetTick() - tickstart) < wait)
  {
  }
}

HAL_Delay()是HAL库底层封装的毫秒级延时函数。以上是其在HAL库的原型定义,HAL_GetTick()中返回的uwTick是一个32位的全局变量,其在SysTick_Handler中不断自增(默认时钟源分频后默认为1kHz,即每1us自增1),tickstart记录的计时的起点;中间wait计算了完成延时需要的计数长度,默认uwTickFreq为1kHz,其为一个enum,即uwTickFreq=1;底下的while不断读取当前计数并计算计数长度,等待计数超时退出循环。
综上所述,该函数在默认设置下读取一个以1kHz自增变量的起点和终点,其长度wait即表示延时wait微秒。
另外,可能有些人已经考虑到了这个问题,uwTick一直在自增,而该延时函数是在自增的时间刻度上读取了先后两个点,是不是存在uwTick溢出导致延时出错的可能。事实上完全不需要有这样的顾虑,uwTick是32位的变量,直到溢出大约需要135年,而stm32f103的RTC设计最大100年,即只适用于2000~2099,因此完全满足使用需求。
注意:由于sysTick中断优先级较低,且无法设置为高优先级,所以在定时器中断、外部中断等服务函数中无法使用,会导致程序卡死在中断服务函数中

补充
很多人学习51的时候都用过一款工具叫单片机小精灵,在里面可以生成延时函数来实现延时,其代码逻辑是套接循环,利用指令执行的时间占用CPU,而单片机小精灵可以通过计算C语言代码对应的汇编指令和其执行时间,来得到精准的延时,但其灵活性较差,一般只能生成固定时间的延时;如果使用生成的固定时间延时多次调用来实现不同时间的延时,则在每次调用的时候都要压栈弹栈实现现场保护和还原,从而影响延时的精准性。
在实现模拟总线通信时,经常用到微秒级的延时,这个时候也常用__NOP(),以下为其底层原型定义,这是一种在C语言代码中插入汇编的写法,实际上是调用了arm汇编指令nop,即No operation,空指令,占用CPU一个机器周期的时间。

/**
  \brief   No Operation
  \details No Operation does nothing. This instruction can be used for code alignment purposes.
 */
#define __NOP()                             __ASM volatile ("nop")

非阻塞延时的一种近似实现

如果需要执行的任务是周期性交替执行的,并且在某个任务中需要延时,可以在HAL_Delay()的基础上,在while中调用需要周期性执行的任务,即可实现一种近似的非阻塞延时,其函数原型可如下定义。

void Delay_NoneBlock(uint32_t Delay)
{
  uint32_t tickstart = HAL_GetTick();
  uint32_t wait = Delay;

  /* Add a freq to guarantee minimum wait */
  if (wait < HAL_MAX_DELAY)
  {
    wait += (uint32_t)(uwTickFreq);
  }

  while ((HAL_GetTick() - tickstart) < wait)
  {
    Task();	//需要执行的任务
  }
}

实际上,一些建议的单片机系统中就使用了类似的方法进行任务调度。当然,该方法有很大局限性,比如当Task执行时间较长时,该延时函数会很不准确,就失去了利用sysTick延时的意义了。

秒中断

秒中断会每一秒触发一次中断,与定时器中断不同,定时器中断需要手动清除中断标志位(HAL库中在handle中已经清除中断标志位,在回调函数中不需要再次清除),实际上这些指令也会影响定时器中断的准确性,尤其是在高频率进入中断的情况下,而sysTick的秒中断不受任何任何其他因素影响,其精度只和时钟源精度有关。
前面提到过,sysTick具有很好的可移植性,因此一种优化的单片机系统调度方案是利用sysTick秒中断,每次进入秒中断调用调度器,判断是否需要调度。
调用方法
以下代码调用使用HAL库。

  • 调用 HAL_RTCEx_SetSecond_IT(&hrtc) 打开秒中断。
  • 定义 void HAL_RTCEx_RTCEventCallback(RTC_HandleTypeDef *hrtc) 中断服务函数。

日历与时间

RTC(Real Time Clock,实时时钟)

HAL库下常调用相关函数有以下几个。

  • HAL_StatusTypeDef HAL_RTC_SetTime(RTC_HandleTypeDef *hrtc, RTC_TimeTypeDef *sTime, uint32_t Format),设置RTC时间
  • HAL_StatusTypeDef HAL_RTC_GetTime(RTC_HandleTypeDef *hrtc, RTC_TimeTypeDef *sTime, uint32_t Format),读取RTC时间
  • HAL_StatusTypeDef HAL_RTC_SetDate(RTC_HandleTypeDef *hrtc, RTC_DateTypeDef *sDate, uint32_t Format),设置RTC日期
  • HAL_StatusTypeDef HAL_RTC_GetDate(RTC_HandleTypeDef *hrtc, RTC_DateTypeDef *sDate, uint32_t Format),读取RTC时间

STM32 HAL库读取RTC时钟一直不更新时间的问题.
注意:如以上链接中的博文所述,调用HAL_RTC_GetTime之后必须调用HAL_RTC_GetDate解锁日期阴影寄存器,根据测试如果不进行解锁,时间将会一直不更新,这可能是考虑到如果读取时间和日期的时候正好处于零点,可能导致读取到的时间和日期不一致
plus:作者查阅了STM32Cube FW_F1 V1.8.4固件库源码,上述博文中引用的注释说明已经被删除,不知道是不是ST官方修复了该bug,作者并未对其进行再次测试,有测试的小伙伴可以留言评论

日期掉电保持

stm32f103的RTC实际上只计算了从某个固定时间经过tick微秒的日期,因此掉电之后日期不会自动更新,因此日期需要在断电前保存,在再次上电后读取并重置RTC。
方法一
【STM32+cubemx】0009 HAL库开发:RTC实时时钟的使用、掉电时间保持
以上链接中给出了一种掉电保持日期的方法,作者虽未测试,但根据经验上述博文的作者应该没有做长时间(多日)的测试,该方法会出现日期错误。
方法二

  /** Initialize RTC and set the Time and Date
  */
  sTime.Hours = 0;
  sTime.Minutes = 0;
  sTime.Seconds = 0;

  if (HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BIN) != HAL_OK)
  {
    Error_Handler();
  }
  DateToUpdate.WeekDay = RTC_WEEKDAY_SUNDAY;
  DateToUpdate.Month = RTC_MONTH_JANUARY;
  DateToUpdate.Date = 1;
  DateToUpdate.Year = 20;

  if (HAL_RTC_SetDate(&hrtc, &DateToUpdate, RTC_FORMAT_BIN) != HAL_OK)
  {
    Error_Handler();
  }

直接用掉电时保存的日期替换DateToUpdate的参数,HAL库日期读取函数会自动更新日期。
该方法简单方便,但经过作者测试有时候还是会出错,主要出现在跨日期不更新。
方法三 —— 推荐
作者亲测有效的方法,思路是RTC上电时日期默认是2000年1月1日,上电读取一次日期,并计算相对2000年1月1日过去的时间,然后在掉电保存日期的基础上向后延迟相同的时间,即为当前正确的日期。代码如下,将这部分代码插入MX_RTC_Init()即可。

  /* USER CODE BEGIN Check_RTC_BKUP */
	//读取时间以更新日期
	RTC_TimeTypeDef time;
	HAL_RTC_GetTime(&hrtc, &time, RTC_FORMAT_BIN);
	RTC_DateTypeDef date;
	HAL_RTC_GetDate(&hrtc, &date, RTC_FORMAT_BIN);
	//恢复保存的日期
	RTC_DateTypeDef savedDate = RTC_recoverDate();

	uint16_t timeout = 0;
	RTC_DateTypeDef tmpDate = initDate;
	tmpDate.WeekDay = 0x00;
	while (1) {
		//上电默认日期2000/1/1,根据与该日期的时间比较过去了几天
		if (memcmp(&tmpDate, &date, 4) != 0) {
			if (timeout++ >1000) {
				break;
			}
			tmpDate = tomorrow(tmpDate);
			savedDate = tomorrow(savedDate);
		} else {
			HAL_RTC_GetTime(&hrtc, &time, RTC_FORMAT_BIN);
			HAL_RTC_GetDate(&hrtc, &date, RTC_FORMAT_BIN);
			if (HAL_RTC_SetTime(&hrtc, &time, RTC_FORMAT_BIN) != HAL_OK) {
				Error_Handler();
			}
			if (HAL_RTC_SetDate(&hrtc, &savedDate, RTC_FORMAT_BIN) != HAL_OK) {
				Error_Handler();
			}
			RTC_backup();	//将更新的日期保存
			HAL_RTCEx_SetSecond_IT(&hrtc);
			return;
		}
	}
  /* USER CODE END Check_RTC_BKUP */

plus:这里用到了后备区BKUP保存日期,需要手动将日期保存到BKUP,代码如下。

void RTC_backup() {
	HAL_RTCEx_BKUPWrite(&hrtc, RTC_DATE_BKUP, (uint32_t)hrtc.DateToUpdate.Date);
	HAL_RTCEx_BKUPWrite(&hrtc, RTC_MONTH_BKUP, (uint32_t)hrtc.DateToUpdate.Month);
	HAL_RTCEx_BKUPWrite(&hrtc, RTC_YEAR_BKUP, (uint32_t)hrtc.DateToUpdate.Year);
}

由于BKUP是寄存器,可以频繁读写,而且读写速度很快,因此可以直接在主函数while(1)中调用不断写入;
RTC至少1秒才会更新一次,因此也可以在RTC秒中断中调用;
BKUP可存储的数据较少,因此也可以直接写入flash,但flash相对来说读写寿命角度,而且每次写入需要擦除整个扇区,速度较慢,因此一般不能频繁写入,因此如果要保存到flash,则需要开启PVD中断,即掉电中断,在断电前保存日期,但该方法对硬件有一定要求,前面提到flash写入较慢,因此需要在电源并一个大电容,保证在断电期间有足够时间保证日期保存完成。
plus:关于stm32中保存数据的方案,之后作者会单独写一篇博文单独说明。

你可能感兴趣的:(stm32,stm32,单片机)