实验五 SysTick— 系统定时器

一。实验目标

1. 通过编程 systick 实现精准的延时

2. 通过本实验掌握 Keil 5 新建工程、使用 STM32 库函数编写应用程序、将程序下载至实验板的步骤、方法;

3. 通过本实验掌握 STM32 的 systick 精准延时实现。

二。知识储备及设计思路

SysTick 简介

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

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

SysTick 寄存器介绍

SysTick— 系统定时有 4 个寄存器,简要介绍如下。在使用 SysTick 产生定时的时候,

只需要配置前三个寄存器,最后一个校准寄存器不需要使用。

img

再来看看怎样通过使用 SysTick 定时器来实现延时。

延时包含了 delay.c 和 delay.h 两个文件,这两个文件用来实现系统的延时功能,其中包含 7 个函数,其中有 4 个是支持操作系统,在这里我们不研究这 4 个函数,只看需要用到的 3 个函数:

1
2
3
void delay_init(uint8_t SYSCLK);
void delay_ms(uint16_t nms);
void delay_us(uint32_t nus);

在介绍这些函数之前,我们先了解一下编程思想:CM4 内核的处理和 CM3 一样,内部都包含了一个 SysTick 定时器,SysTick 是一个 24 位的倒计数定时器,当计到 0 时,将从 RELOAD 寄存器中自动重装载定时初值。只要不把它在 SysTick 控制及状态寄存器中的使能位清除,就永不停息。SysTick 在《STM32xx 中文参考手册》里面基本没有介绍,其详细介绍,请参阅《STM32F3 与 F4 系列 Cortex M4 内核编程手册》第 230 页。我们就是利用 STM32 的内部 SysTick 来实现延时的,这样既不占用中断,也不占用系统定时器。

这里我们将介绍的是新版本的延时函数,延时函数支持在任意操作系统(OS)下面使用,它可以和操作系统共用 SysTick 定时器。

这里,我们以 UCOSII 为例,介绍如何实现操作系统和我们的 delay 函数共用 SysTick 定时器。首先,我们简单介绍下 UCOSII 的时钟:ucos 运行需要一个系统时钟节拍(类似 “心跳”),而这个节拍是固定的(由 OS_TICKS_PER_SEC 宏定义设置),比如要求 5ms 一次(即可设置:OS_TICKS_PER_SEC=200),在 STM32 上面,一般是由 SysTick 来提供这个节拍,也就是 SysTick 要设置为 5ms 中断一次,为 ucos 提供时钟节拍,而且这个时钟一般是不能被打断的(否则就不准了)。

因为在 ucos 下 systick 不能再被随意更改,如果我们还想利用 systick 来做 delay_us 或者 delay_ms 的延时,就必须想点办法了,这里我们利用的是时钟摘取法。以 delay_us 为例,比如 delay_us(50),在刚进入 delay_us 的时候先计算好这段延时需要等待的 systick 计数次数,这里为 50168(假设系统时钟为 168Mhz,因为我们设置 systick 的频率为系统时钟频率,那么 systick 每增加 1,就是 1/168us),然后我们就一直统计 systick 的计数变化,直到这个值变化了 50168,一旦检测到变化达到或者超过这个值,就说明延时 50us 时间到了。这样,我们只是抓取 SysTick 计数器的变化,并不需要修改 SysTick 的任何状态,完全不影响 SysTick 作为 UCOS 时钟节拍的功能,这就是实现 delay 和操作系统共用 SysTick 定时器的原理。

下面我们开始介绍这几个函数。

三。程序设计

我们不使用操作系统,只看需要用到的三个函数,当需要用到操作系统时,请自行阅读代码。

delay_init 函数

