ARM Cortex-M学习笔记:初识Systick定时器

描述

开发环境:

MDK:凯尔 5.30

STM32立方体MX:V6.4.0

单片机:STM32F103ZET6

Cortex-M的内核中包含Systick定时器了,只要是Cortex-M系列的MCU就会有Systick,因此这是通用的,下面详细分析。

4.1 Systick工作原理分析

SysTick 定时器被捆绑在 NVIC 中,用于产生 SysTick 异常(异常号:15)。 在以前,操作系统和所有使用了时基的系统都必须有一个硬件定时器来产生需要的“滴答”中断,作为整个系统的时基。 滴答中断对操作系统尤其重要。 例如,操作系统可以为多个任务分配不同数目的时间片,确保没有一个任务能霸占系统; 或者将每个定时器周期的某个时间范围赐予特定的任务等,操作系统提供的各种定时功能都与这个滴答定时器有关。 因此,**需要一个定时器来产生周期性的中断,而且最好还让用户程序不能随意访问它的寄存器,以维持操作系统“心跳”的节律。 **

mcu

Cortex-M3 在内核部分包含了一个简单的定时器——SysTick。 因为所有的 CM3 芯片都带有这个定时器,软件在不同芯片生产厂商的 CM3 器件间的移植工作就得以简化。 该定时器的时钟源可以是内部时钟(FCLK,CM3 上的自由运行时钟),或者是外部时钟( CM3 处理器上的 STCLK 信号)。 不过,STCLK 的具体来源则由芯片设计者决定,因此不同产品之间的时钟频率可能大不相同。 因此,需要阅读芯片的使用手册来确定选择什么作为时钟源。 在 STM32 中 SysTick 以 HCLK(AHB 时钟)或 HCLK/8 作为运行时钟,见上图。

SysTick 定时器能产生中断,CM3 为它专门开出一个异常类型,并且在向量表中有它的一席之地。 它使操作系统和其他系统软件在 CM3 器件间的移植变得简单多了,因为在所有 CM3 产品间,SysTick 的处理方式都是相同的。 SysTick 定时器除了能服务于操作系统之外,还能用于其他目的,如作为一个闹铃、用于测量时间等。 **Systick 定时器属于Cortex ** 内核部件 ,可以参考《ARM Cortex-M3 权威指南》((英)JosephYiu 著,宋岩译,北京航空航天大学出版社出版)或“STM32xxx-Cortex-M3programmingmanual” (这是 ST 官方提供的电子版编程手册,可以在 ST 官网下载)来了解。

4.2 Systick寄存器分析

在传统的嵌入式系统软件按中通常实现 Delay(N) 函数的方法为:

for(i = 0; i <= x; i ++);
x --- ;

对于STM32系列微处理器来说,执行一条指令只有几十个 ns,进行 for 循环时,要实现 N 毫秒的 x 值非常大,而且由于系统频率的宽广,很难计算出延时 N 毫秒的精确值。 针对 STM32 微处理器,需要重新设计一个新的方法去实现该功能,以实现在程序中使用 Delay(N)。

Cortex-M3 的内核中包含一个 SysTick 时钟。 SysTick 为一个 24 位递减计数器,SysTick 设定初值并使能后,每经过 1 个系统时钟周期,计数值就减 1。 计数到 0 时,SysTick 计数器自动重装初值并继续计数,同时内部的 COUNTFLAG 标志会置位,触发中断 (如果中断使能情况下)。

在 STM32 的应用中,使用 Cortex-M3 内核的 SysTick 作为定时时钟,设定每一毫秒产生一次中断,在中断处理函数里对 N 减一,在Delay(N) 函数中循环检测 N 是否为 0,不为 0 则进行循环等待; 若为 0 则关闭 SysTick 时钟,退出函数。

注:全局变量 TimingDelay , 必须定义为 volatile 类型 , 延迟时间将不随系统时钟频率改变。

STM32中的Systick 部分内容属于NVIC控制部分,一共有4个寄存器,名称和地址分别是:

  • STK_CTRL, 0xE000E010 -- 控制寄存器

