什么是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的晶振,是最精准的时钟源。
外部晶振一般为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中保存数据的方案,之后作者会单独写一篇博文单独说明。
|