13. SysTick—系统定时器

本章参考资料《dm00046982-stm32-cortexm4-mcus-and-mpus-programming-manual-stmicroelectronics》编程手册 4.5章节 SysTick timer (STK)章节。

因为SysTick是属于Cortex-M内核的外设,有关寄存器的定义和部分库函数都在core_cm4.h这个头文件中实现。 所以学习SysTick的时候可以参考这两个资料,一个是关于Systick外设说明文档,一个是外设操作接口源码。

13.1. SysTick简介

SysTick—系统定时器是属于Cortex-M内核中的一个外设,内嵌在NVIC中。 系统定时器是一个24bit的向下递减的计数器,计数器每计数一次的时间为1/SYSCLK, 一般我们设置系统时钟SYSCLK等于209M。当重装载数值寄存器的值递减到0的时候, 系统定时器就产生一次中断,以此循环往复。

因为SysTick是属于Cortex-M内核的外设,所有基于Cortex-M内核的单片机都具有这个系统定时器, 使得软件在Cortex-M单片机中可以很容易的移植。系统定时器一般用于操作系统,用于产生时基,维持操作系统的心跳。 HAL库中也使用到了Systick定时器,用于软件延时。

13.2. SysTick寄存器介绍

SysTick—系统定时有4个寄存器,简要介绍如下。在使用SysTick产生定时的时候, 只需要配置前三个寄存器,最后一个校准寄存器不需要使用。

表 12‑1 SysTick寄存器汇总

寄存器名称

寄存器描述

CTRL

SysTick控制及状态寄存器

LOAD

SysTick重装载数值寄存器

VAL

SysTick当前数值寄存器

CALIB

SysTick校准数值寄存器

表 12‑2 SysTick控制及状态寄存器

位段

名称

类型

复位值

描述

16

COUNTFLAG

R/W

0

如果在上次读取本寄存器 后, SysTick 已经计到 了 0,则该位为 1。

2

CLKSOURCE

R/W

0

时钟源选择位,0=外部 时钟,1=处理器时钟A HB

1

TICKINT

R/W

0

1=SysTick倒数 计数到 0时产生 SysTick异常请

求,0=数到 0 时无动作。也可以通过读 取COUNTFLAG标 志位来确定计数器是否递 减到0

0

ENABLE

R/W

0

SysTick 定时器的使能位

表 12‑3 SysTick 重装载数值寄存器

位段

名称

类型

复位值

描述

23:0

RELOAD

R/W

0

当倒数计数至零时,将被重装载的值

表 12‑4 SysTick当前数值寄存器

位段

名称

类型

复位值

描述

23:0

CURRENT

R/W

0

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

表 12‑5 SysTick校准数值寄存器

位段

名称

类型

复位值

描述

31

NOREF

R

0

指示是否有参考时钟提供 给处理器

0:提供参考时钟

1:不提供参考时钟

如果器件不提供参考时钟 ,SYST_CSR.C LKSOURCE标志位 为1,不可改写。

30

SKEW

R

1

S指示TENMS的值是 否精确

0:TENMS是精确值

1:TENMS不是精确 值或者不提供

不精确的TENMS值可 以影响作为软件实时时钟 节拍器的适用性。

23:0

TENMS

R

0

重新加载 10ms (100Hz) 计时的值, 受系统时钟偏差的错误。 如果值读取为零,

校准值未知。

系统定时器的校准数值寄存器在定时实验中不需要用到。有关各个位的描述这里引用手册里面的英文版本, 比较晦涩难懂,暂时不知道这个寄存器用来干什么。有研究过的朋友可以交流,起个抛砖引玉的作用。

13.3. STM32CubeIDE的SysTick初始化

在之前的实验中我们已经使用过了Systick定时器,那就是HAL_Dealy函数,HAL_Dealy的 延时机制就是依据Systick实现的,在STM32CubeIDE创建的工程中,默认已经为我们开启了 Systick定时器。接下来我们将分析整个Systick的初始化过程。