mcu

第0位:ENABLE,Systick 使能位

(0:关闭Systick功能; 1:开启Systick功能)

第1位:TICKINT,Systick 中断使能位

(0:关闭Systick中断; 1:开启Systick中断)

第2位:CLKSOURCE,Systick时钟源选择

(0:使用HCLK/8 作为Systick时钟; 1:使用HCLK作为Systick时钟)

第16位:COUNTFLAG,Systick计数比较标志,如果在上次读取本寄存器后,SysTick 已经数到了0,则该位为1。 如果读取该位,该位将自动清零

  • STK_LOAD, 0xE000E014 -- 重载寄存器

mcu

Systick是一个递减的定时器,当定时器递减至0时,重载寄存器中的值就会被重装载,继续开始递减。 STK_LOAD 重载寄存器是个24位的寄存器最大计数0xFFFFFF。

  • STK_VAL, 0xE000E018 -- 当前值寄存器

mcu

也是个24位的寄存器,读取时返回当前倒计数的值,写它则使之清零,同时还会清除在SysTick 控制及状态寄存器中的COUNTFLAG 标志。

  • STK_CALRB, 0xE000E01C -- 校准值寄存器

mcu

校准值寄存器提供了这样一个解决方案:它使系统即使在不同的CM3产品上运行,也能产生恒定的SysTick中断频率。 最简单的作法就是:直接把TENMS的值写入重装载寄存器,这样一来,只要没突破系统极限,就能做到每10ms来一次 SysTick异常。 如果需要其它的SysTick异常周期,则可以根据TENMS的值加以比例计算。 只不过,在少数情况下, CM3芯片可能无法准确地提供TENMS的值(如, CM3的校准输入信号被拉低),所以为保险起见,最好在使用TENMS前检查器件的参考手册。

SysTick定时器除了能服务于操作系统之外,还能用于其它目的:如作为一个闹铃,用于测量时间等。 要注意的是,当处理器在调试期间被喊停( halt)时,则SysTick定时器亦将暂停运作。

4.3 Systick定时器实现-标准库

4.3.1main文件分析

主函数如下:

int main(void)
{      
       /* LED 端口初始化 */
       LED_GPIO_Config();

       /* 配置SysTick 为10us中断一次 */
       SysTick_Init();

       for(;;)
       {

           LED1( ON ); 
           Delay_us(10000);        //10000 * 10us = 100ms
           //Delay_ms(100);
           LED1( OFF );

           LED2( ON );
           Delay_us(10000);             // 10000 * 10us = 100ms
           //Delay_ms(100);
           LED2( OFF );

           LED3( ON );
           Delay_us(10000);             // 10000 * 10us = 100ms
           //Delay_ms(100);
           LED3( OFF ); 
       }     
}

在 main 函数中,SysTick_Init() 和 Delay_us() 这两个函数比较陌生,它们的功能分别是配置好 SysTick 定时器和进行精确延时。 整个 main 函数的流程就是初始化 LED 及SysTick 定时器之后,就进入死循环,轮流点亮 LED1、LED2、LED3,点亮的时间为精确的 100 ms。

4.3.2 stm32f103_SysTick.c文件分析

  • 配置并启动SysTick

我们看一下 SysTick_Init() 这个函数,其功能是启动系统滴答定时器 SysTick ,并将 SysTick 配置为 10μs中断一次。

void SysTick_Init(void)
{
       /*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 外设固件库文件中找到其使用方法。 所以我们在 Keil 环境下直接跟踪这个函数到 core_cm3.h 文件,查看函数的定义。

static __INLINE uint32_tSysTick_Config(uint32_t ticks)
{ 
 if (ticks > SysTick_LOAD_RELOAD_Msk) return (1);            
/* Reload value impossible */                                                         
 SysTick->LOAD  = (ticks &SysTick_LOAD_RELOAD_Msk) - 1;      /* setreload 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 SysTickIRQ and SysTick Timer */
 return (0);           /* Functionsuccessful */
}