该函数用来初始化 2 个重要参数:fac_us 以及 fac_ms;同时把 SysTick 的时钟源选择为外部时钟。具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//初始化延迟函数
//当使用ucos的时候,此函数会初始化ucos的时钟节拍
//SYSTICK的时钟固定为AHB时钟
//SYSCLK:系统时钟频率
void delay_init(uint8_t SYSCLK)
{
#if SYSTEM_SUPPORT_OS
//如果需要支持OS. uint32_t reload;
#endif
HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK);//SysTick频率为HCLK
fac_us=SYSCLK; //不论是否使用OS,fac_us都需要使用
#if SYSTEM_SUPPORT_OS //如果需要支持OS.
reload=SYSCLK; //每秒钟的计数次数 单位为K
reload*=1000000/delay_ostickspersec; //根据delay_ostickspersec设定溢出时间 //reload为24位寄存器,最大值:16777216,在180M下,约合0.745s左右
fac_ms=1000/delay_ostickspersec; //代表OS可以延时的最少单位
SysTick->CTRL|=SysTick_CTRL_TICKINT_Msk;//开启SYSTICK中断
SysTick->LOAD=reload; //每1/OS_TICKS_PER_SEC秒中断一次
SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk; //开启SYSTICK
#else
#endif
}

可以看到,delay_init 函数使用了条件编译,来选择不同的初始化过程,如果不使用 OS 的时候,只是设置一下 SysTick 的时钟源以及确定 fac_us 值。而如果使用 OS 的时候,则会进行一些不同的配置,这里的条件编译是根据 SYSTEM_SUPPORT_OS 这个宏来确定的。

SysTick 是 MDK 定义了的一个结构体(在 core_m4.h 里面),里面包含 CTRL、LOAD、VAL、CALIB 等 4 个寄存器,

SysTick->CTRL 的各位定义如图 5.1 所示:

img

图 5.1 SysTick->CTRL 寄存器各位定义

SysTick-> LOAD 的定义如图 5. 2 所示:

img

图 5. 2 SysTick->LOAD 寄存器各位定义

SysTick-> VAL 的定义如图 5. 3 所示:

img

图 5. 3 SysTick->VAL 寄存器各位定义

SysTick-> CALIB 不常用,在这里我们也用不到,故不介绍了。

1
SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK);

这句代码把 SysTick 的时钟选择为内核时钟,这里需要注意的是:SysTick 的时钟源自 HCLK,假设我们外部晶振为 25M,然后倍频到 180MHZ,那么 SysTick 的时钟即为 180Mhz,也就是 SysTick 的计数器 VAL 每减 1,就代表时间过了 1/180us。所以 fac_us=SYSCLK; 这句话就是计算在 SYSCLK 时钟频率下延时 1us 需要多少个 SysTick 时钟周期。

在不使用 OS 的时候:fac_us,为 us 延时的基数,也就是延时 1us,Systick 定时器需要走过的时钟周期数。当使用 OS 的时候,fac_us,还是 us 延时的基数,不过这个值不会被写到 SysTick->LOAD 寄存器来实现延时,而是通过时钟摘取的办法实现的(前面已经介绍了)。而 fac_ms 则代表 ucos 自带的延时函数所能实现的最小延时时间(如 delay_ostickspersec=200,那么 fac_ms 就是 5ms)。

delay_us 函数

该函数用来延时指定的 us,其参数 nus 为要延时的微秒数。该函数有使用 OS 和不使用 OS 两个版本,这里我们介绍不使用 OS 的时候,实现函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//延时nus//nus为要延时的us数.
//nus:0~190887435(最大值即2^32/fac_us@fac_us=22.5)
void delay_us(uint32_t nus)
{
uint32_t ticks;
uint32_t told,tnow,tcnt=0;
uint32_t reload=SysTick->LOAD; //LOAD的值
ticks=nus*fac_us; //需要的节拍数
told=SysTick->VAL; //刚进入时的计数器值
while(1)
{
tnow=SysTick->VAL;
if(tnow!=told)
{
if(tnow<told)tcnt+=told-tnow; //这里注意一下SYSTICK是一个递减的计数器就可以了.
else tcnt+=reload-tnow+told;
told=tnow;
if(tcnt>=ticks)break; //时间超过/等于要延迟的时间,则退出.
}
};
}

这里就正是利用了我们前面提到的时钟摘取法,ticks 是延时 nus 需要等待的 SysTick 计数次数(也就是延时时间),told 用于记录最近一次的 SysTick->VAL 值,然后 tnow 则是当前的 SysTick->VAL 值,通过他们的对比累加,实现 SysTick 计数次数的统计,统计值存放在 tcnt 里面,然后通过对比 tcnt 和 ticks,来判断延时是否到达,从而达到不修改 SysTick 实现 nus 的延时。对于使用 OS 的时候,delay_us 的实现函数和不使用 OS 的时候方法类似,都是使用的时钟摘取法,只不过使用 delay_osschedlock 和 delay_osschedunlock 两个函数,用于调度上锁和解锁,这是为了防止 OS 在 delay_us 的时候打断延时,可能导致的延时不准,所以我们利用这两个函数来实现免打断,从而保证延时精度。

