单片机学习小组
直播中

刘慧

7年用户 1727经验值
私信 关注

systick具有哪些功能?

systick具有哪些功能?

回帖(1)

李勇俊

2022-2-10 10:57:04
前言

systick是Cortex-M内置的一个节拍定时器功能。它具有以下几个特点:



  • 24位计数器
  • 递减计数
  • 可以产生中断

本文主要介绍该功能在STM32F429上的使用方法。本文主要参考文献:



  • Joesph Yiu.ARM cortex-M3与cortex-M4权威指南(第三版).清华大学出版社
  • ST.STM32 Cortex®-M4 MCUs and MPUs programming manual
  • 正点原子.STM32F429开发指南-HAL库版本_V1.1

更新:



  • 2021.01.22——更新delay.c文件的另一种写法。
  • 2021.03.03——更新delay.c文件的第三种写法。
  • 2021.05.18——增加官方手册对于寄存器的描述。
  • 2021.05.21——更新delay.c文件的写法。

寄存器

因为 systick处于底层,且内容比较简单,所以本文可以介绍一下其寄存器。其共有4个寄存器,在HAL库中通过一个结构体表达:

/**
  brief  Structure type to access the System Timer (SysTick).
*/
typedef struct
{
    __IOM uint32_t CTRL;                   /*!< Offset: 0x000 (R/W)  SysTick Control and Status Register */
    __IOM uint32_t LOAD;                   /*!< Offset: 0x004 (R/W)  SysTick Reload Value Register */
    __IOM uint32_t VAL;                    /*!< Offset: 0x008 (R/W)  SysTick Current Value Register */
    __IM  uint32_t CALIB;                  /*!< Offset: 0x00C (R/ )  SysTick Calibration Register */
} SysTick_Type;
其寄存器在内核中的功能为:

在STM32F429中略有不同,且由于第四个校正值寄存器使用较少,在此不再详细列出,其余的寄存器在STM32上使用为:
STK_CTRL




  • 位16:上一次读取该位以后,计数器重新到达过0,该位置1。
  • 位2:选择时钟源。


    • 0: AHB/8
    • 1:AHB


  • 位1:SysTick中断使能


    • 0:禁止中断:
    • 1: 使能中断


  • 位0:计数器使能



    • 0:计数器禁用
    • 1:计数器使能


STK_LOAD

位23到位0有效。写入的数值+1为中断产生的实际周期值。例如,需要每隔100个节拍周期产生一个中断,该寄存器的值设置为99。
STK_VAL

位23到位0有效。写入任何值,该寄存器清零且将STK_CTRL中的COUNTFLAG位清零。读取该寄存器,返回当前计数器的计数值。




正点原子
正点原子编写的代码与systick关系最大就是delay函数。若没有使用实时操作系统时,可以将delay.c简化为:


#include "delay.h"
#include "sys.h"


static u32 fac_us = 0;                                                        //us延时倍乘数


/**
  * @brief   延时函数初始化
  * @note    None
  * @param  SYSCLK AHB时钟频率(单位MHz)
  * @retval  None
  */
void delay_init ( u8 SYSCLK )
{
    HAL_SYSTICK_CLKSourceConfig ( SYSTICK_CLKSOURCE_HCLK );                //SysTick频率为HCLK
    fac_us = SYSCLK;                                                                                        //设置全局变量
}






/**
  * @brief   延时函数微秒
  * @note    None
  * @param  nus:延时的微秒数,0~2^32/fac_us
  * @retval  None
  */
void delay_us ( u32 nus )
{
    u32 ticks;                                                                        //存储需要节拍数
    u32 told, tnow, tcnt = 0;                                        //told:上一次存储的时间,tnew:当前时间;tcent:累计的时间差
    u32 reload = SysTick->LOAD;                                        //读取sysTick装载值
    ticks = nus * fac_us;                                                 //需要的节拍数
    told = SysTick->VAL;                                        //读取当前计数值
    while ( 1 )
    {
        tnow = SysTick->VAL;
        if ( tnow != told )
        {
                        /*获得上一次记录的时间和当前时间之间的差值*/
            if ( tnow < told ) tcnt += told - tnow;       
            else tcnt += reload - tnow + told;


            /*将当前时间记录下来*/
                        told = tnow;
            if ( tcnt >= ticks ) break;                        //时间超过/等于要延迟的时间,则退出.
        }
    }
}


/**
  * @brief   延时函数毫秒
  * @note    None
  * @param  nms:延时的毫秒数
  * @retval  None
  */
void delay_ms ( u16 nms )
{
    u32 i;
    for ( i = 0; i < nms; i++ )
        delay_us(1000);
}


通过简化,可以看到代码很容易理解,工分成三个函数:


delay初始化函数
微秒延时函数
毫秒延时函数
由于毫秒延时函数比较简单,此处不再详细展开。


delay初始化函数
该函数就只有两行代码,其作用为:


