Bootstrap

STM32F429第十篇之systick

前言

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);
}

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

  1. delay初始化函数
  2. 微秒延时函数
  3. 毫秒延时函数

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

delay初始化函数

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

  1. 将Systick的时钟源设置为HCLK,一般的,也就是系统时钟180MHz。
  2. 将系统时钟传递给全局变量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;			//时间超过/等于要延迟的时间,则退出.
        }
    }
}

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

  1. systick的时钟源为AHB,一般的也就是系统时钟,为180MHz。其周期为1/180 us。
  2. 累积的时间界定值为:ticks = nus * fac_us=180*nus。
  3. 所以,累积时间为:ticks*1/180=nus。

综上,参数的取值就是延时的微秒数。

HAL库

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

HAL_InitTick(TICK_INT_PRIORITY);

其源程序定义为:

__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 */
}

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

  1. 设置计数器的计数周期。
  2. 清除计数器当前计数值。
  3. 启动计数器开始计数,启动中断,设置时钟源为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函数进行又一步的改进:

  1. 第一次记录时间放在函数最前面,提高计算的精度。
  2. 将除法运算中的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);
}


;