在这个函数定义的前面有关于它的注释,如果我们不想去研究它的具体实现,可以根据这段注释了解函数的功能:这个函数启动了 SysTick ; 并把它配置为计数至 0 时引起中断; 输入的参数 ticks 为两个中断之间的脉冲数,即相隔 ticks 个时钟周期会引起一次中断; 配置 SysTick 成功时返回 0,出错时返回 1。 但是,这段注释并没有告诉我们它把 SysTick 的时钟设置为 AHB 时钟还是 AHB/8,这是一个十分关键的问题,于是,我们将对这个函数的具体实现进行分析,与大家再分享一下如何分析底层库函数。 分析底层库函数,要有 SysTick 定时器工作分析的知识准备。

  • 检查输入参数

SysTick_Config() 第 3 行代码是检查输入参数 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 <

#define SysTick_CTRL_CLKSOURCE_Pos          2                                            /*!< SysTick CTRL: CLKSOURCE Position */
#define SysTick_CTRL_CLKSOURCE_Msk         (1ul <

#define SysTick_CTRL_TICKINT_Pos            1                                            /*!< SysTick CTRL: TICKINT Position */
#define SysTick_CTRL_TICKINT_Msk           (1ul <

#define SysTick_CTRL_ENABLE_Pos             0                                            /*!< SysTick CTRL: ENABLE Position */
#define SysTick_CTRL_ENABLE_Msk            (1ul <

/* 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 Calibration Register Definitions */
#define SysTick_CALIB_NOREF_Pos            31                                            /*!< SysTick CALIB: NOREF Position */
#define SysTick_CALIB_NOREF_Msk            (1ul <

#define SysTick_CALIB_SKEW_Pos             30                                            /*!< SysTick CALIB: SKEW Position */
#define SysTick_CALIB_SKEW_Msk             (1ul <

