一文看透 SysTick 定时器
在嵌入式 MCU 开发中,有这么一个 “神奇” 的模块,无论是新手工程师还是资深工程师,无论是裸机开发还是 RTOS 开发,无论是简单项目还是复杂项目,都会接触或使用到。
这,就是 SysTick 定时器,也叫做滴答定时器。甚至当你使用 ST 的 CubeMX 工具生成一个不进行任何配置的初始工程时,其默认也会使用 SysTick 作为 HAL 库的时钟心跳:
/**
* @brief This function handles System tick timer.
*/
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 */
}
为什么 SysTick 有如此魅力,其本质是什么,为什么各厂家的 MCU (Cortex-M 内核)都有各自不同的定时器外设,唯独 SysTick 却好像没有任何变化......
今天,我们就从其概念,到使用,来全面了解一下这个神奇的内核模块。
为什么要有 SysTick
Cortex-M 处理器内集成了一个小型的名为 SysTick 的定时器,它属于 NVIC 的一部分,且可以产生 SysTick 异常(异常类型 #15)。SysTick 为简单的向下计数的 24 位计数器,可以基于处理器时钟频率或参考时钟频率(通常为片上时钟源)来运行。
在现代操作系统中,通常需要一个周期性的中断来定期触发 OS 内核的调度,从而来进行任务管理和上下文切换。这也使得操作系统能在不同的时间片内运行不同的任务。处理器设计还需要确保运行在非特权等级的应用任务无法禁止该定时器,否则就有可能由于错误地禁用定时器而造成系统的锁死。
之所以在处理器内部要有一个定时器,主要还是为了提高软件的可移植性。正如开头所说,由于所有的 Cortex-M 处理器都存在一个相同的 SysTick 定时器,也就能够使原本在一个 Cortex-M3/M4 处理器上设计的操作系统能够复用到其他的 Cortex-M3/M4 处理器中。
即使你的程序不需要嵌入式操作系统,也可以将 SysTick 作为简单的定时器外设,来实现周期性的中断、延时或时间测量等功能。
SysTick 定时器操作
SysTick 定时器中存在 4 个寄存器。CMSIS-Core 头文件中定义了一个名为 SysTick 的结构体,简化了对这些寄存器的访问,如下表所示:

SysTick 内部包含了一个 24 位向下计数的计数器。它会根据处理器时钟或一个参考时钟信号(在 ARM Cortex-M3 或 Cortex-M4 计数参考手册中也被称作 STCLK)来递减计数值。关于参考时钟信号的具体实现取决于微控制器的设计,有些情况下它可能并不存在。并且由于上升沿检测的采样逻辑,参考时钟至少得比处理器时钟慢两倍。
在设置了控制和状态寄存器的第 0 位使能该计数器后,当前值寄存器中的数值会在每个处理器时钟周期或参考时钟的上升沿减小。若计数减至 0,它会从重装载寄存器中加载数值并继续运行。
另外一个寄存器为 SysTick 校准值寄存器。它为软件提供了校准信息。由于 CMSIS-Core 提供了一个名为 SystemCoreClock 变量(CMSIS 1.2 及之后版本可用, CMSIS 1.1 或之前的版本则使用变量 SystemFrequency),因此就未使用 SysTick 校准寄存器。系统初始化函数 SystemInit() 函数设置了该变量,而且每次系统时钟配置改变时都要对其进行更新。这种软件手段比利用 SysTick 校准值寄存器的硬件方式更为灵活。
SysTick 的简单框图如下:

SysTick 寄存器细节如下表所示:
SysTick 控制和状态寄存器(0xE000E010)

SysTick 重装载值寄存器(0xE000E014)

SysTick 当前值寄存器(0xE000E018)

SysTick 校准值寄存器(0xE000E01C)

使用 SysTick
如果只是想产生周期性的 SysTick 中断,最简单的方法就是使用 CMSIS-Core 中的函数 SysTick_Config :
uint32_t SysTick_Config(uint32_t ticks);
该函数将 SysTick 中断间隔设置为 ticks,使能计数器使用处理器时钟,然后设置 SysTick 异常为最低优先级。其实现代码如下:
__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 */
}
如果要在 30 MHz 的时钟频率下产生 1kHz 的 SysTick 异常,则可以使用:
SysTick_Config(SystemCoreClock / 1000);
变量 SystemCoreClock 应该存放正确的时钟频率数值,在此也就是 30 * 10^6,或者,也可以直接写成这样:
SysTick_Config(30000); // 30MHz / 1000 = 30000
此时 “SysTick_Handler(void)” 的触发频率就变成了 1kHz。
若 SysTick_Config 函数的输入参数无法适用到 24 位重装载数值寄存器(大于 0xFFFFFF),SysTick_Config 函数返回 1,否则返回 0。
在多数情况下,你可能会使用参考时钟或不想使用 SysTick 中断,此时则可以不使用 SysTick_Config 函数并可以选择直接配置 SysTick 寄存器,推荐遵循以下流程:
-
将 0 写入 SysTick->CTRL 寄存器来禁用 SysTick 定时器。这个操作是可选的,但还是推荐要进行这一步,因为如果你的代码中包含了重用部分(复制粘贴),那么 SysTick 可能已经被使能过了。
-
将新的重装载值写入 SysTick->LOAD,这个值应该为周期数减一。
-
将任意值写入 SysTick 当前值寄存器 SysTick->VAL,清零该寄存器。
-
配置 SysTick 控制和状态寄存器 SysTick->CTRL 来启动 SysTick 定时器。
由于 SysTick 定时器会向下计数到 0,因此,若要设置 SysTick 周期为 1000,你应该将重装载值(SysTick->LOAD)设置为 999。
若要在轮询模式中使用 SysTick 定时器,则可以使用 SysTick 控制和状态寄存器(SysTick->CTRL)中的计数标志来确定定时器何时变为 0。例如,可以将 SysTick 定时器设定为特定数值,然后等它变为 0,以此来实现延时。
SysTick->CTRL = 0; // 禁用 SysTick
SysTick->LOAD = 0xFF; // 计数范围为 255 到 0 (256 个周期)
SysTick->VAL = 0; // 清除当前计数值以及计数标志
SysTick->CTRL = 5; // 使能 SysTick 定时器并使用处理器时钟
while ((SysTick->CTRL & 0x00010000)==0);// 等待计数标志置位
SysTick->CTRL = 0; // 禁用 SysTick
若要将 SysTick 中断用作在一定时间后触发的单次操作,则可以将重装载值减少 12 个周期,以补偿中断等待时间(关于中断延迟可查阅这篇文章)。例如,要使 SysTick 中断服务函数在 300 个时钟周期后执行:
volatile int SysTickFired; // 全局标志位,标志 SysTickAlarm 被执行
...
SysTick->CTRL = 0; //禁用 SysTick
SysTick->LOAD = (300-12); // 设置重装载值,并减 12 来补偿异常延迟
SysTick->VAL = 0; // 清除当前计数值
SysTickFired = 0; // 清零标志位
SysTick->CTRL = 0x7; //使能 SysTick, SysTick 中断以及使用处理器时钟
while (SysTickFired == 0); // 等待中断服务函数将标志位置位
在单次的 SysTick 处理函数中,需要禁用 SysTick 来避免 SysTick 中断再次触发。并且我们可能还需要清除 SysTick 的挂起状态来避免由于处理逻辑太长而导致的挂起状态已经被再次置位了:
void SysTick_Handler(void) // SYSTICK 中断服务函数
{
SysTick->CTRL = 0x0; // 禁用 SysTick
...; // 处理逻辑
SCB->ICSR j= 1<<25; // 清除 SYSTICK 挂起位从而避免此时已经被再次挂起
SysTickFired++; // 更新标志位从而让主程序知道 SysTick 定时任务已经被执行
return;
}
若同时产生了另一个中断,则 SysTick 中断可能会延迟。
SysTick 定时器可以用于时间测量。例如,你可以使用下面的代码来测量一个简短函数的持续时间:
unsigned int start_time, stop_time, cycle_count;
SysTick->CTRL = 0; // 禁用 SysTick
SysTick->LOAD = 0xFFFFFFFF; // 设置 SysTick 重装载值为最大值
SysTick->VAL = 0; // 清空 SysTick 当前值
SysTick->CTRL = 0x5; // 使能 SysTick,选择处理器时钟
while(SysTick->VAL != 0); // 等待当前值更新为 0
start_time = SysTick->VAL; // 获取当前值
function(); // 调用函数
stop_time = SysTick->VAL; // 获取当前值
cycle_count = start_time - stop_time;
由于 SysTick 定时器是向下计数的,start_time 的数值会比 stop_time 要大。并且可能还需要在测量的最后检查一下 count_flag。若 count_flag 置位,那么实际测试的时间就是大于 0xFFFFFF 个时钟周期的。在这种情况下你需要使能 SysTick 中断并且在中断服务函数中计算 SysTick 计数器溢出的次数。最终的时间还需要计算这个溢出的时间。
SysTick 还提供了一个校准值寄存器。若这个信息是有效的,则 SysTick->CALIB 寄存器的最低 24 位表示要得到 10 ms SysTick 间隔所需的重装载值。不过,许多微控制器中并没有这个信息,这种情况下 TENMS 位域读出的值为 0。当然,上文中已经提到,CMSIS-Core 提供了一个表示频率信息的软件变量,这种方式更加灵活并且也得到了大多数微控制器供应商的支持。
SysTick 校准值寄存器的 Bit31 可以用于确定参考时钟是否存在。
注意点
最后,在使用 SysTick 定时器时需注意以下几点:
-
SysTick 定时器中的寄存器只能在特权状态下访问。
-
参考时钟在一些微控制器中可能并不存在。
-
若应用中存在嵌入式 OS,SysTick 一般会被 OS 使用,此时在通常情况下就不能再被应用任务使用了。
-
当处理器在调试期间暂停时,SysTick 定时器会停止计数。
-
根据微控制器的实际设计,SysTick 定时器可能在某些休眠模式中停止计数。
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐


所有评论(0)