6、Sysstick(系统滴答定时器)
6.1 SysTick——操作系统的心跳
SysTick定时器被捆绑在NVIC中,用于产生SysTick异常(异常号:15)。在以前,操作系统和有所有使用了时基的系统,都必须要一个硬件定时器来产生需要的“滴答”中断,作为整个系统的时基。滴答中断对操作系统尤其重要。例如,操作系统可以为多个任务许以不同数目的时间片,确保没有一个任务能霸占系统;或者把每个定时器周期的某个时间范围赐予特定的任务等,还有操作系统提供的各种定时功能,都与这个滴答定时器有关。因此,需要一个定时器来产生周期性的中断,而且最好还让用户程序不能随意访问它的寄存器,以维持操作系统“心跳”的节律。
Cortex-M3在内核部分 包含了一个简单的定时器——SysTick timer。因为所有的CM3芯片都带有这个定时器,软件在不同芯片生产厂商的 CM3器件间的移植工作就得以化简。该定时器的时钟源可以是内部时钟(FCLK,CM3上的自由运行时钟),或者是外部时钟( CM3处理器上的STCLK信号)。不过,STCLK的具体来源则由芯片设计者决定,因此不同产品之间的时钟频率可能会大不相同。因此,需要阅读芯片的使用手册来确定选择什么作为时钟源。在STM32中SysTick 以 HCLK(AHB时钟)或HCLK/8 作为运行时钟。见图61。
图61 时钟树(部分)-SysTick timer 时钟来源
SysTick定时器能产生中断,CM3为它专门开出一个异常类型,并且在向量表中有它的一席之地。它使操作系统和其它系统软件在CM3器件间的移植变得简单多了,因为在所有CM3产品间,SysTick的处理方式都是相同的。SysTick定时器除了能服务于操作系统之外,还能用于其它目的:如作为一个闹铃,用于测量时间等。
Systick 定时器属于cortex内核部件,可以参考《CortexM3权威指南》或《STM32xxx-Cortex编程手册》来了解
6.2 SysTick timer工作分析
SysTick是一个24位的定时器,即一次最多可以计数224 个时钟脉冲,这个脉冲计数值被保存到 当前计数值寄存器STK_VAL (SysTick current value register) 中,只能向下计数,每接收到一个时钟脉冲STK_VAL的值就向下减1,直至0,当STK_VAL的值被减至0时,由硬件自动把重载寄存器STK_LOAD(SysTick reload value register)中保存的数据加载到STK_VAL,重新向下计数。当STK_VAL的值被计数至0时,触发异常,就可以在中断服务函数中处理定时事件了。
当然,要使SysTick进行以上工作必须要进行SysTick进行配置。它的控制配置很简单,只有三个控制位和一个标志位,都位于寄存器STK_CTRL(SysTick control and status register )中,见图6。
图62 Systick CTRL寄存器
Bit0: ENABLE
为SysTick timer 的使能位,此位为1的时候使能SysTick timer,此位为0的时候关闭SysTick timer。
Bit1:TICKINT
为异常触发使能位,此位为1的时候并且STK_VAL计数至0时会触发SysTick异常,此位被配置为0的时候不触发异常
Bit2:CLKSOURCE
为SysTick的时钟选择位,此位为1的时候SysTick的时钟为AHB时钟,此位为0的时候SysTick时钟为AHB/8(AHB的八分频)。
Bit16:COUNTFLAG
为计数为0标志位,若STK_VAL计数至0,此标志位会被置1。
与SysTick控制相关的所有寄存器如图 02,其中上面没有介绍的STK_CALIB寄存器是用于校准的,不常用。
图 02 SysTick寄存器映像
6.3 SysTick精确延时实例精讲
前面的的实验例程中,当有延时需要的时候,我们都是利用内核循环执行变量自减的代码来实现,延时的时间无法精确测量,有很大的局限性,当我们需要精确延时时,就可以利用SysTick timer实现,理论上它的最小计时单位为AHB的时钟周期,即1/72000000 秒,72分之一的微秒,足以满足大部分极端应用需求。本小节以实例讲解如何利用SysTick进行精确延时。
6.3.1实验描述及工程文件清单
6.3.2配置工程环境
本SysTick timer精确延时实验中我们用到了GPIO、RCC外设,所以我们先要把以下库文件添加到工程stm32f10x_gpio.c、stm32f10x_rcc.c 。
由于本实验中,SysTick的中断是在文件core_cm3.h的函数配置的,没有使用NVIC来配置中断,所以可不添加misc.c文件 。而core_cm3.h在包含stm32f10x.h头文件时已被添加进工程了。
接下来添加旧工程中的外设用户文件led.c,新建SysTick.c及SysTick.h文件,并在 stm32f10x_conf.h 中把使用到的ST库的头文件注释去掉。
/**
*****************************************************
* @file Project/STM32F10x_StdPeriph_Template/stm32f10x_conf.h
* @author MCD Application Team
* @version V3.5.0
* @date 08-April-2011
* @brief Library configuration file.
**************************************/
#include "stm32f10x_gpio.h"
#include "stm32f10x_rcc.h"
6.3.3 main文件
我们从看main函数看起:
/*
* 函数名:main
* 描述 :主函数
* 输入 :无
* 输出 :无
*/
int main(void)
{
/* LED 端口初始化 */
LED_GPIO_Config();
/* 配置SysTick 为10us中断一次 */
SysTick_Init();
for(;;)
{
LED1( 0 );
Delay_us(50000); // 50000 * 10us = 500ms
LED1( 1 );
LED2( 0 );
Delay_us(50000); // 50000 * 10us = 500ms
LED2( 1 );
LED3( 0 );
Delay_us(50000); // 50000 * 10us = 500ms
LED3( 1 );
}
}
在main函数中,我们只见到SysTick_Init() 和Delay_us() 这两个函数比较陌生,它们的功能分别是配置好SysTick 定时器和进行精确延时。
整个main函数的流程就是先初始化好LED及SysTick定时器之后,就进入死循环,轮流点亮LED1、LED2、LED3,点亮的时间为精确的500ms。
6.3.4配置并启动SysTick timer
接下来我们看一下SysTick_Init() 这个函数,它是由用户在SysTick.c这个文件中实现的,其功能是启动系统滴答定时器SysTick,并将SysTick配置为 10 us 中断一次:
/*
* 函数名:SysTick_Init
* 描述 :启动系统滴答定时器 SysTick
* 输入 :无
* 输出 :无
* 调用 :外部调用
*/
void SysTick_Init(void)
{
/* SystemFrequency / 1000 1ms中断一次
* SystemFrequency / 100000 10us中断一次
* SystemFrequency / 1000000 1us中断一次
*/
// if (SysTick_Config(SystemFrequency / 100000)) // ST3.0.0库版本
if (SysTick_Config(SystemCoreClock / 100000)) // ST3.5.0库版本
{
/* Capture error */
while (1);
}
// 关闭滴答定时器
SysTick->CTRL &= ~ SysTick_CTRL_ENABLE_Msk;
}
本函数实际上只是调用了SysTick_Config()函数,它是属于内核层的Cortex-M3通用函数,位于core_cm3.h文件中,若调用SysTick_Config() 配置SysTick不成功,则进入死循环,初始化SysTick成功后,先关闭定时器,在需要的时候再开启。
SysTick_Config() 函数无法在《STM32外设固件库帮助手册.chm》文件中找到其使用方法。所以我们在keil环境下直接跟踪这个函数到core_cm3.h文件,查看函数的定义:
/**
* @brief Initialize and start the SysTick counter and its interrupt.
*
* @param ticks number of ticks between two interrupts
* @return 1 = failed, 0 = successful
*
* Initialise the system tick timer and its interrupt and start the
* system tick timer / counter in free running mode to generate
* periodical interrupts.
*/
static __INLINE uint32_t SysTick_Config(uint32_t ticks)
{
/* Reload value impossible */
if (ticks > SysTick_LOAD_RELOAD_Msk) return (1);
/* set reload register */
SysTick->LOAD = (ticks & SysTick_LOAD_RELOAD_Msk) - 1;
/* set Priority for Cortex-M0 System Interrupts */
NVIC_SetPriority (SysTick_IRQn, (1<<__NVIC_PRIO_BITS) - 1);
/* Load the SysTick Counter Value */
SysTick->VAL = 0;
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk |
SysTick_CTRL_TICKINT_Msk |
SysTick_CTRL_ENABLE_Msk; /* Enable SysTick IRQ and SysTick Timer */
return (0); /* Function successful */
}
在这个函数定义的前面,有关于它的注释,如果我们不想去研究它的具体实现,可以根据这段注释了解函数的功能:这个函数启动了SysTick timer;并把它配置为计数至0时引起中断;输入的参数ticks为两个中断之间的脉冲数,即相隔ticks个时钟周期会引起一次中断;配置SysTick成功时返回0,出错进返回1。
但是,这段注释并没有告诉我们它把SysTick的时钟设置为AHB时钟还是AHB/8,这是一个十分关键的问题,于是,野火对这个函数的具体实现进行分析,与大家再分享一下如何分析底层库函数。
分析底层库函数,要有0小节关于SysTick timer工作分析的知识准备。
检查输入参数
SysTick_Config()第1行代码是检查输入参数ticks,因为ticks是脉冲计数值,要被保存到重载寄存器STK_LOAD寄存器中,再由硬件把STK_LOAD值加载到 当前计数值寄存器STK_VAL使用的,STK_LOAD和STK_VAL都是24位的,所以当输入参数ticks大于其可存储的最大值时,将由这行代码检查出错误返回。
位指示宏及位屏蔽宏
检查ticks参数没有错误后,就稍稍处理一下把ticks-1赋值给STK_LOAD寄存器,要注意的是减1,若STK_VAL从ticks-1向下计数至0,实际上就经过了ticks个脉冲。这句赋值代码中使用到了宏SysTick_LOAD_RELOAD_Msk,与其它库函数类似,这个宏是用来指示寄存器的特定位置 或进行位屏蔽用的。它及类似的宏定义如下:
/* SysTick Control / Status Register Definitions */
#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 Reload Register Definitions */
#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 Current Register Definitions */
#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 Calibration Register Definitions */
#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 */
/*@}*/ /* end of group CMSIS_CM3_SysTick */
其中的寄存器位指示宏:SysTick_xxx_Pos ,宏展开后即为xxx在相应寄存器中的位置,如控制SysTick时钟源的SysTick_CTRL_CLKSOURCE_Pos ,宏展开为2,这个寄存器位正是在寄存器STK_CTRL中的Bit2。
而寄存器位屏蔽宏:SysTick_xxx_Msk,宏展开是xxx的位全部置1后,左移SysTick_xxx_Pos位。如控制SysTick时钟源的SysTick_CTRL_CLKSOURCE_Msk,宏展开为 (1ul << SysTick_CTRL_CLKSOURCE_Pos) ,把无符号长整型数值(ul) 1左移2位,得到了一个只有Bit2:CLKSOURCE 位被置1,其它位为0的数值,这样的数值配合位操作 &(按位与)、| (按位或)可以很方便地修改寄存器的某些位。假如控制CLKSOURCE 需要四个寄存器位,这个宏就应该被改为(0xf ul << SysTick_CTRL_CLKSOURCE_Pos) ,这样就会得到一个关于CLKSOURCE的四位被置1的值,这些宏的参数就是这样被确定的。
寄存器位指示宏和位屏蔽宏在操作寄存器的代码(大部分库函数)中用得十分广泛,在前面GPIO_Init()函数分析时也遇到很多,为了方便以后再使用,野火就给这两类宏取了这两个名字。
配置中断向量及重置STK_VAL寄存器
回到SysTick_Config()函数,接下来调用了NVIC_SetPriority ()函数配置了SysTick中断,这就是为什么我们在外部没有再使用NVIC配置SysTick中断的原因。配置好SysTick中断后把STK_VAL寄存器重新赋值为0(在使能SysTick时,硬件会把存储在STK_LOAD寄存器中的ticks值加载给它)。
配置SysTick timer时钟为AHB
在这段代码最后,向STK_CTRL寄存器写入了SysTick timer的控制参数,配置为使用AHB时钟,使能计数至0时引起中断,使能SysTick。执行了这行代码,SysTick就开始运行,进行脉冲计数了。
若读者想要使用AHB/8作为时钟,可以调用库函数SysTick_CLKSourceConfig()进行修改,也可以直接对SysTick_Config()函数的代码进行修改。
使能、关闭定时器
由于调用SysTick_Config()函数之后,SysTick定时器就被开启了,但我们在初始化的时候并不希望这样,而是在需要的时候再开启。所以在SysTick_Init()函数中,调用完SysTick_Config() 配置好后先把定时器关闭了。 SysTick的开启和关闭由寄存器STK_CTRL的Bit0:ENABLE位 来控制,使用位屏蔽宏,以操作寄存器的方式实现:
// 使能滴答定时器
SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk;
// 失能滴答定时器
SysTick->CTRL &= ~ SysTick_CTRL_ENABLE_Msk;
6.3.5定时时间的计算
现在回到函数SysTick_Init(),在调用SysTick_Config()函数时,向它输入的参数为:SystemCoreClock / 100000 ,SystemCoreClock 为定义了系统时钟(SYSCLK)频率的宏,即等于AHB的时钟频率,本书的所有例程中AHB都是被配置为72MHz的,也就是这个SystemCoreClock 宏展开为数值7200 0000。
根据前面对SysTick_Config()函数的介绍,它的输入参数为SysTick将要计的脉冲数,经过ticks个脉冲(经过ticks个时钟周期)后将触发中断,触发中断后又重新开始计数。
由此我们可以算出定时的时间,下面为计算公式:
T=ticks*(1/f)
T 为要定时的总时间。
ticks为SysTick_Config()的输入参数。
1/ f 即为SysTick timer使用的时钟源的时钟周期,f为该时钟源的时钟频率,当时钟源确定后为常数。
例如:本实验例子中,使用时钟源为AHB时钟,其频率被配置为72MHz。调用函数时,把ticks赋值为ticks=SystemFrequency / 10 000 =720,表示720个时钟周期中断一次;(1/f)是时钟周期的时间,此时(1/f =1/72 us ),所以最终定时总时间T=720*(1/72),为720个时钟周期,正好是10us。
SysTick定时器的定时时间(配置为触发中断,即为中断周期),由ticks参数决定,最大定时周期不能超过224个。以下是几种常用的中断周期配置,就是根据上面的公式计算出来的。
/* ticks 常取以下值 */
SystemFrequency / 1000 // 1ms中断一次
SystemFrequency / 100000 // 10us中断一次
SystemFrequency / 1000000 // 1us中断一次
6.3.6编写中断服务函数
回到main函数,我们使LED工作在一个无限循环中,在LED的开与关之间调用了Delay_us()函数:
while (1)
{
//SysTick->CTRL = 1 << SYSTICK_ENABLE; // 使能滴答定时器
LED1( 0 );
Delay_us(50000); // 50000 * 10us = 500ms
LED1( 1 );
LED2( 0 );
Delay_us(50000); // 50000 * 10us = 500ms
LED2( 1 );
LED3( 0 );
Delay_us(50000); // 50000 * 10us = 500ms
LED3( 1 );
//SysTick->CTRL = 0 << SYSTICK_ENABLE; // 失能滴答定时器
}
一旦我们调用了Delay_us() 函数,SysTick定时器就被开启,按照设定好的定时周期递减计数,SysTick的计数寄存器里面的值减为0时,就进入中断函数,当中断函数执行完毕之后由重新计时,如此循环,除非它被关闭。
Delay_us()函数实现如下:
/*
* 函数名:Delay_us
* 描述 :us延时程序,10us为一个单位
* 输入 :- nTime
* 输出 :无
* 调用 :Delay_us( 1 ) 则实现的延时为 1 * 10us = 10us
* :外部调用
*/
void Delay_us(__IO u32 nTime)
{
TimingDelay = nTime;
// 使能滴答定时器
SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk;
while(TimingDelay != 0);
}
使能了SysTick之后,就使用 while(TimingDelay != 0)语句等待TimingDelay变量变为0,这个变量是在中断服务函数中被修改的。
因此,我们需要编写相应的中断服务程序,在本实验室中我们配置为 10us 中断一次,每次中断把TimingDelay减1。中断程序在stm32f10x_it.c中实现:
/**
* @brief This function handles SysTick Handler.
* @param None
* @retval : None
*/
void SysTick_Handler(void)
{
TimingDelay_Decrement();
}
SysTick中断属于系统异常向量,在stm32f10x_it.c文件中已经默认有了它的中断服务函数SysTick_Handler(),但内容为空。我们在找到这个函数,在里面调用了用户函数TimingDelay_Decrement()。
TimingDelay_Decrement()是由用户编写的一个应用程序,在SysTick.c中实现:
/*
* 函数名:TimingDelay_Decrement
* 描述 :获取节拍程序
* 输入 :无
* 输出 :无
* 调用 :在 SysTick 中断函数 SysTick_Handler()调用
*/
void TimingDelay_Decrement(void)
{
if (TimingDelay != 0x00)
{
TimingDelay--;
}
}
每次进入SysTick中断就调用一次TimingDelay_Decrement() 函数,把全局变量TimingDelay自减一次。用户函数Delay_us () 在TimingDelay被减至 等于 0时,才退出延时循环,即我们对TimingDelay赋的值为要中断的次数。
所以总的延时时间T延时= T中断周期 * TimingDelay 。
至此,SysTick的精确延时功能讲解完毕。
6.3.7使用SysTick的测量时间的功能
稍微改变一下用法,我们就可以利用SysTick进行时间测量。
当我们开启SysTick定时器后,定时器开始工作,我们可以定义一个变量a来对中断次数进行记录,在定时器进入中断时,这个变量就a ++,当我们关闭定时器后,将变量的数值乘与定时器的中断周期 就等于测量时间。这个功能野火一般用于测量程序的运行时间,特别是涉及到算法的程序,这对于优化算法是有非常大的帮助。假如你的算法的是us级别的,那么SysTick就应该设定为us级中断,如果是ms级别的,就将SysTick设定为ms级中断。
6.3.8实验现象
将野火STM32开发板供电(DC5V),插上JLINK,将编译好的程序下载到开发板,即可看到板载的3个LED以500ms的频率闪烁。 |