1 SysTick定时器简介
什么是SysTick?
SysTick定时器也叫SysTick滴答定时器,它是Cortex-M3内核的一个外设,被嵌入在NVIC 中,用于产生SYSTICK异常(异常号:15)。SysTick是一个24位的倒计数系统节拍计时器System Tick timer,每计数一次所需时间为1/SYSTICK,SYSTICK是系统定时器时钟,它可以直接取系统时钟,还可以通过系统时钟8分频后获取。当计数到0时,它就会从Load寄存器中自动重装定时初值,重新向下递减计数,如此循环往复。如果开启SysTick中断的话,当定时器计数到0,将产生一个中断信号。因此只要知道计数的次数就可以准确得到它的延时时间。只要不把CTRL寄存器中的ENABLE清0,它就永不停止。
SysTick作用?
在单任务引用程序中,因为其架构就决定了它执行任务的串行性,这就引出一个问题:当某个任务出现问题时,就会牵连到后续的任务,进而导致整个系统崩溃。要解决这个问题,可以使用实时操作系统(RTOS)。因为RTOS以并行的架构处理任务,单一任务的崩溃并不会牵连到整个系统。这样用户出于可靠性的考虑可能就会基于RTOS来设计自己的应用程序。SYSTICK存在的意义就是提供必要的时钟节拍,为RTOS的任务调度提供一个有节奏的“心跳”。微控制器的定时器资源一般比较丰富,比如STM32存在8个定时器,为啥还要再提供一个SYSTICK?原因就是所有基于ARM Cortex_M3内核的控制器都带有SysTick定时器,这样就方便了程序在不同的器件之间的移植。而使用RTOS的第一项工作往往就是将其移植到开发人员的硬件平台上,由于SYSTICK的存在无疑降低了移植的难度。SysTick定时器除了能服务于操作系统之外,还能用于其它目的:如作为一个闹铃,用于测量时间等。要注意的是,当处理器在调试期间被喊停(halt)时,则SysTick定时器亦将暂停运作。
2 SysTick时钟的选择
SysTick寄存器说明在《Cortex-M3权威指南》(SysTick定时器章节)有说明:
SysTick->CTRL:控制和状态寄存器
SysTick->LOAD:重装载寄存器
SysTick->VAL:当前值寄存器
SysTick->CALIB:校准值寄存器
用户可以在位于Cortex_M3处理器系统控制单元中的系统节拍定时器控制和状态寄存器(SysTick control and status register ,SCSR)选择SysTick 时钟源。如将SCSR中的CLKSOURCE位置位,SysTick会在CPU频率下运行;而将CLKSOUCE位清除则SysTick会以CPU主频的1/8频率运行。
在3.5版本的库函数中与SysTick相关的函数有两个:第一个,SysTick_Config(uint32_t ticks),在core_cm3.h头文件中进行定义的。第二个,void SysTick_CLKSourceConfig(uint32_t SysTick_CLKSource),在misc.c文件中定义的。
在core_cm3.h头文件中进行定义的SysTick_Config(uint32_t ticks)函数主要的作用有一下方面:
1、初始化systick
2、打开systick
3、打开systick的中断并设置优先级
4、返回一个0代表成功或1代表失败
Uint32_t ticks 即为重装值,这个函数默认使用的时钟源是AHB,即不分频。要想分频,调用void SysTick_CLKSourceConfig(uint32_t SysTick_CLKSource),但是要注意函数调用的次序,先调用SysTick_Config(uint32_t ticks),后调用SysTick_CLKSourceConfig(uint32_t SysTick_CLKSource)。
在misc.c头文件中进行定义的void SysTick_CLKSourceConfig(uint32_t SysTick_CLKSource)函数主要的作用有一下方面:
1、选择systick的时钟源,AHB时钟或AHB的8分频
2、库函数中默认使用的是AHB时钟(在SysTick_Config()函数中设置),即72MHz
1)在core_cm3.h头文件中uint32_t SysTick_Config(uint32_t ticks)函数说明:
/**
* @摘要 初始化并启动SysTick计数器及其中断。
*
* @参数 ticks 两个中断之间的滴答数
* @返回值 1 = failed, 0 = successful
*
* 初始化系统滴答计时器及其中断,并在自由运行模式下启动系统滴答计时器/计数器,以产生定时中断。
*/
static __INLINE uint32_t SysTick_Config(uint32_t ticks)
{
if (ticks > SysTick_LOAD_RELOAD_Msk) return (1); /* 不可能的重新加载值,重装载值必须小于0XFF FFFF,因为这是一个24位的递减计数器 */
SysTick->LOAD = (ticks & SysTick_LOAD_RELOAD_Msk) - 1; /* 设置重装载值寄存器,SysTick_LOAD_RELOAD_Msk定义见后面*/
NVIC_SetPriority (SysTick_IRQn, (1<<__NVIC_PRIO_BITS) - 1); /* 设置Cortex-M3系统中断优先级 */
SysTick->VAL = 0; /* 加载SysTick计数器值 */
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk | //配置CTRL寄存器,选择内核时钟FCLK为时钟源(STM32 的FCLK为72M)
SysTick_CTRL_TICKINT_Msk | //开启SysTick中断
SysTick_CTRL_ENABLE_Msk; //SysTick使能
return (0); /* 初始化成功 */
}
在core_cm3.h头文件SysTick相关的寄存器定义:
typedef struct
{
__IO uint32_t CTRL; /* Offset: 0x00 SysTick控制和状态寄存器 */
__IO uint32_t LOAD; /* Offset: 0x04 SysTick重新加载值寄存器 */
__IO uint32_t VAL; /* Offset: 0x08 SysTick当前值寄存器 */
__I uint32_t CALIB; /* Offset: 0x0C SysTick校准寄存器 */
} SysTick_Type;
在core_cm3.h头文件SysTick寄存器相关的位的宏定义:
/* SysTick控制/状态寄存器定义 */
#define SysTick_CTRL_COUNTFLAG_Pos 16 /*!< SysTick CTRL: COUNTFLAG Position */
#define SysTick_CTRL_COUNTFLAG_Msk (1ul << SysTick_CTRL_COUNTFLAG_Pos) /*!< SysTick CTRL: COUNTFLAG Mask */
#define SysTick_CTRL_CLKSOURCE_Pos 2 /*!< SysTick CTRL: CLKSOURCE Position */
#define SysTick_CTRL_CLKSOURCE_Msk (1ul << SysTick_CTRL_CLKSOURCE_Pos) /*!< SysTick CTRL: CLKSOURCE Mask */
#define SysTick_CTRL_TICKINT_Pos 1 /*!< SysTick CTRL: TICKINT Position */
#define SysTick_CTRL_TICKINT_Msk (1ul << SysTick_CTRL_TICKINT_Pos) /*!< SysTick CTRL: TICKINT Mask */
#define SysTick_CTRL_ENABLE_Pos 0 /*!< SysTick CTRL: ENABLE Position */
#define SysTick_CTRL_ENABLE_Msk (1ul << SysTick_CTRL_ENABLE_Pos) /*!< SysTick CTRL: ENABLE Mask */
/* SysTick重新加载寄存器定义 */
#define SysTick_LOAD_RELOAD_Pos 0 /*!< SysTick LOAD: RELOAD Position */
#define SysTick_LOAD_RELOAD_Msk (0xFFFFFFul << SysTick_LOAD_RELOAD_Pos) /*!< SysTick LOAD: RELOAD Mask */
/* SysTick当前寄存器定义 */
#define SysTick_VAL_CURRENT_Pos 0 /*!< SysTick VAL: CURRENT Position */
#define SysTick_VAL_CURRENT_Msk (0xFFFFFFul << SysTick_VAL_CURRENT_Pos) /*!< SysTick VAL: CURRENT Mask */
/* SysTick校准寄存器定义 */
#define SysTick_CALIB_NOREF_Pos 31 /*!< SysTick CALIB: NOREF Position */
#define SysTick_CALIB_NOREF_Msk (1ul << SysTick_CALIB_NOREF_Pos) /*!< SysTick CALIB: NOREF Mask */
#define SysTick_CALIB_SKEW_Pos 30 /*!< SysTick CALIB: SKEW Position */
#define SysTick_CALIB_SKEW_Msk (1ul << SysTick_CALIB_SKEW_Pos) /*!< SysTick CALIB: SKEW Mask */
#define SysTick_CALIB_TENMS_Pos 0 /*!< SysTick CALIB: TENMS Position */
#define SysTick_CALIB_TENMS_Msk (0xFFFFFFul << SysTick_VAL_CURRENT_Pos) /*!< SysTick CALIB: TENMS Mask */
2)在core_cm3.h头文件中void SysTick_CLKSourceConfig(uint32_t SysTick_CLKSource)函数说明:
/**
* @摘要 配置SysTick时钟源
* @参数 SysTick_CLKSource: SysTick时钟源。
* 该参数可以是以下值之一:
* @arg SysTick_CLKSource_HCLK_Div8: SysTick时钟源为AHB时钟除以8。
* @arg SysTick_CLKSource_HCLK: 选择AHB时钟作为SysTick时钟源。
* @返回值 无
*/
void SysTick_CLKSourceConfig(uint32_t SysTick_CLKSource)
{
/* 检查参数 */
assert_param(IS_SYSTICK_CLK_SOURCE(SysTick_CLKSource));
if (SysTick_CLKSource == SysTick_CLKSource_HCLK)
{
SysTick->CTRL |= SysTick_CLKSource_HCLK;
}
else
{
SysTick->CTRL &= SysTick_CLKSource_HCLK_Div8;
}
}
SysTick时钟源的定义:
#define SysTick_CLKSource_HCLK_Div8 ((uint32_t)0xFFFFFFFB) //将控制状态寄存器的第二位置0,即用外部时钟源
#define SysTick_CLKSource_HCLK ((uint32_t)0x00000004) //将控制状态寄存器的第二位置1,即用内核时钟
#define IS_SYSTICK_CLK_SOURCE(SOURCE) (((SOURCE) == SysTick_CLKSource_HCLK) || \
((SOURCE) == SysTick_CLKSource_HCLK_Div8))
3 Systick使用实践
Systick定时时间的设定:
重装载值=systick 时钟频率(Hz) X 想要的定时时间(S)
如果时钟频率为:AHB的8分频;AHB=72MHz那么systick的时钟频率为72/8MHz=9MHz。若要定时1秒,则重装载值=9000000X1=9000000,调用函数:SysTick_Config(9000000X1)。若要定时1毫秒,重状态值=9000000X0.001=9000,调用函数:SysTick_Config(9000000/1000);
Systick的中断处理函数:
在startup_stm32f10x_hd.s启动文件中有定义:DCD SysTick_Handler ; SysTick Handler
根据需要直接编写中断处理函数即可:Void SysTick_Handler (void)
注意:如果在工程中,加入了stm32f10x_it.c,而又在主函数中编写中断函数,则会报错。因为在stm32f10x_it.c文件中,也有这个中断函数的声明,只是内容是空的。
/**
* @摘要 这个函数处理SysTick Handler。
* @参数 无
* @返回值 无
*/
void SysTick_Handler(void)
{
}
中断优先级的修改:
在调用SysTick_Config(uint32_t ticks)之后,调用 void NVIC_SetPriority(IRQn_Type IRQn, uint32_t priority)。这个函数在core_cm3.h头文件中。具体内容如下:
/**
* @摘要 设置中断的优先级
* @参数 IRQn 用于设置优先级的中断数
* @参数 priority 设定的优先级
*为指定的中断设置优先级。中断号可以是正数来指定外部(设备特定的)中断,也可以是负数来指定内部(核心)中断。
* 注释: 不能为每个核心中断设置优先级。
*/
static __INLINE void NVIC_SetPriority(IRQn_Type IRQn, uint32_t priority)
{
if(IRQn < 0) {
SCB->SHP[((uint32_t)(IRQn) & 0xF)-4] = ((priority << (8 - __NVIC_PRIO_BITS)) & 0xff); } /* 设置Cortex-M3系统中断优先级 */
else {
NVIC->IP[(uint32_t)(IRQn)] = ((priority << (8 - __NVIC_PRIO_BITS)) & 0xff); } /* 设置Cortex-M3系统中断优先级 */
}
下面以一个实例来说明:利用systick来实现以1秒的时间间隔,闪亮一个LED指示灯,指示灯接在GPIOA.8,低电平点亮。
#include "stm32f10x.h"
//函数声明
void GPIO_Configuration(void); //设置GPIOA.8端口
u32 t; //定义一个全局变量
int main(void)
{
// SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8);
SysTick_Config(9000000);
SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8);
GPIO_Configuration();
while(1);
}
//GPIOA.8设置函数
void GPIO_Configuration(void)
{
GPIO_InitTypeDef GPIO_InitStruct; //定义一个端口初始化结构体
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //打开GPIOA口时钟
GPIO_InitStruct.GPIO_Mode=GPIO_Mode_Out_PP; //设置为推挽输出
GPIO_InitStruct.GPIO_Speed=GPIO_Speed_50MHz; //设置输出频率50M
GPIO_InitStruct.GPIO_Pin=GPIO_Pin_8; //指定第8脚
GPIO_Init(GPIOA,&GPIO_InitStruct); //初始化GPIOA.8
GPIO_SetBits( GPIOA, GPIO_Pin_8); //置高GPIOA.8,关闭LED
}
//systick中断函数
void SysTick_Handler(void)
{
t++;
if(t>=1)
{
if(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_8)==1)
{GPIO_ResetBits( GPIOA, GPIO_Pin_8);}
}
if(t>=2)
{
if(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_8)==0)
{GPIO_SetBits( GPIOA, GPIO_Pin_8);}
t=0;
}
}
以上基本讲完了,下面是我在网上看到的一些其他理解。
基于STM32F10x V3.5.0库如何操作Systick定时器呢?首先:STM32 的内核库已经提供了这个功能。只要配置SysTick_Config()即可实现。看下面的程序段:
/*
* 函数名:SysTick_Init
* 描述 :启动系统滴答定时器 SysTick
* 输入 : 无
* 输出 :无
* 调用 : 外部调用
*/
void SysTick_Init(void)
{
if(SysTick_Config(SystemCoreClock/1000)) //1ms定时器
{
while(1);
}
//SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk; //若无法启动则关闭
}
SysTick_Config()的参数,其实就是一个时钟次数,叫SysTick重装定时器的值。意思就是我要多少个1/fosc 时间后中断一下。
根据学过的物理中的时间与频率的公式:fosc=1/T T=1/fosc ,fosc为系统的频率。如果STM32时钟频率为:72MHz,每次的时间为:T=1/72MHz。1秒钟为:1/(每次的时间)=1/(1/72MHz)=72 000 000次。1MHz是:1000 000。反过来讲。SysTick_Config(72000)代表:72000*(1/72MHz)=1/1000=1(ms)。即定时为1ms。如果需要1S,可以通过设置一个全局变量,然后定初值得为1000,这样,每个systick中断一次,这个全局变量减1,减到0,即systick中断1000次,时间为:1ms*1000=1S。从而实现1S的定时。因为SysTick定时器是:24位的,最大定时时间为:2的24次方*(1/72MHz)的时间,这里系统频率为:72MHz的情况下。
如何使用这个Systick用于程序设计上的延时或是定时作用呢?如下:__IO uint32_t TimingDelay; 定义一个全局变量,注意类型为 volatile的。volatile的作用: 作为指令关键字,确保本条指令不会因为编译器的优化而省略,且要求每次直接读值,然后定义一个延时或是定时函数:
/*
* 函数名:Delay_ms
* 描述 :ms延时程序,1ms为一个单位
* 输入 : - nTime
* 输出 :无
* 示例 : Delay_ms(1) 实现的延时为:1*ms=1ms
* 调用 :外部调用
*/
void Delay_ms(uint16_t nTime)
{
TimingDelay = nTime;
//使能系统滴答定时器
while(TimingDelay !=0);
}
还要在系统的中断函数文件:stm32f10x_it.c/h 里面,修改系统自带的 SysTick 函数。这个函数要么没有声明或是为空操作。这里加入定时延时里的处理。即中断后,全局变量做个延时处理即可。在stm32f10x_it.c里修改如下:添加外部的声明:extern __IO uint32_t TimingDelay; 修改这个函数:SysTick_Handler ,这是系统的关于SysTick_Handler的中断服务程序名,在启动文件里如:startup_stm32f10x_hd.s 有它的定义的名字。不要弄错了。否则无法中断处理。
/**
* @摘要 这个函数处理SysTick Handler。
* @参数 无
* @返回值 无
*/
void SysTick_Handler(void)
{
if (TimingDelay != 0x00)
{
TimingDelay--;
}
}
以上,即定义配置好了Systick定时器。如何使用呢?很简单。Delay_ms(500); 即为延时500ms。当然,使用前,请先初始化:SysTick_Init();否则无法使用并影响后续的程序运行,这个很重要,就像打开了串口中断,你不清标志位,也同样在接收字符后,CPU中断在那里,而不能继续执行!。使用外设功能,需要初始化!
4 配置延时函数(systick定时器)
4.1 delay_init函数
该函数初始化了两个重要参数:fac_us和fac_ms,同时将SysTick是时钟源配置为外部时钟,具体代码如下:
//初始化延迟函数
//SYSTICK的时钟固定为HCLK时钟的1/8
//SYSCLK:系统时钟
void delay_init()
{
//选择外部时钟 HCLK/8
SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8);
fac_us = SystemCoreClock/8000000; //为系统时钟的1/8
fac_ms = fac_us * 1000; //1ms = 1us * 1000
}
SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8); 这里把SysTick的时钟配置为外部时钟,要注意的是SysTick 的时钟源自与HCLK的8分频,如果外部晶振为8MHz,然后倍频到72MHz,那么SysTick的时钟就是72/8 = 9MHz,即是SysTick的计数器每减1,时间就过去了1/9us。所以fac_us = SystemCoreClock/8000000; 表示过了1us,可能有部分人对这句话不太理解,下面做个大概解释,上面说到systick为系统时钟的8分频,假设系统时钟倍频到72MHz,8分频即为9MHz,所以每一个systick时钟周期的时间为1/9us,SystemCoreClock/8000000 的意思是算出 1us 需要几个 systick 周期,即72000000/8000000 = 9个systick时钟周期,fac_us = 9 * 1/9us = 1us 没毛病,还没看懂的多看几遍就理解了,接下来的fac_ms = fac_us * 1000,1ms等于1000 us 这个不用怎么解释了。
该注意的是delay_init函数需要在main函数的开头调用作为对systick定时器的配置,fac_us和fac_ms变量需要在定义为全局变量,因为接下来的两个函数还将用到。
static uint8_t fac_us; //us延时倍乘数
static uint16_t fac_ms; //ms延时倍乘数,在ucos下,代表每个节拍的ms数
4.2 delay_us函数
该函数用来延时指定的us,其参数nus为要延时的毫秒数,代码如下:
//功能: delay_us
//参数: nus
void delay_us(uint32_t nus)
{
uint32_t temp;
SysTick->LOAD = nus * fac_us; //时间加载
SysTick->VAL = 0x00; //清空计数器
SysTick->CTRL|= SysTick_CTRL_ENABLE_Msk; //开始倒数
do{
temp = SysTick->CTRL;
}while(temp&0x01&&! (temp&(1<<16))); //等待时间到达
SysTick->CTRL&= ~SysTick_CTRL_ENABLE_Msk; //关闭计数器
SysTick->VAL = 0x00; //清空计数器
}
以上是配置SysTick的寄存器实现的延时,有兴趣的可以参阅core_cm3.h头文件CTRL、LOAD、VAL、CALIB
这4个寄存器。
4.3 delay_ms函数
该函数用来延时指定的ms,其参数nms为要延时的毫秒数,代码如下:
//功能: delay_ms
//参数: nms 注意nms的范围
//SysTick->LOAD为24位寄存器,所以,最大延时为:nms<=0xffffff*8*1000/SYSCLK
//SYSCLK单位为Hz,nms单位为ms,对72M条件下,nms<=1864
void delay_ms(uint16_t nms)
{
uint32_t temp;
SysTick->LOAD = nms * fac_ms; //时间加载(SysTick->LOAD为24bit)
SysTick->VAL = 0x00; //清空计数器
SysTick->CTRL|= SysTick_CTRL_ENABLE_Msk; //开始倒数
do
{
temp = SysTick->CTRL;
} while (temp&0x01&&! (temp&(1<<16))); //等待时间到达
SysTick->CTRL&= ~SysTick_CTRL_ENABLE_Msk; //关闭计数器
SysTick->VAL = 0x00; //清空计数器
}
创建 delay.h
加入上面3个函数的声明,然后在main函数或者其他函数中直接调用 delay_us()
函数和 delay_ms()
函数就可以了。
5 总结
1)要使用systick定时器,只需调用SysTick_Config(uint32_t ticks)函数即可,函数自动完成:重装载值的装载,时钟源选择,计数寄存器复位,中断优先级的设置(最低),开中断,开始计数的工作。
2)要修改时钟源调用SysTick_CLKSourceConfig(uint32_t SysTick_CLKSource),也可按照SysTick_Config()中默认设置FCLK不变。
3)要修改中断优先级调用 void NVIC_SetPriority(IRQn_Type IRQn, uint32_t priority)
应用说明:
1)因systick是一个24位的定时器,故重装值最大值为2的24次方=16 777 215,要注意不要超出这个值。
2)systick是cortex_m3的标配,不是外设。故不需要在RCC寄存器组打开他的时钟。
3)每次systick溢出后会置位计数标志位和中断标志位,计数标志位在计数器重装载后被清除,而中断标志位也会随着中断服务程序的响应被清除,所以这两个标志位都不需要手动清除。
4)采用使用库函数的方法,只能采用中断的方法响应定时器计时时间到,如要采用查询的方法,那只能采用设置systick的寄存器的方法,具体操作以后再做分析。