04. 系统滴答定时器

一、系统滴答定时器概述

  SysTick,即系统滴答定时器,它包含在 M3/4/7 内核里面,核心是一个 24 位的递减计数器。当计数值减到 0 时,将从 RELOAD 寄存器中自动重装载定时初值,开始新一轮计数。只要不把它在 SysTick 控制及状态寄存器中的使能位清除,就永不停息。

SysTick工作原理

二、SysTick寄存器介绍

  SysTick 定义在 core_m4.h 里面,里面包含 CTRL、LOAD、VAL、CALIB 等 4 个寄存器。

【1】、SysTick 控制及状态寄存器(CTRL)

SysTick的CTRL寄存器

ST 公司将 SysTick->CTRL 的位 2 定义成时钟源的分频系数,CLKSOURCE=0 时 8 分频,CLKSOURCE=1 时 1 分频;

【2】、SysTick 重装载数值寄存器(LOAD)

SysTick的LOAD寄存器

【3】、SysTick 当前数值寄存器(VAL)

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);
}
posted @ 2023-10-27 20:06  星光樱梦  阅读(22)  评论(0编辑  收藏  举报