#define SysTick_CALIB_TENMS_Pos             0                                            /*!< SysTick CALIB: TENMS Position */
#define SysTick_CALIB_TENMS_Msk            (0xFFFFFFul <
/*@}*/ /* 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 <

配置中断向量及重置 STK_VAL 寄存器回到 SysTick_Config()函数,接下来调用了 NVIC_SetPriority () 函数并配置了 SysTick中断,这就是为什么我们在外部没有再使用 NVIC 配置 SysTick 中断的原因。 配置好SysTick 中断后把 STK_VAL 寄存器重新赋值为 0(在使能 SysTick 时,硬件会把存储在STK_LOAD 寄存器中的 ticks 值加载给它)。配置 SysTick 时钟为 AHB在这段代码最后,向 STK_CTRL 寄存器写入了 SysTick 的控制参数,配置为使用AHB 时钟,使能计数至 0 时引起中断,使能 SysTick。 执行了这行代码,SysTick 就开始运行并进行脉冲计数了。若读者想要使用 AHB/8 作为时钟,可以调用库函数SysTick_CLKSourceConfig() 进行修改,也可以直接对 SysTick_Config() 函数的代码进行修改。使能、关闭定时器由于调用 SysTick_Config()函数之后,SysTick 定时器就被开启了,但我们在初始化的时候并不希望这样,而是根据需要再开启。 所以在 SysTick_Init() 函数中,调用完SysTick_Confi g()并配置好后,应先把定时器关闭了。 SysTick 的开启和关闭由寄存器STK_CTRL 的 Bit0 :ENABLE 位来控制,使用位屏蔽宏以操作寄存器的方式实现。SysTick->CTRL |=SysTick_CTRL_ENABLE_Msk; // 使能滴答定时器SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk; // 关闭滴答定时器定时时间的计算在调用SysTick_Config()函数时,向它输入的参数为SystemCoreClock /100000,SystemCoreClock为定义了系统时钟(SYSCLK)频率的宏,即等于 AHB的时钟频率。 在本书的所有例程中AHB 都是被配置为 72 MHz 的,也就是这个 SystemCoreClock 宏展开为数值 7200 0000。根据前面对 SysTick_Config()函数的介绍,它的输入参数为 SysTick 将要计时的脉冲数,经过 ticks 个脉冲(经过 ticks 个时钟周期)后将触发中断,触发中断后又重新开始计数。 由此我们可以算出定时的时间,下面为计算公式:T=即时报价×(1/f)其中,T 为要定时的总时间; ticks 为 SysTick_Config() 的输入参数; 1/ f 即为SysTick 使用的时钟源的时钟周期,f 为该时钟源的时钟频率,当时钟源确定后为常数。例如:本实验例子中,使用时钟源为 AHB 时钟,其频率被配置为 72 MHz。 调用函数时,把 ticks 赋值为 ticks=SystemFrequency/ 100000 =720,表示 720 个时钟周期中断一次; 1/f 是时钟周期的时间,此时(1/f =1/72 μs),所以最终定时总时间 T=720×(1/72),为720 个时钟周期,正好是 10 μs。SysTick 定时器的定时时间(配置为触发中断,即为中断周期)由 ticks 参数决定,最大定时周期不能超过 2^24^ 个。编写中断服务函数一旦我们调用了 Delay_us() 函数,SysTick 定时器就被开启,按照设定好的定时周期递减计数,当 SysTick 的计数寄存器的值减为 0 时,就进入中断函数,当中断函数执行完毕之后重新计时,如此循环,除非它被关闭。void Delay_us(__IO u32 nTime){ TimingDelay = nTime; // 使能滴答定时器 SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk; while(TimingDelay != 0);}使能了 SysTick 之后,就使用 while(TimingDelay != 0)语句等待 TimingDelay 变量变为 0,这个变量是在中断服务函数中被修改的。 因此,我们需要编写相应的中断服务程序,在本实验室中我们配置为 10μs 中断一次,每次中断把 TimingDelay 减 1。 中断程序在 stm32f10x_it.c 中实现。void SysTick_Handler(void){ TimingDelay_Decrement(); }SysTick中断属于系统异常向量,在stm32f10x_it.c文件中已经默认有了它的中断服务函数SysTick_Handler(),但内容为空。 我们找到这个函数,其调用了用户函数TimingDelay_Decrement()。 后者是由用户编写的一个应用程序。void TimingDelay_Decrement(void){ if (TimingDelay != 0x00) { TimingDelay--; }}每次进入 SysTick 中断就调用一次TimingDelay_Decrement()函数,使全局变量TimingDelay 自减一次。 用户函数 Delay_us ()在TimingDelay 被减至0时,才退出延时循环,即我们对 TimingDelay 赋的值为要中断的次数。 所以总的延时时间:T 延时 = T 中断周期 x TimingDelay至此,SysTick 的精确延时功能讲解完毕。4.4 Systick定时器实现-HAL库4.4.1 STM32Cube配置工程关于如何使用STM32Cube新建工程在前文已经讲解过了,这里直说配置GPIO部分内容。 本文要实现流水灯,其实输出为初始化设置为高电平还是低电平都可以,因为流水灯需要不断反转。 在上一节笔者已经讲过了。1.GPIO配置我们将PB0、PG6、PG7配置输出模式(高电平、低电平均可)、输出速率、上/下拉等,默认即可。2.时钟源配置3.时钟配置4.sys配置(滴答定时器配置)以上配置和GPIO流水灯是一样的,本文只具体讲解Systick的内容。4.4.2 Systick定时器具体代码分析Systick属于内核部分,相关的寄存器定义与库函数都在内核相关的文件core_cm3.h中,在上标准库函数版本中已经分析过了。 那么HAL库函数是如何初始化Systick的呢? 在HAL_Init()函数中调用了HAL_InitTick()函数,这才是Systick初始化入口。__weak HAL_StatusTypeDefHAL_InitTick(uint32_t TickPriority){ /* Configure the SysTick to have interrupt in 1ms time basis*/ if (HAL_SYSTICK_Config(SystemCoreClock / (1000U / uwTickFreq)) > 0U) { return HAL_ERROR; } /* Configure the SysTick IRQ priority */ if (TickPriority < (1UL << __NVIC_PRIO_BITS)) { HAL_NVIC_SetPriority(SysTick_IRQn,TickPriority, 0U); uwTickPrio = TickPriority; } else { return HAL_ERROR; } /* Return function status */ return HAL_OK;}HAL_SYSTICK_Config()函数和标准库函数差不多,默认中断周期是1ms,HAL_TICK_FREQ_DEFAULT是一个宏定义表示计数的频率,默认是1,也就是1KHz,也就是1/1000,那么中断一次的时间为72000000/1000/1*(1/72000000)=1ms。 那么我们要延时1s怎么做呢。 我们在上一节流水灯使用了HAL_Delay()函数,函数原型如下。__weak void HAL_Delay(uint32_tDelay){ uint32_t tickstart = HAL_GetTick(); uint32_t wait = Delay; /* Add a freq to guarantee minimum wait */ if (wait < HAL_MAX_DELAY) { wait += (uint32_t)(uwTickFreq); } while ((HAL_GetTick() - tickstart) < wait) { }}在函数中HAL_Delay(),(HAL_GetTick() -tickstart) < wait用于延时的中断周期数,在Systick初始化函数中,中断间隔为1ms,HAL_Delay ()函数的传入参数Delay表示多少个中断周期,也就是我们最终的延时,我们传入Delay = 500,那么最终的延时就是500ms。我们再来看看HAL_GetTick()函数。__weak uint32_tHAL_GetTick(void){ return uwTick;}HAL_GetTick()函数很简单,不断获取uwTick得值,这是一个全局变量,可以发现在HAL_IncTick()函数中使用过。 那么HAL_IncTick()函数被那个函数调用了呢?__weak void HAL_IncTick(void){ uwTick += uwTickFreq;}不难发现,在stm32f1xx_it.c中间中的SysTick_Handler()函数中调用了HAL_IncTick()函数,SysTick_Handler()也就是滴答定时器的中断服务函数,也就是中断一次会调用一次,也就会uwTick变量累加一次,最终uwTick累加到Delay次,表示此次延时结束。void SysTick_Handler(void){ /* USER CODE BEGIN SysTick_IRQn 0 */ /* USER CODE END SysTick_IRQn 0 */ HAL_IncTick(); /* USER CODE BEGIN SysTick_IRQn 1 */ /* USER CODE END SysTick_IRQn 1 */}好了,使用STM32Cube配置SysTick定时器的延时就讲解完成了,在主函数是使用延时函数控制LED就是流水灯了。int main(void){ /* USER CODE BEGIN 1 */ /* USER CODE END 1 */ /* MCUConfiguration--------------------------------------------------------*/ /* Reset of all peripherals, Initializes the Flash interface and theSystick. */ HAL_Init(); /* USER CODE BEGIN Init */ /* USER CODE END Init */ /* Configure the system clock */ SystemClock_Config(); /* USER CODE BEGIN SysInit */ /* USER CODE END SysInit */ /* Initialize all configured peripherals */ MX_GPIO_Init(); /* USER CODE BEGIN 2 */ /* USER CODE END 2 */ /* Infinite loop */ /* USER CODE BEGIN WHILE */ while (1) { /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ HAL_GPIO_TogglePin(GPIOB,GPIO_PIN_0); HAL_Delay(500); HAL_GPIO_TogglePin(GPIOB,GPIO_PIN_0); HAL_GPIO_TogglePin(GPIOG,GPIO_PIN_6); HAL_Delay(500); HAL_GPIO_TogglePin(GPIOG,GPIO_PIN_6); HAL_GPIO_TogglePin(GPIOG,GPIO_PIN_7); HAL_Delay(500); HAL_GPIO_TogglePin(GPIOG,GPIO_PIN_7); } /* USER CODE END 3 */}4.5实验现象将编译好的程序下载到板子中,可以看到三个LED灯不同地闪烁。

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分