将Systick的时钟源设置为HCLK,一般的,也就是系统时钟180MHz。
将系统时钟传递给全局变量fac_us,为计算微秒延时函数做准备。
在第一个步骤中,调用一个函数HAL_SYSTICK_CLKSourceConfig(),该函数的定义为:


/**
  * @brief  Configures the SysTick clock source.
  * @param  CLKSource: specifies the SysTick clock source.
  *          This parameter can be one of the following values:
  *             @arg SYSTICK_CLKSOURCE_HCLK_DIV8: AHB clock divided by 8 selected as SysTick clock source.
  *             @arg SYSTICK_CLKSOURCE_HCLK: AHB clock selected as SysTick clock source.
  * @retval None
  */
void HAL_SYSTICK_CLKSourceConfig(uint32_t CLKSource)
{
  /* Check the parameters */
  assert_param(IS_SYSTICK_CLK_SOURCE(CLKSource));
  if (CLKSource == SYSTICK_CLKSOURCE_HCLK)
  {
    SysTick->CTRL |= SYSTICK_CLKSOURCE_HCLK;
  }
  else
  {
    SysTick->CTRL &= ~SYSTICK_CLKSOURCE_HCLK;
  }
}


根据上述源程序,很容易理解,通过设置STK_CTRL寄存器,直接设定systick的时钟源。


微秒延时函数
void delay_us ( u32 nus )
{
    u32 ticks;                                                                        //存储需要节拍数
    u32 told, tnow, tcnt = 0;                                        //told:上一次存储的时间,tnew:当前时间;tcent:累计的时间差
    u32 reload = SysTick->LOAD;                                        //读取sysTick装载值
    ticks = nus * fac_us;                                                 //需要的节拍数
    told = SysTick->VAL;                                        //读取当前计数值
    while ( 1 )
    {
        tnow = SysTick->VAL;
        if ( tnow != told )
        {
                        /*获得上一次记录的时间和当前时间之间的差值*/
            if ( tnow < told ) tcnt += told - tnow;       
            else tcnt += reload - tnow + told;


            /*将当前时间记录下来*/
                        told = tnow;
            if ( tcnt >= ticks ) break;                        //时间超过/等于要延迟的时间,则退出.
        }
    }
}


为了照顾在使用系统的情况,正点原子使用延时函数的方法叫 时钟摘取法。即通过在循环中,不断叠加时间差,知道时间差的数值大于等于设定值,则延时时间到,函数结束。其延时时间为计算方法为:


systick的时钟源为AHB,一般的也就是系统时钟,为180MHz。其周期为1/180 us。
累积的时间界定值为:ticks = nus * fac_us=180*nus。
所以,累积时间为:ticks*1/180=nus。
综上,参数的取值就是延时的微秒数。


HAL库
在正点原子的程序中,很奇怪的一点就是,没有启动计数器,也没有设置计数器的加载值。这是因为在HAL库中HAL_init()函数已经做了这些工作。关于HAL_init()使用介绍,可以参考博客。其中与Systick有关的就是下面这条语句:


HAL_InitTick(TICK_INT_PRIORITY);
1
其源程序定义为:


__weak HAL_StatusTypeDef HAL_InitTick(uint32_t TickPriority)
{
  /*Configure the SysTick to have interrupt in 1ms time basis*/
  HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq()/1000U);


  /*Configure the SysTick IRQ priority */
  HAL_NVIC_SetPriority(SysTick_IRQn, TickPriority ,0U);


  /* Return function status */
  return HAL_OK;
}


此处,我们只分析第一步,第二步与中断相关的内容暂且不看,其源代码为:


