极客秀
搜索

HAL_Delay(0)是不延时嘛?你真的了解STM32中的延时函数嘛 ?

1

前言

今晚群友利用逻辑分析仪测试HAL_Delay的实际延时时间。这里感谢群友: MDLZCOOL 的测试数据

测得HAL_Delay(0)的延时时间为约为1ms。

HAL_Delay(100)的实际延时时间为101ms。

其实在这之前我一直以为HAL_Delay的实际延时毫秒数就是我们填入的参数。但是实际上的延时时间是填入的参数值+1。

并且HAL_Delay的准确度会随着延时时间的延长而逐渐产生偏差(依旧非常非常准确200ms时仅0.004ms的偏差)。

那么本期我们就来探究一下HAL_Delay的底层机制。

2

HAL_Delay的底层机制

STM32的延时由BaseTime时基提供,BaseTime默认会选择系统时钟Systick他是系统滴答定时器。但是这并不是固定的,也可以选择为STM32上的硬件定时器外设。

当我们选择好时基之后,系统会在HAL_Init中对时基进行初始化。

这里我们为了能够更好的看清楚定时器的工作机制,我们开一个硬件定时器作为时基来看看HAL_Delay的流程。
如果是系统默认systick的话,这个中断在SysTick_Handler触发。硬件定时器的话,则在定时器中断回调函数触发。


void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim){  /* USER CODE BEGIN Callback 0 */  
  /* USER CODE END Callback 0 */  if (htim->Instance == TIM2) {    HAL_IncTick();  }  /* USER CODE BEGIN Callback 1 */  
  /* USER CODE END Callback 1 */}  
__weak void HAL_IncTick(void){  uwTick += (uint32_t)uwTickFreq;}  
/* 如果是系统默认systick的话,这个中断在SysTick_Handler触发void SysTick_Handler(void){  /* USER CODE BEGIN SysTick_IRQn 0 */  
  /* USER CODE END SysTick_IRQn 0 */  HAL_IncTick();  /* USER CODE BEGIN SysTick_IRQn 1 */  
  /* USER CODE END SysTick_IRQn 1 */}*/

当定时器触发的时候会触发HAL_IncTick();函数,这个函数的作用是 ** 让计数值增加 **
,可以看到系统设置的定时器设置为1KHZ,这也就是为什么HAL_Delay为什么最低的定时时间是1ms.


/** @defgroup HAL_Exported_Variables HAL Exported Variables  * @{  */__IO uint32_t uwTick;uint32_t uwTickPrio   = (1UL << __NVIC_PRIO_BITS); /* Invalid PRIO */HAL_TickFreqTypeDef uwTickFreq = HAL_TICK_FREQ_DEFAULT;  /* 1KHz *//**  

同样的,我们可以修改这个默认值,来修改HAL_Delay的对应 ** 延长时间 ** 。


typedef enum{  HAL_TICK_FREQ_10HZ         = 100U,  HAL_TICK_FREQ_100HZ        = 10U,  HAL_TICK_FREQ_1KHZ         = 1U,  HAL_TICK_FREQ_DEFAULT      = HAL_TICK_FREQ_1KHZ} HAL_TickFreqTypeDef;  

库中提供了三类时间,分别是1KHZ对应1ms,100HZ对应10ms,10HZ对应100ms,但是大部分情况下1ms更为通用。之后我们看看HAL_Delay的运行机制。


__weak uint32_t HAL_GetTick(void){  return uwTick;}  
__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的时候,会获取当前的计数值,之后根据我们设定的定时值来计算我们需要定时多少时间,之后通过轮询 ** 等待到达既定时间 ** 退出。


  if (wait < HAL_MAX_DELAY)  {    wait += (uint32_t)(uwTickFreq);  }

而正是这句代码,所以当我们传入Delay变量的时候,wait被赋予Delay的值,之后会对wait加一次基础定时时间。这也就是为什么HAL_Delay(0)的实际定时时间是1ms,
** HAL_Delay(t)的实际定时时间就是t+1 ms ** 。

不过暂时还不知道为什么这么做,但是逻辑很奇怪。 **
我觉得正常的逻辑应该是判断wait的时间是否>HAL_MAX_DELAY而不是对其递增。这个逻辑不太能明白其缘由。 **

这个问题有待考究。

其次就是有没有可能发送tickstart 本来就比较大了,然后wait的值也比较大。从而导致

**(HAL_GetTick() – tickstart) < wait这个条件不成立呢? **

事实上观察代码可以看到tickstart和wait都是无符号32位整数。最大计数值为4294967295,如果是1ms触发一次递增。其到达一次最大值需要
49.71天 。而即使是计数值到达最大值,他在溢出之后也会从0x00…..0开始技术,而HAL_GetTick() – tickstart是
** 差模运算 ** ,假设 tickstart 是 0xFFFFFF00(即约
4294967040),然后系统继续运行了一段时间,HAL_GetTick() 达到 0x00000050(即 80),此时它发生了溢出,变为
0x00000050。

uint32_t difference = HAL_GetTick() - tickstart;  // 差值是 (0x00000050 - 0xFFFFFF00)difference = 0x00000050 - 0xFFFFFF00 = 0x00000050 + 0x000000FF = 0x0000014F

结果为 0x00000014F,即正确的差值。

所以 ** 丝毫不用担心计数值溢出 ** 的问题。

HAL_Delay的流程图如下:

这种死循环是一种阻塞式延时,因此不能在其他中断函数中使用,因为增加计数值的中断 ** 通常是优先级最低的一类中断 **
,在其他中断服务函数中调用这个函数的话,就会导致 中断函数一直在等待Delay中的死循环结束 。但是由于Delay的优先级不够,因此出现了一种
** 高优先级任务等待低优先级任务 ** 的情况。这种现象被称作 优先级翻转 。

解决这个问题的方法也可以通过提高BaseTime的优先级来解决。

3

微秒级延时

通过这个原理,我们可以使用硬件定时器来实现微秒级延时。


void delay_us(uint32_t us){    uint32_t start = __HAL_TIM_GET_COUNTER(&htim1);    uint32_t delay = us;    // 确保延时不会溢出    if(delay > 0xFFFF)    {        delay = 0xFFFF;    }    while((uint16_t)(__HAL_TIM_GET_COUNTER(&htim1) - start) < delay)    {            }}  

我们可以通过类似HAL_Delay的方式来使用硬件定时器实现微秒级延时。

4

总结

这样子就大概了解了STM32的延时原理,希望能通过向大家介绍这些原理来预防一些代码中的低级错误,例如中断中调用HAL_Delay导致死锁。或者FreeRTOS中出现的可能的优先级翻转功能等等。

虽然库函数很方便,但是重视些底层和运行机制总是没错的。

1.转载请保留原文链接谢谢!
2.本站所有资源文章出自互联网收集整理,本站不参与制作,如果侵犯了您的合法权益,请联系本站我们会及时删除。
3.本站发布资源来源于互联网,可能存在水印或者引流等信息,请用户擦亮眼睛自行鉴别,做一个有主见和判断力的用户。
4.本站资源仅供研究、学习交流之用,若使用商业用途,请购买正版授权,否则产生的一切后果将由下载用户自行承担。
5.联系方式(#替换成@):pm#vimge.com

  相关内容