×
嵌入式 > 技术百科 > 详情

零死角玩转stm32-初级篇之Sysstick(系统滴答定时器)

发布时间:2020-06-09 发布时间:
|

    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_LOADSTK_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 / 100000SystemCoreClock 为定义了系统时钟(SYSCLK)频率的宏,即等于AHB的时钟频率,本书的所有例程中AHB都是被配置为72MHz的,也就是这个SystemCoreClock 宏展开为数值7200 0000。

根据前面对SysTick_Config()函数的介绍,它的输入参数为SysTick将要计的脉冲数,经过ticks个脉冲(经过ticks个时钟周期)后将触发中断,触发中断后又重新开始计数。

由此我们可以算出定时的时间,下面为计算公式:

T=ticks*(1/f)

T 为要定时的总时间。

ticksSysTick_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的频率闪烁。


『本文转载自网络,版权归原作者所有,如有侵权请联系删除』

热门文章 更多
FPGA及CPLD应用领域不断拓展