STM32学习笔记(5)——系统定时器SysTick
单独拿出来讲的一个内核外设(所以不要期望在STM32中文参考手册找到它!即使找到也只会叫你看cm3内核编程手册),说明它真的很重要。
一、系统定时器Systick
1. SysTick简介
SysTick是一个24位的系统节拍定时器,具有自动重载和溢出中断功能,所有基于Cortex M3或Cortex M4处理器的微控制器都有这个定时器。
Systick定时器常用来做延时,或者用来做实时系统的心跳时钟。这样可以节省MCU资源,不用浪费一个定时器。比如UCOS中,分时复用,需要一个最小的时间戳,一般在STM32+UCOS系统中,都采用Systick做UCOS心跳时钟。
它一个24位的倒计数定时器(意味着有2^24个时间间隔),计到0时,将从RELOAD寄存器中自动重装载定时初值。只要不把它在SysTick控制及状态寄存器中的使能位清除,就永不停息,即使在睡眠模式下也能工作。
学过51单片机的同学应该对计数器比较熟悉了。
SysTick在NVIC的中断向量号为6(可对照STM32中文参考手册9.1.2节中断和异常向量中表格的灰色部分)。
2. SysTick相关寄存器
SysTick有4个寄存器:CTRL(控制和状态寄存器)、LOAD(自动重装载除值寄存器)、VAL(当前值寄存器)、CALIB(校准值寄存器)(如图所示)。
关于这4个寄存器的参考资料为 《 STM32F10xxx Cortex-M3编程手册》(STM32F10xxx/20xxx/21xxx/L1xxxx
Cortex ® -M3 programming manual)(没找到中文版的,自己生啃一下英文吧),4.5节SysTick timer (STK)。4.5.6节还有SysTick寄存器地图。
由于篇幅够多,下面我们来一一介绍这4个寄存器(其实引用了上述手册的介绍):
(1)SysTick control and status register (STK_CTRL)
16位(COUNTFLAG): SysTick数到0后,该位为1;其他情况为0。如果读取该位,也会置0。说白了就是用来标志是不是数完了。
2位(CLKSOURCE): 0为外部时钟源(STCLK)(HCLK的1/8),1为内部时钟源(FCLK)(HCLK)。用来选择时钟源的。顺便提一句,HCLK用的是AHB时钟。
1位(TICKINT): 1为计数器数到零时产生SysTick中断请求,0为不产生请求。
0位(ENABLE): 用来使能的。
其余位保留。
(2)SysTick reload value register (STK_LOAD)
23位-0位(RELOAD): 存储的是一个值,当计数到0时,将重新装载该值,开始新一轮倒数。
其余位保留。
(3)SysTick current value register (STK_VAL)
23位-0位(CURRENT): 跟上面的有点像,也是存储一个值。但是,当你读取它时,寄存器会返回这个值(就是读取了这个值);当你写它时会自动清零,同时CTRL寄存器的第16位(COUNTFLAG)置0。
其余位保留。
(4)SysTick calibration value register (STK_CALIB)
这个寄存器似乎比较少关注到,对于用户来讲应该不常用?
31位(NOREF): 1为没有外部参考时钟(外部时钟源STCLK不可用),0为外部参考时钟可用。
30位(SKEW): 1为校准值不是准确的10ms,0为校准值是准确的10ms。
23位-0位(TENMS): 存储的是10ms的间隔倒计时的格数。芯片设计者应该提供这数值,若该值为0,则无法使用校准功能。
其余位保留。
在头文件core_cm3.h中有结构体定义:
typedef struct
{
__IO uint32_t CTRL; /*!< Offset: 0x00 SysTick Control and Status Register */
__IO uint32_t LOAD; /*!< Offset: 0x04 SysTick Reload Value Register */
__IO uint32_t VAL; /*!< Offset: 0x08 SysTick Current Value Register */
__I uint32_t CALIB; /*!< Offset: 0x0C SysTick Calibration Register */
} SysTick_Type;
3. 详解SysTick_Config()函数
在使用SysTick之前,用户必须调用SysTick_Config()函数对其进行配置。这个函数是怎样进行配置的呢?我们可以看看头文件core_cm3.h 的1694行:
static __INLINE uint32_t SysTick_Config(uint32_t ticks)
{
if (ticks > SysTick_LOAD_RELOAD_Msk) return (1); /* Reload value impossible */
SysTick->LOAD = (ticks & SysTick_LOAD_RELOAD_Msk) - 1; /* set reload register */
NVIC_SetPriority (SysTick_IRQn, (1<<__NVIC_PRIO_BITS) - 1); /* set Priority for Cortex-M0 System Interrupts */
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 */
}
uint32_t SysTick_Config(uint32_t ticks)
函数形参ticks
为两个中断之间的脉冲数(number of ticks between two interrupts),即相隔ticks个时钟周期会引起一次中断。
if (ticks > SysTick_LOAD_RELOAD_Msk) return (1);
判断ticks
是否符合规则,这个是什么规则呢?就是判断ticks
是否大于SysTick_LOAD_RELOAD_Msk
,如果大于,则返回1(failed),表示不符合规则。
SysTick_LOAD_RELOAD_Msk
是一个宏,在文件388行可以找到:
#define SysTick_LOAD_RELOAD_Pos 0
#define SysTick_LOAD_RELOAD_Msk (0xFFFFFFul << SysTick_LOAD_RELOAD_Pos)
0xFFFFFF在十进制中是2^24(=16777216),其中,ul的意思是无符号long型整数(
unsigned long int)。然后我们就知道,ticks
最大值不能超过2^24(因为是24位系统定时器),否则就配置失败。
SysTick->LOAD = (ticks & SysTick_LOAD_RELOAD_Msk) - 1; /* set reload register */
这里就是将ticks写入load寄存器,ticks是计数最开始的值。减去一的原因是计数从0结束。
NVIC_SetPriority (SysTick_IRQn, (1<<__NVIC_PRIO_BITS) - 1);
NVIC中断配置,SysTick_IRQn
在头文件stm32f10x.h可以找到:
SysTick_IRQn = -1, /*!< 15 Cortex-M3 System Tick Interrupt*/
宏定义__NVIC_PRIO_BITS
可以在本文件core_cm3.h找到:
#define __NVIC_PRIO_BITS 4 /*!< STM32 uses 4 Bits for the Priority Levels*/
因此(1<<__NVIC_PRIO_BITS) - 1
的意思是1左移4位(即16),表达式的值为15(注意二进制表示为1111),这里面的4是因为(寄存器NVIC->IPRx)使用4个位来配置中断优先级。
所以SysTick的优先级是最低的吗?由于没有指定优先级分组,所以这就要看怎么分了。
假如分组为1,则将1111分为两组,前面组为抢占优先级,只有一个1(说明抢占优先级为1),而后面组为响应优先级,为111(说明响应优先级为7),需要注意到这里1和0的个数表示了二进制的数,进而指明了优先级别的高低。
假如分组为2,则将1111分为两组,前面组为抢占优先级,有11(说明抢占优先级为3),而后面组为响应优先级,为11(说明响应优先级为3)。
假如分组为3,则将1111分为两组,前面组为抢占优先级,有111(说明抢占优先级为7),而后面组为响应优先级,为1(说明响应优先级为1)。以此类推,所以SysTick的优先级是不一定的,只要你去配置,你想怎样都行。
SysTick->VAL = 0;
将0载入到val寄存器中。
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk | SysTick_CTRL_TICKINT_Msk | SysTick_CTRL_ENABLE_Msk;
配置时钟源、中断请求和使能。
4. SysTick相关的函数
SysTick_CLKSourceConfig() //Systick时钟源选择,在misc.c中
SysTick_Config(uint32_t ticks) //初始化systick,时钟为HCLK,并开启中断,在core_m3.h中
void SysTick_Handler(void);
//Systick中断服务函数,需要用户自行编写
二、延时函数
使用SysTick写出来的延时函数,其精度要比软件延时要高。不过这个函数要自己写。
下面是一个写好的微妙级别的延时函数,我们来分析一下:
void Systick_Delay_us(uint32_t us)
{
uint32_t i;
SysTick_Config(72);
for( i = 0; i < us; i++)
{
while( !((SysTick -> CTRL) & (1 << 16)) );
}
SysTick -> CTRL &= ~SysTick_CTRL_ENABLE_Msk;
}
SysTick_Config(72);
配置SysTick时钟。这里为什么是72呢?有个公式:t = reload * ( 1/clk )
,clk为内核时钟频率,reload为LOAD寄存器的值。在这个程序中,Clk = 72MHz,reload = 72,因此 t = (72) *(1 / 72 MHz)= 1us。
while( !((SysTick -> CTRL) & (1 << 16)) );
这里有必要说明一下(SysTick -> CTRL) & (1 << 16)
是什么意思。SysTick -> CTRL
是CTRL寄存器的首地址,1<<16为左移16位,两者进行与运算,正好得到CTRL寄存器第16位地址,即COUNTFLAG,因此这条语句是用来判断计数是否到0。如果没到0,while继续循环。每跳出一次while循环,说明已经过去了1us的时间,而for循环则循环了指定次数的1us。
SysTick -> CTRL &= ~SysTick_CTRL_ENABLE_Msk;
每一次使用完SysTick后要记得关闭。
三、简单例程
1. 不使用中断服务函数
//在systick.c中
void Systick_Delay_us(uint32_t us)
{
uint32_t i;
SysTick_Config(72);
for( i = 0; i < us; i++)
{
while( !((SysTick -> CTRL) & (1 << 16)) );
}
SysTick -> CTRL &= ~SysTick_CTRL_ENABLE_Msk;
}
void Systick_Delay_ms(uint32_t ms)
{
uint32_t i;
SysTick_Config(72000);
for( i = 0; i < ms; i++)
{
while( !((SysTick -> CTRL) & (1 << 16)) );
}
SysTick -> CTRL &= ~SysTick_CTRL_ENABLE_Msk;
}
//在main.c中
//实现功能:每隔500ms,LED0和LED1交错亮灭一次
while(1)
{
GPIO_ResetBits(GPIOB,GPIO_Pin_5);
GPIO_SetBits(GPIOE,GPIO_Pin_5);
Systick_Delay_ms(500);
GPIO_SetBits(GPIOB,GPIO_Pin_5);
GPIO_ResetBits(GPIOE,GPIO_Pin_5);
Systick_Delay_ms(500);
}
2. 使用中断服务函数
//systick.h
#ifndef __SYSTICK_H
#define __SYSTICK_H
#include "stm32f10x.h"
#include "core_cm3.h"
void Systick_Delay_ms(uint32_t ms);
void Systick_Delay_us(uint32_t us);
void SysTick_Init(void);
void Delay_ms(uint32_t ms);
#endif
//systick.c
#include "systick.h"
/* use interrupt */
__IO uint32_t TimingDelay;
void SysTick_Init(void)
{
if(SysTick_Config(72000) == 1)
{
while(1);
}
SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk;
}
void Delay_ms(uint32_t ms)
{
TimingDelay = ms;
SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk;
while(TimingDelay != 0);
}
//stm32f10x_it.h
#include "stm32f10x_it.h"
extern uint32_t TimingDelay; //看见这玩意我就恶心
void SysTick_Handler(void)
{
if(TimingDelay != 0x00)
{
TimingDelay--;
}
}
//main.c
SysTick_Init();
while(1)
{
GPIO_ResetBits(GPIOB,GPIO_Pin_5);
GPIO_SetBits(GPIOE,GPIO_Pin_5);
Delay_ms(500);
GPIO_SetBits(GPIOB,GPIO_Pin_5);
GPIO_ResetBits(GPIOE,GPIO_Pin_5);
Delay_ms(500);
}
上面这种写法用了extern全局变量,个人感觉很别扭,但是中断服务函数是没有形参的,所以还有一种折中的写法(自己写一个函数,然后中断服务函数去调用):
//systick.h
#ifndef __SYSTICK_H
#define __SYSTICK_H
#include "stm32f10x.h"
#include "core_cm3.h"
void Systick_Delay_ms(uint32_t ms);
void Systick_Delay_us(uint32_t us);
void SysTick_Init(void);
void Delay_ms(uint32_t ms);
void timeDec(void); //自己写的函数,待会服务函数去调用
static __IO uint32_t TimingDelay;
#endif
//systick.c
#include "systick.h"
/* use interrupt */
void SysTick_Init(void)
{
if(SysTick_Config(72000) == 1)
{
while(1);
}
SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk;
}
void Delay_ms(uint32_t ms)
{
TimingDelay = ms;
SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk;
while(TimingDelay != 0);
}
void timeDec(void)
{
if(TimingDelay != 0x00)
{
TimingDelay--;
}
}
//stm32f10x_it.h
#include "stm32f10x_it.h"
void SysTick_Handler(void)
{
timeDec(); //调用自己写的函数
}
再次,我个人习惯是,extern只有到万不得已才使用,因为这玩意太破坏文件之间的耦合度了,而且被多个文件使用,不太好追踪源头。