今天在写软件I2C时看到了us级的延时,而HAL库提供的delay是毫秒级的,于是我便想怎么改进它,先梳理一下大致思路:
1.你得知道HAL_Delay是如何实现的,这里我贴出ST库提供的源码:
/**
* @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)
{
}
}
其实就是就是保留延时的数字,然后阻塞,轮询是否到了设置的时间,并且这里使用的是systick,也就是滴答定时器,对滴答定时器感兴趣的大家可以自行去搜索相关资料,我在这里简单阐述一下我自己的理解:滴答定时器就是一个简单的定时器,提供中断,在无rtos情况下常用于延时函数,在rtos中常用作时基单元。到这里你就可以理解为延时函数其实就是不断询问systick的值,然后和自己的数做对比,判断是否到时间了,注意这里不难看出来步数是1ms,也就是最小单位是1ms。
2.溯源寻根,寻找滴答定时器的时钟频率,这里我截图cubemx的时钟配置部分,因为脉络更为清晰
可以看出来system timer那里其实是由HCLK分频得来的,1分频或者8分频,所以照我的HCLK为168Mhz时,那么我8分频得到的system timer频率就是21Mhz,好,现在频率知道了,最大计数值是多少呢?我们看看内核编程手册提供的systick的计数寄存器也就是说最大可计数是2^24-1,结合我们上面的频率可得出最大一个更新中断间隔是0.798915s,也就是798915us,所以这里我们也知道了一次最大可延时798915us。
3.到这里我就有一个疑问,那按照2所说的,步数应该是us级别的,那不就和STM库提供的延时函数矛盾了吗?于是到了这里我就去查看源码,看到底是怎么回事。
查找源码过程:
1.首先我们看上面的HAL_Delay代码发现逻辑很清晰,那么就应该先看在HAL_GetTick()函数这里
/**
* @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;
}
直接返回这个值,这个值是什么呢?是不是systick的LOAD寄存器值呢?我们先按下不表。
2.没找到直接原因,我们看看滴答定时器的初始化代码,在Cubemx生成的main.c里面我们看到HAL_Init()于是转到源码
HAL_StatusTypeDef HAL_Init(void)
{
/* Configure Flash prefetch, Instruction cache, Data cache */
#if (INSTRUCTION_CACHE_ENABLE != 0U)
__HAL_FLASH_INSTRUCTION_CACHE_ENABLE();
#endif /* INSTRUCTION_CACHE_ENABLE */
#if (DATA_CACHE_ENABLE != 0U)
__HAL_FLASH_DATA_CACHE_ENABLE();
#endif /* DATA_CACHE_ENABLE */
#if (PREFETCH_ENABLE != 0U)
__HAL_FLASH_PREFETCH_BUFFER_ENABLE();
#endif /* PREFETCH_ENABLE */
/* Set Interrupt Group Priority */
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);
/* Use systick as time base source and configure 1ms tick (default clock after Reset is HSI) */
HAL_InitTick(TICK_INT_PRIORITY);
/* Init the low level hardware */
HAL_MspInit();
/* Return function status */
return HAL_OK;
}
根据注释我们注意到了InitTick这个函数,于是我们直接进入,
__weak HAL_StatusTypeDef HAL_InitTick(uint32_t TickPriority)
{
/* Configure the SysTick to have interrupt in 1ms time basis*/
if (HAL_SYSTICK_Config(SystemCoreClock / (1000U / uwTickFreq)) > 0U)
{
return HAL_ERROR;
}
/* Configure the SysTick IRQ priority */
if (TickPriority < (1UL << __NVIC_PRIO_BITS))
{
HAL_NVIC_SetPriority(SysTick_IRQn, TickPriority, 0U);
uwTickPrio = TickPriority;
}
else
{
return HAL_ERROR;
}
/* Return function status */
return HAL_OK;
}
哎,找到了,这个HAL_SYSTICK_Config(SystemCoreClock / (1000U / uwTickFreq))是不是感觉很熟悉很亲切呢,这是设置滴答定时器的中断计数值,也就是中断之间间隔时间,打个比方,你要1ms进入中断,那就设置systick的自动重装载寄存器的值为1ms*21M=21000,其实就是所以systickclk/1000。我们再看看这个SystemCoreClock和(1000U / uwTickFreq)的含义,这里我直接说明SystemCoreClock就是系统内核时钟,就是系统主频,就是SYSCLK,uwTickFreq是一个枚举体变量,它代表中断频率,
/** @defgroup HAL_TICK_FREQ Tick Frequency
* @{
*/
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;
这样就和我们前面的systickclk/1000对上了,但是这个当然更灵活一点,可以选择中断频率,但是应该是systickclk而不是SystemCoreClock,这怎么理解呢?难道两者数值相等吗?
3.我们现在寻找这两者的区别,根据我们的时钟树设置,两者根本不相等!因为我们的SYSTICK前面的分频系数并不是1,而是8,也就是8分频,是系统时钟频率除以8!,带着这个疑惑我们在进入时基单元配置函数
/**
* @brief Initializes the System Timer and its interrupt, and starts the System Tick Timer.
* Counter is in free running mode to generate periodic interrupts.
* @param TicksNumb Specifies the ticks Number of ticks between two interrupts.
* @retval status: - 0 Function succeeded.
* - 1 Function failed.
*/
uint32_t HAL_SYSTICK_Config(uint32_t TicksNumb)
{
return SysTick_Config(TicksNumb);
}
在进入!终于看到了庐山真面目
__STATIC_INLINE uint32_t SysTick_Config(uint32_t ticks)
{
if ((ticks - 1UL) > SysTick_LOAD_RELOAD_Msk)
{
return (1UL); /* Reload value impossible */
}
SysTick->LOAD = (uint32_t)(ticks - 1UL); /* set reload register */
NVIC_SetPriority (SysTick_IRQn, (1UL << __NVIC_PRIO_BITS) - 1UL); /* set Priority for Systick Interrupt */
SysTick->VAL = 0UL; /* Load the SysTick Counter Value */
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk |
SysTick_CTRL_TICKINT_Msk |
SysTick_CTRL_ENABLE_Msk; /* Enable SysTick IRQ and SysTick Timer */
return (0UL); /* Function successful */
}
#endif
/*@} end of CMSIS_Core_SysTickFunctions */
#ifdef __cplusplus
}
注意这段代码: SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk;其实就是对时钟源的选择,我们查看这个宏定义,发现这里居然是设置的还是1分频!
#define SysTick_CTRL_CLKSOURCE_Pos 2U /*!< SysTick CTRL: CLKSOURCE Position */
#define SysTick_CTRL_CLKSOURCE_Msk (1UL << SysTick_CTRL_CLKSOURCE_Pos) /*!< SysTick CTRL: CLKSOURCE Mask */
也就是说系统时钟频率数值上就是等于滴答定时器的时钟频率。我们设置的8分频根本没有起作用,也就是说这里出现了BUG,因为它并不影响延时函数的使用,但是这里确实是出现了BUG!
4.我们怀着严谨学习的态度,发现这里出现了BUG,同时我们意识到ms的出现必定是在HAL_GetTick()函数的返回值这里,并且我们根据前面中断频率的理解,大胆推断这里的返回值一定是在滴答定时器的中断服务函数中递增的,于是我们直接查看中断服务函数句柄
/**
* @brief This function handles System tick timer.
*/
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 */
}
进入IncTick(),终于功夫不负有心人,验证了我们的推断,
/**
* @brief This function is called to increment a global variable "uwTick"
* used as application time base.
* @note In the default implementation, this variable is incremented each 1ms
* in SysTick ISR.
* @note This function is declared as __weak to be overwritten in case of other
* implementations in user file.
* @retval None
*/
__weak void HAL_IncTick(void)
{
uwTick += uwTickFreq;
}
4.到这里我突然想到之前刷到短视频,一个新手女生入门玩32板子,到了按键那块,下面一个“佬”说:智能车选手报道,按键延时不用软延时,(也就是使用while循环),应该用滴答定时器,这样可以提高效率,不让MCU等待。我纳闷,用滴答定时器不是还得阻塞,还得等待吗?只是精度的问题吧?我点开下面的评论,全是迎合他的,汗,只有极少人能抱有我这样的想法的人。
解决方法:其实我们可以直接重定义这个HAL_Delay()函数,使其级别为us级别,具体代码我就不贴了,我看正点原子提供的ms和us延时函数就挺不错的,大家可以参考一下,如果大家想要这个重定义函数源码我可以验证后发在评论区或者其它地方。
收获:对HAL库下滴答定时器的配置和延时函数了解清楚了,另外发现了一个Cubemx生成的代码的BUG。