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

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

HAL_Delay(100)的实际延时时间为101ms。
其实在这之前我一直以为HAL_Delay的实际延时毫秒数就是我们填入的参数。但是实际上的延时时间是填入的参数值+1。
并且HAL_Delay的准确度会随着延时时间的延长而逐渐产生偏差(依旧非常非常准确200ms时仅0.004ms的偏差)。
那么本期我们就来探究一下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的优先级来解决。
通过这个原理,我们可以使用硬件定时器来实现微秒级延时。
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的方式来使用硬件定时器实现微秒级延时。
这样子就大概了解了STM32的延时原理,希望能通过向大家介绍这些原理来预防一些代码中的低级错误,例如中断中调用HAL_Delay导致死锁。或者FreeRTOS中出现的可能的优先级翻转功能等等。
虽然库函数很方便,但是重视些底层和运行机制总是没错的。


登录 或 注册 后才可以进行评论哦!
还没有评论,抢个沙发!