04. 系统滴答定时器
一、系统滴答定时器概述
SysTick,即系统滴答定时器,它包含在 M3/4/7 内核里面,核心是一个 24 位的递减计数器。当计数值减到 0 时,将从 RELOAD 寄存器中自动重装载定时初值,开始新一轮计数。只要不把它在 SysTick 控制及状态寄存器中的使能位清除,就永不停息。
二、SysTick寄存器介绍
SysTick 定义在 core_m4.h 里面,里面包含 CTRL、LOAD、VAL、CALIB 等 4 个寄存器。
【1】、SysTick 控制及状态寄存器(CTRL)
ST 公司将 SysTick->CTRL 的位 2 定义成时钟源的分频系数,CLKSOURCE=0 时 8 分频,CLKSOURCE=1 时 1 分频;
【2】、SysTick 重装载数值寄存器(LOAD)
【3】、SysTick 当前数值寄存器(VAL)
三、实现延迟
这里我们使用 时钟摘取法 的方式实现延迟。以 Delay_us() 函数为例,比如 Delay_us(50),在刚进入 Delay_us() 函数的时候先计算好这段延时需要等待的 SysTick 计数次数,这里为 50 * 168 * 168
(假设系统时钟为 168Mhz,因为 SysTick 的频率等于系统时钟频率,那么 SysTick 每增加 1,就是 1/168us),然后我们就一直统计 SysTick 的计数变化,直到这个值变化了 50 * 168,一旦检测到变化达到或者超过这个值,就说明延时 50us 时间到了。这样,我们只是抓取 SysTick 计数器的变化,并不需要修改 SysTick 的任何状态。
3.1、延迟初始化函数
uint16_t g_frequency_us = 0; // us延时倍乘数
/**
* @brief 延迟初始化函数
*
* @param clock 系统时钟频率,单位为MHz
*/
void Delay_Init(uint16_t clock)
{
HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK); // 设置SysTick时钟源为HCLK
g_frequency_us = clock; // 1us定时的计数频率
}
HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK);
这句代码把 SysTick 的时钟选择为 内核时钟,这里需要注意的是:SysTick 的时钟源自 HCLK,假设我们外部晶振为 8MHz,然后倍频到 168MHz,那么 SysTick 的时钟即为 168MHz,也就是 SysTick 的计数器 VAL 每减 1,就代表时间过了 1/168us。
3.2、微秒级延迟函数
/**
* @brief 微秒级延迟函数
*
* @param time 要延迟的微秒数
*/
void Delay_us(uint32_t time)
{
uint32_t tick = 0;
uint32_t t_old = 0, t_now = 0, t_count = 0;
uint32_t reload = SysTick->LOAD; // LOAD的值
tick = time * g_frequency_us; // 延迟时间加载
t_old = SysTick->VAL;
while (1)
{
t_now = SysTick->VAL; // tnow用于记录当前的SysTick->VAL 值
if (t_now < t_old)
{
t_count += t_old - t_now; // 在一轮内,t_count加等于t_old到t_now的差值
}
else // SysTick是向下计数的定时器,当VAL值大于t_old时,表示已经到一轮了
{
t_count += reload - t_now + t_old; // 超过一轮内,t_count加等于重装值减t_now到t_old的差值,即VAL-(t_now-t_old)
}
t_old = t_now; // t_old用于记录最近一次的SysTick->VAL值
if (t_count >= tick) // 时间超过或等于要延迟的时间,则定时时间到,退出
{
break;
}
}
}
这里使用了 时钟摘取法,tick 是延时 time 需要等待的 SysTick 计数次数(也就是延时时间),t_old 用于记录最近一次的 SysTick->VAL 值,然后 t_now 则是当前的 SysTick->VAL 值,通过他们的对比累加,实现 SysTick 计数次数的统计,统计值存放在 t_count 里面,然后通过对比 t_count 和 tick,来判断延时是否到达,从而达到不修改 SysTick 实现 time 的延时。
3.3、毫秒级延迟函数
void Delay_ms(uint32_t time)
{
// 这里用540,是考虑到可能有超频应用,比如248M的时候,delay_us()最大只能延时541ms左右了
uint32_t repeat = time / 540;
uint32_t remain = time % 540;
while (repeat)
{
Delay_us(540 * 1000); // 利用delay_us()实现540ms延时
repeat--;
}
if (remain)
{
Delay_us(remain * 1000); // 利用delay_us(),把尾数延时(remain ms)给做了
}
}
该函数其实就是多次调用 delay_us() 函数,来实现毫秒级延时的。我们做了一些处理,使得调用 delay_us() 函数的次数减少,这样时间会更加精准。
3.4、重定向HAL库延迟函数
HAL 库提供的延时函数,只能实现简单的毫秒级别延时,没有实现 us 级别延时。HAL 库的 HAL_Delay() 函数定义如下:
__IO uint32_t uwTick;
uint32_t uwTickPrio = (1UL << __NVIC_PRIO_BITS); /* Invalid PRIO */
__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)
{
}
}
__weak uint32_t HAL_GetTick(void)
{
return uwTick;
}
void SysTick_Handler(void)
{
HAL_IncTick();
}
__weak void HAL_IncTick(void)
{
uwTick += uwTickFreq;
}
HAL 库实现延时功能非常简单,首先定义了一个 32 位全局变量 uwTick,在 Systick 中断服务函数 SysTick_Handler() 中通过调用 HAL_IncTick() 实现 uwTick 值不断增加,也就是每隔 1ms 增加 uwTickFreq,而 uwTickFreq 默认是 1。而 HAL_Delay() 函数在进入函数之后先记录当前 uwTick 的值,然后不断在循环中读取 uwTick 当前值,进行减运算,得出的就是延时的毫秒数。
但是,HAL 库的延时函数有一个局限性,在中断服务函数中使用 HAL_Delay() 会引起混乱(虽然一般禁止在中断中使用延时函数),因为它是通过中断方式实现,而 SysTick 的中断优先级是最低的,所以在中断中运行 HAL_Delay() 会导致延时出现严重误差。
HAL 库默认的延时函数(HAL_Dealy() 函数)是用 __weak 修饰的弱类型的函数,当我们重写 HAL_Delay() 函数来覆盖 HAL 库默认的延时函数。
/**
* @brief 重定向HAL库的延迟函数
*
* @param time 要延迟微秒数
*/
void HAL_Delay(uint32_t time)
{
Delay_ms(time);
}