前言
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()使用介绍,可以参考博客<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 */
}
除去中断部分,我们可以看到,该程序共有三个功能:
- 设置计数器的计数周期。
- 清除计数器当前计数值。
- 启动计数器开始计数,启动中断,设置时钟源为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);
}