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中出现的可能的优先级翻转功能等等。
虽然库函数很方便,但是重视些底层和运行机制总是没错的。