__STATIC_INLINE uint32_t SysTick_Config(uint32_t ticks)
{
  if ((ticks - 1UL) > SysTick_LOAD_RELOAD_Msk)                                        //设置的值大于24位
  {
    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|                                                        //时钟源为AHB
          SysTick_CTRL_TICKINT_Msk|                                                                                        //打开时钟中断
          SysTick_CTRL_ENABLE_Msk;                                                                                        //启动systick时钟
  
  return (0UL);                                                     /* Function successful */
}


除去中断部分,我们可以看到,该程序共有三个功能:


设置计数器的计数周期。
清除计数器当前计数值。
启动计数器开始计数,启动中断,设置时钟源为AHB。
又因为传递的参数为HAL_RCC_GetHCLKFreq()/1000U,即AHB时钟频率的千分之一,所以,该时钟的计时周期为1ms。


delay.c
在程序中,应该减少全局变量的使用,减少函数之间的耦合,所以上述程序可以修改为:


/**
  ******************************************************************************
  * @name    delay.c
  * @author  zhy
  * @version 1.0
  * @date    2020.11.11
  * @brief   通过时钟窃取法,得到延时时钟的功能。
  ******************************************************************************
  */


#include "stm32f4xx.h" //包含该头文件不会出现引用错误
#include "delay.h"


/**
  * @brief   延时函数微秒
  * @note    None
  * @param   nus:延时的微秒数,0~2^32/fac_us
  * @retval  None
  */
void delay_us(uint32_t nus)
{
    uint32_t fac_us = HAL_RCC_GetSysClockFreq() / 1e6; //us延时倍乘数
    uint32_t ticks;                                    //存储需要节拍数
    uint32_t told, tnow, tcnt = 0;                     //told:上一次存储的时间,tnew:当前时间;tcent:累计的时间差
    uint32_t reload = SysTick->LOAD;                   //读取sysTick装载值
    ticks = nus * fac_us;                              //需要的节拍数
    told = SysTick->VAL;                               //读取当前计数值


    while (1)
    {
        tnow = SysTick->VAL;
        if (tnow != told)
        {
            /*获得上一次记录的时间和当前时间之间的差值*/
            if (tnow < told)
                tcnt += told - tnow;
            else
                tcnt += reload - tnow + told;


            /*将当前时间记录下来*/
            told = tnow;
            if (tcnt >= ticks)
                break; //时间超过/等于要延迟的时间,则退出.
        }
    }
}


/**
  * @brief   延时函数毫秒
  * @note    None
  * @param   nms:延时的毫秒数
  * @retval  None
  */
void delay_ms(uint16_t nms)
{
    uint32_t i;
    for (i = 0; i < nms; i++)
        delay_us(1000);
}


第三种写法
/**
  * @brief   延时函数微秒
  * @note    当时间比较短时,误差很大。
  * @param   nus:延时的微秒数,0~2^32/fac_us
  * @retval  None
  */
void delay_us(uint32_t nus)
{
    uint32_t told = SysTick->VAL;                          //记录当前时间值:放在第一句
    uint32_t fac_us = HAL_RCC_GetSysClockFreq() / 1000000; //us延时倍乘数
    uint32_t ticks = nus * fac_us;                         //存储需要节拍数
    uint32_t tnow, tcnt = 0;                               //told:上一次存储的时间,tcnt:累计的时间差
    uint32_t reload = SysTick->LOAD;                       //读取sysTick装载值


    while (1)
    {
        tnow = SysTick->VAL;
        
        /*获得上一次记录的时间和当前时间之间的差值*/
        if (tnow < told)
        {
            tcnt += told - tnow;
        }
        else
        {
            tcnt += reload - tnow + told;
        }


        /*将当前时间记录下来*/
        told = tnow;


        if (tcnt >= ticks)
            break; //时间超过/等于要延迟的时间,则退出.
    }
}


此处,对于delay_us函数进行又一步的改进:


第一次记录时间放在函数最前面,提高计算的精度。
将除法运算中的double类型替换为整数型,提高计算的速度。
delay.c更新
为了提高实时性,还是正点原子的写法比较优秀。更新最新的写法如下:


/**
  ******************************************************************************
  * @name    delay.c
  * @author  zhy
  * @version 1.0
  * @date    2020.11.11
  * @brief   通过时钟窃取法,得到延时时钟的功能。
  ******************************************************************************
  * @version 1.1
  * @date    2021.04.07
  * @brief   更新delay_us函数,减小delay误差
  ******************************************************************************
  * @version 1.2
  * @date    2021.05.21
  * @brief   更新delay_us函数,减小delay误差
  ******************************************************************************
  */


#include "stm32f4xx.h" //包含该头文件不会出现引用错误
#include "delay.h"


//{
/*-----------------------------------私有域:开始--------------------------------------*/


uint32_t fac_us = 0; //us延时倍乘数
uint32_t reload = 0; //读取sysTick装载值


/*-----------------------------------私有域:结束--------------------------------------*/
//}


/**
* @brief delay函数初始化
* @note 使用delay函数必须要初始化
* @param {*}无
* @retval 无
*/
void delay_init(void)
{
    fac_us = HAL_RCC_GetSysClockFreq() / 1000000; //系统主频/10^6
    reload = SysTick->LOAD;                       //过边界点
}


/**
  * @brief   延时函数微秒
  * @note    当nus取值比较小时,该延时函数的误差比较大
  * @param   nus:延时的微秒数,0~2^32/fac_us
  * @retval  None
  */
void delay_us(uint32_t nus)
{
    uint32_t told = SysTick->VAL;  //记录当前时间值:放在第一句
    uint32_t ticks = nus * fac_us; //存储需要节拍数
    uint32_t tnow, tcnt = 0;       //told:上一次存储的时间,tnew:当前时间;tcent:累计的时间差


    while (1)
    {
        tnow = SysTick->VAL;                                                    //时钟时递减时钟
        tcnt = tnow <= told ? tcnt + told - tnow : tcnt + reload - tnow + told; //获得上一次记录的时间和当前时间之间的差值
        if (tcnt >= ticks)
            return;  //时间超过/等于要延迟的时间,则退出.
        told = tnow; //将当前时间记录下来
    }
}


/**
  * @brief   延时函数毫秒
  * @note    None
  * @param   nms:延时的毫秒数
  * @retval  None
  */
void delay_ms(uint16_t nms)
{
    uint32_t i;
    for (i = 0; i < nms; i++)
        delay_us(1000);
}
举报

更多回帖

发帖
×
20
完善资料,
赚取积分