delay_ms 函数

该函数是用来延时指定的 ms 的,其参数 nms 为要延时的毫秒数。该函数有使用 OS 和不使用 OS 两个版本,这里我们分别介绍,首先是不使用 OS 的时候,实现函数如下:

1
2
3
4
5
6
7
//延时nms
//nms:要延时的ms数
void delay_ms(uint16_t nms)
{
uint32_t i;
for(i=0;i<nms;i++) delay_us(1000);
}

该函数其实就是多次调用前面所讲的 delay_us 函数,来实现毫秒级延时的。

HAL 库延时函数 HAL_Delay 解析

前面我们讲解了提供的使用 Systick 实现延时相关函数。实际上,HAL 库有提供延时函数,只不过它只能实现简单的毫秒级别延时,没有实现 us 级别延时。下面我们列出 HAL 库实现延时相关的函数。首先是功能配置函数:

// 调用 HAL_SYSTICK_Config 函数配置每隔 1ms 中断一次:文件 stm32f4xx_hal.c 中定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
__weak HAL_StatusTypeDef HAL_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 配置函数:文件 stm32f4xx_hal_context.c 中定义

1
2
3
4
uint32_t HAL_SYSTICK_Config(uint32_t TicksNumb)
{
return SysTick_Config(TicksNumb);
}

// 内核的 Systick 配置函数,配置每隔 ticks 个 systick 周期中断一次 // 文件 core_cm4.h 中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
STATIC_INLINE uint32_t SysTick_Config(uint32_t ticks)
{
if ((ticks - 1UL) > SysTick_LOAD_RELOAD_Msk)
{
return (1UL); /* Reload value impossible */
}
SysTick->LOAD = (uint32_t)(ticks - 1UL); /* set reload register */
NVIC_SetPriority (SysTick_IRQn, (1UL << __NVIC_PRIO_BITS) - 1UL); /* set Priority for Systick Interrupt */
SysTick->VAL = 0UL; /* 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 (0UL); /* Function successful */
}

上面三个函数,实际上开放给 HAL 调用的主要是 HAL_InitTick 函数,该函数在 HAL 库初始化函数 HAL_Init 中会被调用。该函数通过间接调用 SysTick_Config 函数配置 Systick 定时器每隔 1ms 中断一次,永不停歇。

接下来我们来看看延时的逻辑控制代码:

//Systick 中断服务函数:文件 stm32f4xx_it.c 中

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

// 下面代码均在文件 stm32f4xx_hal.c 中

1
2
3
4
5
6
static __IO uint32_t uwTick; //定义计数全局变量

__weak void HAL_IncTick(void)
{
uwTick += uwTickFreq;
}
1
2
3
4
__weak uint32_t HAL_GetTick(void)
{
return uwTick;
}

// 开放的 HAL 延时函数,延时 Delay 毫秒

1
2
3
4
5
6
7
8
9
10
11
12
13
__weak void HAL_Delay(uint32_t Delay)
{
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 库实现延时功能非常简单,首先定义了一个 32 位全局变量 uwTick,在 Systick 中断服务函数 SysTick_Handler 中通过调用 HAL_IncTick 实现 uwTick 值不断增加,也就是每隔 1ms 增加 1。而 HAL_Delay 函数在进入函数之后先记录当前 uwTick 的值,然后不断在循环中读取 uwTick 当前值,进行减运算,得出的就是延时的毫秒数,整个逻辑非常简单也非常清晰。

但是,HAL 库的延时函数有一个局限性,在中断服务函数中使用 HAL_Delay 会引起混乱,因为它是通过中断方式实现,而 Systick 的中断优先级是最低的,所以在中断中运行 HAL_Delay 会导致延时出现严重误差。所以一般情况下,推荐大家使用我们提供的延时函数库。

四。实验结果

将编译好的代码下载到实验板中,按下复位键,流水灯会间隔 1s 闪烁。

五。思考与拓展

本实验通过使用 systick 实现延时,还有没其他精确延时方式?和怎么实现?