13.3.1. 代码分析

我们将根据程序上电的执行顺序一步一步解析Systick是如何被初始化的, main—> HAL_Init—> HAL_InitTick—> HAL_SYSTICK_Config—> SysTick_Config—> HAL_NVIC_SetPriority

13.3.1.1. HAL_Init函数

HAL_Init函数是main执行的第一个函数,详细代码如 HAL_Init函数 所示

stm32mp1xx_hal.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
 HAL_StatusTypeDef HAL_Init(void)
 {
     #if defined (CORE_CM4)
     HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);
     #endif

     SystemCoreClock = HAL_RCC_GetSystemCoreClockFreq();

     if(HAL_InitTick(TICK_INT_PRIORITY) != HAL_OK)
     {
         return HAL_ERROR;
     }

     HAL_MspInit();

     return HAL_OK;
 }
  • 第4行,设置中断优先级分组。中断优先级分组设置在整个代码中只需要设置一次即可, 在Systick中同样需要设置中断有优先级。

  • 第7行,获取系统的时钟,我们知道Systick定时器的初始化和系统时钟有着莫大的关系, 需要根据系统时钟设置初始化Systick定时器。

  • 第9行,调用HAL_InitTick初始化Systick定时器。

13.3.1.2. HAL_InitTick函数

HAL_InitTick函数传入的参数是TICK_INT_PRIORITY(值为0),用来设置中断的优先级,设置 为0表示设置为最高的中断优先级。HAL_InitTick函数详细代码如 HAL_InitTick函数 所示(省略与A7代码相关内容)

stm32mp1xx_hal.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
 __weak HAL_StatusTypeDef HAL_InitTick(uint32_t TickPriority)
 {
     #if defined (CORE_CM4)
     if ((uint32_t)uwTickFreq == 0U)
     {
         return HAL_ERROR;
     }

     if (HAL_SYSTICK_Config(SystemCoreClock /(1000U / uwTickFreq)) > 0U)
     {
         return HAL_ERROR;
     }

     if (TickPriority < (1UL << __NVIC_PRIO_BITS))
     {
         HAL_NVIC_SetPriority(SysTick_IRQn, TickPriority, 0U);
         uwTickPrio = TickPriority;
     }
     else
     {
         return HAL_ERROR;
     }
     #endif /* CORE_CM4 */

     return HAL_OK;
 }
  • 第4-7行,uwTickFreq变量用来表示设置Systick每秒钟中断的频率,在stm32mp1xx_hal.c中声明。当 设置每秒的中断频率为0显然是错的直接返回。

  • 第9-12行,配置Systick定时器,这个函数只是对SysTick_Config函数进行了分装, SysTick属于cortex-m内核的外设,STM32MP157有关的寄存器定义和库函数都在内核相关的库文件core_cm4.h中。 即SysTick_Config函数定义在core_cm4.h中,稍后进行讲解。

  • 第14-22行,设置中断优先级。首先对我们的传入的参数进行了出错判断。当设置的中断优先级大于16时,设置的中断 优先级是错误的。

当我们重新配置rcc时钟时,SystemCoreClock的值会重新更新,当调用HAL_RCC_OscConfig函数时, HAL_InitTick函数将会被调用重新配置Systick定时器。同时HAL_InitTick使用__weak作为修饰, 如果有必要我们可以重新编写HAL_InitTick函数。

13.3.1.3. SysTick_Config函数

HAL_SYSTICK_Config的内容很简单,就是调用SysTick_Config。接下来我们详细介绍下SysTick_Config函数,

