MCU Systick深度解析:实现简单延时与系统运行时间精准获取!

GD MCU 使用Systick实现板级us ms延时函数及获取系统运行时间

  • Systick是什么
  • MCU中Systick关键参数
  • Systick配置
  • 使用Systick实现延时原理
  • 1、实现us延迟
  • 2、实现ms延时
  • 使用Systick 实现获取系统运行时间原理
  • Systick是什么

    Systick 是一种简单的定时器,属于Cortex M 核中的NVIC 控制器的一部分。
    主要用来周期性的计数和产生中断。
    可以用来给实时操作系统使用,也可以用做其他的时间上的计数。
    

    MCU中Systick关键参数

    核心计数宽度为24bit 的向下递减的自动重装载计数器,计数器每计数一次的时间是1/ClkSource,ClkSource 一般是基于系统时钟的分频,大部分情况下是1分频,即和系统时钟保持一致。

    所以在实际配置或者操作Systick的过程中,我们核心关注计数器的周期、当前计数值,就可以将计数转换成我们需要的时间。
    周期和具体的计数值在我们的寄存器中的值为如下图:


    对应代码中的变量为:

    	typedef struct
    	{
    	  __IO uint32_t CTRL;                    /*!< Offset: 0x000 (R/W)  SysTick Control and Status Register */
    	  __IO uint32_t LOAD;                    /*!< Offset: 0x004 (R/W)  SysTick Reload Value Register       */
    	  __IO uint32_t VAL;                     /*!< Offset: 0x008 (R/W)  SysTick Current Value Register      */
    	  __I  uint32_t CALIB;                   /*!< Offset: 0x00C (R/ )  SysTick Calibration Register        */
    	} SysTick_Type;
    	
    	工程中使用:
    	SysTick->VAL : 当前计数值
    	SysTick->LOAD:装载值(计数总数)
    	
    	SysTick->CTRL:控制及状态寄存器
    	SysTick->CALIB:校准寄存器(不常用)
    

    在配置时,我们会操作LOAD、VAL、CTRL,
    使用计数实现其他功能时,则更多的使用VAL 和LOAD进行转换

    Systick配置

    常见的Systick配置如下,核心实现时uint32_t SysTick_Config(uint32_t ticks)和配置中断优先级,具体如下:

    	/**
     * System Clock Configuration
     */
    void SystemClock_Config(void)
    {
    	//SystemCoreClock 是系统主时钟,RT_TICK_PER_SECOND 是分频系数,一般是1KHz
        SysTick_Config(SystemCoreClock / RT_TICK_PER_SECOND);
        NVIC_SetPriority(SysTick_IRQn, 0);
    }
    
    __STATIC_INLINE uint32_t SysTick_Config(uint32_t ticks)
    {
      if ((ticks - 1) > SysTick_LOAD_RELOAD_Msk)  return (1);      /* Reload value impossible */
    
      SysTick->LOAD  = ticks - 1;                                  /* set reload register */
      NVIC_SetPriority (SysTick_IRQn, (1<<__NVIC_PRIO_BITS) - 1);  /* set Priority for Systick Interrupt */
      SysTick->VAL   = 0;                                          /* 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 (0);                                                  /* Function successful */
    }
    

    上述配置中,
    以主频72M为例,按照1Khz的分频,实际配置的计数值LOAD为 72000 000 / 1000 – 1 = 71999;,实际工程中的配置也是如此(如下图):

    也就是说,每次计数减到0,发生Systick中断,这个中断的频率为72M/(71999 + 1) = 1Khz,**即每1/1Khz = 0.001s(1ms)发生一次中断**
    

    1ms一次的Systick 的中断关系,是我们实现延时或者获取系统的运行时间的核心。

    使用Systick实现延时原理

    还是以72M主频+71999 的装载值为例

    1、实现us延迟

    Systick 每1ms触发一次中断,重新装载VAL再进行倒数计数,也就是,有如下对应关系:
    1ms = (load + 1) 个计数
    1us = (load + 1) / 1000 个计数
    n us = n* (load + 1) / 1000个计数
    那实现延迟的原理不就是:在Systick 未满足n us个计数时,系统就循环死等,直到tick计数大于等于n us个计数,此时才认为延迟时间到了,跳出死等的循环。
    实现思路:

    1. 延迟函数入参为us;
    2. 进入延迟函数获取当前的滴答数记录为t_old;
    3. 获取系统的load值(为接下来将us转换为tick做准备);
    4. 将us转换为需要等待的tick数:t_us_ticks = us * (t_load + 1) / 1000;
    5. 进入死循环等待tick到
      这里要注意:
      判断tick计数其实就是计算t_old 和实时的t_now = SysTick->VAL; 的差值,也即经过的tick数,这里要考虑到t_now 和 t_old的大小,因为t_now减到0后,它会重新装载大数,继续减,所以主要有如下两种情况:
    情况1:
        t_old >= t_now,经过的tick cnt = t_old - tnow;
    
    情况2:
    	val tick 重新装载了,此时cnt tick = told(上一次的tick已经跑过了,额外又经过了load+1 - t_now个tick)
        t_old < t_now,cnt_tick = told + ((load+1) - t_now);
    
    1. 判断经过的cnt_tick 和n us的tick大小,超过n us的tick大小,则退出死循环,至此延时结束。
      代码实现如下:
    /*  
        使用systick 自减的到0,会触发重新装载的逻辑来实现延时的逻辑
        系统中的load 值71999,减到0的频率是72000000/(71999 + 1) = 1Khz,
        也就是每1/1Khz = 1ms触发一次重新装载的中断,所以时间关系如下:
        1ms = (load + 1) 个计数
        1us = (load + 1) / 1000 个计数
        n us = n* (load + 1) / 1000个计数
    
        原理:
        将us转换为计数的tick值,死循环,然后判断实时的tick有无达到us的计数值,达到后退出死循环,时间目的达到
    
        1进入该函数获取获取当前的tick值t_old,循环中获取实时的tick值t_now
        通过不断比较us转换后的tick和经过的tick数
        当经过的tick数大于us的tick后,即可退出
    
        对于经过的tick数来说,有如下两种情况:
        情况1:
        t_old >= t_now,经过的tick cnt = t_old - tnow;
    
        情况2:val tick 重新装载了,此时cnt tick = told(上一次的tick已经跑过了,额外又经过了load+1 - t_now个tick)
        t_old < t_now,cnt_tick = told + ((load+1) - t_now);
    */
    void delay_us(uint64_t us)
    {
        uint64_t t_old = SysTick->VAL;
    	
        uint64_t t_load = SysTick->LOAD;
    	
        uint64_t t_now = 0, cnt_tick = 0;
    
        uint64_t t_us_ticks = us * (t_load + 1) / 1000;
    
        do
        {
            t_now = SysTick->VAL;
            cnt_tick += t_old >= t_now ? (t_old - t_now) : (t_old + ((t_load + 1) - t_now));//记得需要考虑cnt_tick溢出的场景
            t_old = t_now;
        }while(cnt_tick < t_us_ticks);
    }
    

    2、实现ms延时

    基于实现的us延时,循环1000次即可

    void delay_ms(uint64_t ms)
    {
        for(int i = 0; i < ms; i++)
        {
            delay_us(1000);
        }
    }
    

    使用Systick 实现获取系统运行时间原理

    还是以72M主频+71999 的装载值为例:
    Systick 每1ms触发一次中断,重新装载VAL再进行倒数计数,也就是,有如下对应关系:
    1ms = (load + 1) 个计数
    1us = (load + 1) / 1000 个计数
    n us = n* (load + 1) / 1000个计数
    那么每个计数所耗费的时间就是1/(load + 1) ms,也即1* 1000000 / (load + 1) ns。
    cnt个计数,对应的时间为n * 1000000 / (load + 1) ns。
    所以实现的原理是:

    1. 每发生一次Systick中断,记录一次ms的计数(过去经过了多少时间),在Systick中断处理函数中增加ms自增计数
    2. 获取当前的计数值,统计未满LOAD计数对应的时间

    实现代码如下:

    	// 1、Sytick 中断处理函数增加ms自增函数
    	/**
     * This is the timer interrupt service routine.
     *
     */
    void SysTick_Handler(void)
    {
    		hw_inc_tick();
    }
    
    //2、自增计数变量定义
    volatile static uint32_t hw_sys_tick_ms = 0;	//ms 自增计数变量
    void hw_inc_tick(void)
    {
      ++hw_sys_tick_ms;
    }
    
    //3、 获取自增ms计数
    uint32_t hw_return_tick_ms(void)
    {
    	return hw_sys_tick_ms;
    }
    
    //4、实现获取ns 系统函数
    /* 
    		获取系统从启动后到现在过了多少ns
    	原理:使用systick 自减的到0,会触发重新装载的逻辑来实现延时的逻辑
        系统中的load 值71999,减到0的频率是72000000/(71999 + 1) = 1Khz,
        也就是每1/1Khz = 1ms触发一次重新装载的中断,所以时间关系如下:
        1ms = (load + 1) 个计数
        1us = (load + 1) / 1000 个计数
    		1ns = (load + 1) / 1000000个计数
    		即:1个计数 = 1000000 / (load + 1) ns
        n us = n* (load + 1) / 1000个计数
     */
    uint64_t hw_system_get_ns(void)
    {
    	
        uint64_t t_ns = hw_return_tick_ms() * 1000000;
    		
    		uint64_t t_now  = SysTick->VAL;
        
        uint64_t t_load = SysTick->LOAD;
    	
    		uint64_t t_cnt  = t_load + 1 - t_now;	//暂未特殊考虑t_now 等于0的情况
    	
    		t_ns += t_cnt * (1000000 / (t_load + 1));
    	
        return t_ns; 
    }
    
    

    至此,如何利用Systick 进行时间延时、系统运行时间获取均已结束。
    当然,上述的代码中,并未考虑到变量溢出、或者计数为0的场景,更严谨的情况下,应该考虑这些场景。
    本文暂不赘述,只做梳理!

    作者:Blue不够,得是金牌Blue才行!

    物联沃分享整理
    物联沃-IOTWORD物联网 » MCU Systick深度解析:实现简单延时与系统运行时间精准获取!

    发表回复