目录
什么是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可以在主电源断电的情况下继续工作,相当于万年历的功能。
关于时钟树
上图是从stm32f103c8t6数据手册中找到的时钟树局部,可以看出RTC支持三个时钟源。即LSI、LSE、HSE。
LSI,即Low Speed Internal,内部低速时钟,使用该时钟源的优点是可以剩下一个外部晶振,缺点是并不精准。LSE,即Low Speed External,外部低速时钟,一般选用32.768kHz的晶振,是最精准的时钟源。
HSE,High Speed External,高速外部时钟,将外部高速晶振经过128分频后提供时钟源给RTC。外部晶振一般为32.768kHz,这是因为32768正好是215,在数字电路中一般分频系数为2n 较容易实现,因此使用32.768kHz的晶振很容易分频得到1ms
注意!如果需使用万年历功能,在主电源掉电时使用后备电源供电时,保持时间准确,则必须使用LSE时钟源
功能
延时
阻塞延时
虽然很多人一听到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中保存数据的方案,之后作者会单独写一篇博文单独说明。