core_cm4.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 __STATIC_INLINE uint32_t SysTick_Config(uint32_t ticks)
 {
     // 不可能的重装载值,超出范围
     if ((ticks - 1UL) > SysTick_LOAD_RELOAD_Msk) {
         return (1UL);
     }

     // 设置重装载寄存器
     SysTick->LOAD  = (uint32_t)(ticks - 1UL);

     // 设置中断优先级
     NVIC_SetPriority (SysTick_IRQn, (1UL << __NVIC_PRIO_BITS) - 1UL);

     // 设置当前数值寄存器
     SysTick->VAL   = 0UL;

     // 设置系统定时器的时钟源为AHBCLK
     // 使能系统定时器中断
     // 使能定时器
     SysTick->CTRL  = SysTick_CTRL_CLKSOURCE_Msk |
                     SysTick_CTRL_TICKINT_Msk   |
                     SysTick_CTRL_ENABLE_Msk;
     return (0UL);
 }

形参ticks用来设置重装载寄存器的值,最大不能超过重装载寄存器的值224, 当重装载寄存器的值递减到0的时候产生中断,然后重装载寄存器的值又重新装载往下递减计数, 以此循环往复。紧随其后设置好中断优先级,最后配置系统定时器的时钟为AHBCLK, 使能定时器和定时器中断,这样系统定时器就配置好了。

SysTick_Config()库函数主要配置了SysTick中的三个寄存器:LOAD、VAL和CTRL,有关具体的部分看代码注释即可。

在SysTick_Config()库中也设置中断优先级,这里是core_cm4.c文件的内容,为了库的完整性在正常的情况下我们不会去修改 任何库提供的函数内容,因此我们需要在此外的内容上额外设置一个可设置的Systick的中断优先级。

13.3.1.4. SysTick定时时间的计算

SysTick定时器的计数器是向下递减计数的,计数一次的时间为1/SystemCoreClock, 当重装载寄存器中的值VALUE的值减到0的时候,产生中断。可知中断一次的时间为 VALUE /SystemCoreClock,当我们将SystemCoreClock /(1000U / uwTickFreq)作为参数传递给SysTick_Config函数时, 可得到中断的时间为 uwTickFreq/1000,hal库中uwTickFreq的值设置为1,即每1ms将产生一次中断。

13.3.1.5. SysTick中断服务函数

当Systick产生中断之后将执行SysTick_Handler中断服务函数。

stm32mp1xx_it.c
1
2
3
4
 void SysTick_Handler(void)
 {
     HAL_IncTick();
 }

在Systick中断函数中调用了HAL_IncTick函数,HAL_IncTick函数 内容如下

stm32mp1xx_it.c
1
2
3
4
 __weak void HAL_IncTick(void)
 {
     uwTick += (uint32_t)uwTickFreq;
 }

HAL_IncTick函数中只对uwTick进行加法操作,uwTick是一个全局变量。可理解为用于计数系统的运行时间,单位为ms。 HAL_IncTick函数同样也是个使用__weak弱定义的函数,当我们需要使用Systick实现其他功能时可重新编写这个函数。

13.3.1.6. HAL_Delay函数的实现

stm32mp1xx_it.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
 __weak void HAL_Delay(uint32_t Delay)
 {
     uint32_t tickstart = HAL_GetTick();
     uint32_t wait = Delay;

     if (wait < HAL_MAX_DELAY)
     {
         wait += (uint32_t)(uwTickFreq);
     }

     while ((HAL_GetTick() - tickstart) < wait)
     {
     }
 }
  • 第3行,使用HAL_GetTick函数用来获取uwTick变量的值。

  • 第6-9行,添加频率以保证最小的等待时间,当我们的uwTickFreq设置为比较大时,如HAL_TICK_FREQ_100HZ(具体的数值为10), 意味着每10ms将产生一次Systick中断,而此时传入的参数Delay如果小于10,保证满足最小的等待时间。

  • 第12行,当前系统运行时间与函数刚进来的是的运行时间相差超过wait时,则跳出while循环。

在这里我们可以发现,HAL_Delay函数的延时并不是那么准确的,它的准确度与Systick的中断频率息息相关, 最大的误差为±一个Systick中断间隔。

同样HAL_Delay函数的定义是以__weak修饰的函数, 在有必要时可重新编写整个HAL_Delay函数。