11.SysTick 详解:从原理到实战,掌握 Cortex-M 系统嘀嗒定时器
摘要:SysTick 是 ARM Cortex-M 内核内置的 24 位向下计数定时器,具有可产生固定周期中断、跨芯片通用等特点,常用于 RTOS 时钟节拍、精确延时和时间测量。本文从工作原理、寄存器详解、配置步骤入手,结合 STM32F103 给出寄存器直操作与 CMSIS 标准函数两种实战代码,并梳理典型应用场景与常见问题排查,帮助读者快速掌握 SysTick 的使用方法。
关键词:SysTick、Cortex-M、定时器、延时、RTOS
一、SysTick 概述
SysTick(System Tick,系统嘀嗒定时器)是 ARM Cortex-M 内核中内置的一个简单 24 位向下计数定时器,属于 NVIC(嵌套向量中断控制器)的一部分。它独立于芯片厂商的外设定时器,所有基于 Cortex-M 内核的 MCU(如 STM32、GD32(兆易创新)、NRF52 等)都具备这个外设。
核心特点:24 位递减计数器、可产生固定周期中断、内核级外设(跨芯片通用)、通常用作操作系统时基或精确延时。
主要用途
-
操作系统时基:FreeRTOS、RT-Thread、uC/OS 等 RTOS 的时钟节拍(Tick)通常由 SysTick 提供,典型节拍为 1ms。
-
精确延时:裸机程序中利用 SysTick 实现微秒/毫秒级延时函数。
-
时间测量:作为简单的时间基准,测量代码执行时间。
-
轮询调度:在非 OS 环境下提供周期性任务触发。
二、工作原理
SysTick 的计数流程可以概括为「装载初值、递减计数、归零重装」三个环节,其完整循环如下图所示:
flowchart TD
A[向 LOAD 写入初值 N] --> B[使能定时器,VAL 从 N 开始]
B --> C[每个时钟周期 VAL 递减 1]
C --> D{VAL 减到 0?}
D -- 否 --> C
D -- 是 --> E[COUNTFLAG 置 1]
E --> F{是否开启中断?}
F -- 是 --> G[触发 SysTick 异常]
F -- 否 --> H[继续轮询 COUNTFLAG]
G --> I[从 LOAD 重新装载初值]
H --> I
I --> C
整个流程是一个不断循环的闭环:计数器从重装载值 N 开始,每个时钟周期递减 1,减到 0 时置位 COUNTFLAG 并根据是否开启中断决定触发异常还是轮询等待,随后自动从 LOAD 重新装载初值进入下一轮计数,如此往复形成稳定的系统时基。
SysTick 的工作机制非常简单:
-
向重装载寄存器(LOAD)写入初值 N。
-
使能定时器后,计数器(VAL)从 N 开始,每个时钟周期递减 1。
-
当计数器减到 0 时:
-
若开启了中断,则触发 SysTick 异常(异常号 15,优先级可配置)。
-
COUNTFLAG 状态位置 1。
-
计数器自动从 LOAD 重新装载初值,继续下一轮计数。
-
注意:SysTick 是 24 位计数器,最大计数值为 2^24 - 1 = 16,777,215。若时钟频率为 72MHz,最大定时时间约为 16,777,215 / 72,000,000 ≈ 233ms。超过这个时长需要多次中断累计。
三、寄存器详解
SysTick 共有 4 个寄存器,映射在内核外设地址空间(基地址 0xE000E010):
|
寄存器 |
偏移地址 |
作用 |
|---|---|---|
|
SYST_CSR(CTRL) |
0x00 |
控制与状态寄存器:使能、中断使能、时钟源选择、计数标志 |
|
SYST_RVR(LOAD) |
0x04 |
重装载值寄存器:24 位,计数器归零时重新装载的值 |
|
SYST_CVR(VAL) |
0x08 |
当前计数值寄存器:读取返回当前计数,写入任意值会清零并清除 COUNTFLAG |
|
SYST_CALIB(CALIB) |
0x0C |
校准寄存器:指示 10ms 校准值及是否有精确参考时钟(通常由芯片厂商设置) |
CTRL 寄存器位定义
|
位 |
名称 |
说明 |
|---|---|---|
|
0 |
ENABLE |
定时器使能位。1 = 使能计数,0 = 关闭 |
|
1 |
TICKINT |
中断使能位。1 = 计数到 0 时触发 SysTick 异常,0 = 不触发中断(可轮询 COUNTFLAG) |
|
2 |
CLKSOURCE |
时钟源选择。1 = 内核时钟(HCLK/ AHB 时钟),0 = 外部参考时钟(STCLK,通常为 HCLK/8,具体由芯片决定) |
|
16 |
COUNTFLAG |
计数标志位。计数器从上一次读取后减到 0 过则为 1;读取 CTRL 寄存器后自动清零 |
四、配置步骤
配置 SysTick 通常遵循以下四步:
-
设置重装载值:根据所需定时周期和时钟频率计算 LOAD 值。 公式:LOAD = 定时周期(秒) × 时钟频率(Hz) - 1 例如 72MHz 内核时钟、1ms 周期:LOAD = 0.001 × 72,000,000 - 1 = 71,999
-
清零当前计数值:向 VAL 寄存器写入任意值(通常写 0),清零计数器和 COUNTFLAG。
-
配置控制寄存器:设置时钟源、使能中断、使能定时器。
-
实现中断服务函数:如果开启了中断,编写 SysTick_Handler() 处理函数。
五、代码实战
5.1 直接操作寄存器(以 STM32F103 为例,72MHz)
// 配置 SysTick:1ms 定时中断
void SysTick_Init(void)
{
// 设置重装载值:72MHz 下 1ms = 72000 个时钟周期,减 1
// LOAD 寄存器(SYST_RVR,偏移 0x04)为 24 位重装载值寄存器,
// 计数器归零后会自动从该值重新装载。LOAD = 周期(秒) × 频率(Hz) - 1
// = 0.001 × 72,000,000 - 1 = 71,999,写入 72000 - 1 即 71,999。
SysTick->LOAD = 72000 - 1;
// 清零当前计数值,同时清除 COUNTFLAG
// VAL 寄存器(SYST_CVR,偏移 0x08)为当前计数值寄存器,
// 写入任意值会清零计数器并清除 CTRL 中的 COUNTFLAG 位(位 16),
// 确保使能后第一个计数周期从完整的 LOAD 值开始,避免首周期不完整。
SysTick->VAL = 0;
// 配置控制寄存器(SYST_CSR/CTRL,偏移 0x00):
// 以下三个宏定义来自 CMSIS 头文件 core_cm3.h(STM32F103 为 Cortex-M3 内核),
// 由 ARM 在 CMSIS 标准中统一定义,位于 core_cm3.h 的 SysTick 结构体声明附近:
// SysTick_CTRL_CLKSOURCE_Msk = (1UL << 2) —— 位 2,时钟源选择
// SysTick_CTRL_TICKINT_Msk = (1UL << 1) —— 位 1,中断使能
// SysTick_CTRL_ENABLE_Msk = (1UL << 0) —— 位 0,定时器使能
// 使用位掩码按位或(|)组合,可避免误改其它位,比直接写数值更清晰、可移植。
//
// CLKSOURCE=1:选择内核时钟(HCLK/AHB 时钟,此处为 72MHz)作为计数时钟源;
// 若为 0 则使用外部参考时钟 STCLK(通常为 HCLK/8)。
// TICKINT=1:计数到 0 时触发 SysTick 异常(异常号 15),进入 SysTick_Handler()。
// ENABLE=1:使能定时器,计数器开始从 LOAD 值递减。
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk |
SysTick_CTRL_TICKINT_Msk |
SysTick_CTRL_ENABLE_Msk;
}
// 全局毫秒计数器
volatile uint32_t g_sys_tick = 0;
// SysTick 中断服务函数(启动文件中已定义弱符号,此处重写)
void SysTick_Handler(void)
{
g_sys_tick++; // 每 1ms 加 1
}
// 获取当前毫秒数
uint32_t GetTick(void)
{
return g_sys_tick;
}
// 毫秒级延时
void Delay_ms(uint32_t ms)
{
uint32_t start = GetTick();
while ((GetTick() - start) < ms);
}
5.2 使用 CMSIS 标准函数(推荐)
CMSIS 提供了 SysTick_Config() 函数,一行即可完成配置:
// 配置 1ms SysTick 中断(SystemCoreClock 为内核时钟频率,单位 Hz)
// 函数内部自动完成 LOAD、VAL、CTRL 的配置及中断优先级设置
if (SysTick_Config(SystemCoreClock / 1000))
{
// 配置失败(通常是 LOAD 值超过 24 位范围)
while (1);
}
// 中断服务函数同上
void SysTick_Handler(void)
{
g_sys_tick++;
}
SysTick_Config(ticks)的参数是两次中断之间的时钟周期数。要得到 1ms:ticks = SystemCoreClock / 1000;要得到 1us:ticks = SystemCoreClock / 1000000。
5.3 微秒级延时(轮询方式,不占用中断)
static uint32_t fac_us = 0; //us延时倍乘数
void delay_init(uint8_t SYSCLK)
{
fac_us = SYSCLK;
}
void delay_us(uint32_t nus)//100 6800
{
uint32_t ticks;
uint32_t told, tnow, tcnt = 0;
uint32_t reload = SysTick->LOAD; //LOAD的值
ticks = nus * fac_us; //需要的节拍数
told = SysTick->VAL; // 24 刚进入时的计数器值
while (1)
{
tnow = SysTick->VAL;//22 20 0
if (tnow != told)
{
if (tnow < told)
tcnt += told - tnow; //这里注意一下SYSTICK是一个递减的计数器就可以了.
else
tcnt += reload - tnow + told;
told = tnow;
if (tcnt >= ticks)
break; //时间超过/等于要延迟的时间,则退出.
}
};
}
void delay_ms(uint16_t nms)
{
uint32_t i;
for (i = 0; i < nms; i++)
delay_us(1000);
}
六、典型应用场景
6.1 RTOS 时钟节拍
以 FreeRTOS 为例,其 vPortSetupTimerInterrupt() 函数内部就是通过配置 SysTick 来产生 1ms(configTICK_RATE_HZ = 1000)的节拍中断,驱动任务调度。用户通常无需手动配置。
6.2 裸机非阻塞延时
利用全局 Tick 计数器实现非阻塞延时,避免 CPU 空转:
void App_Task(void)
{
static uint32_t last_led_tick = 0;
static uint32_t last_sensor_tick = 0;
uint32_t now = GetTick();
// 每 500ms 翻转 LED
if (now - last_led_tick >= 500)
{
last_led_tick = now;
LED_Toggle();
}
// 每 100ms 读取传感器
if (now - last_sensor_tick >= 100)
{
last_sensor_tick = now;
Sensor_Read();
}
}
七、注意事项与常见问题
-
计数器初值:使能后第一次计数是从 LOAD 值开始,不是从 0 开始。如果 VAL 未清零就使能,第一次计数周期可能不完整,因此配置时务必先写 VAL = 0。
-
校准寄存器:CALIB 寄存器中的 TENMS 字段给出了产生 10ms 延时所需的计数值,但不同芯片厂商实现可能不同,实际开发中建议根据具体时钟频率自行计算,不依赖 CALIB。

常见问题排查
下面汇总了 SysTick 使用中常见的 5 个典型问题,并给出原因分析与解决方案,便于快速定位和修复。
|
常见问题 |
原因分析 |
解决方案 |
|---|---|---|
|
延时不准(偏长或偏短) |
LOAD 值计算错误,或实际内核时钟频率与配置不一致;CLKSOURCE 选择了错误的时钟源。 |
确认 SystemCoreClock 与实际运行频率一致,按 LOAD = 周期(秒) × 频率(Hz) - 1 重新计算;核对 CLKSOURCE 位,确保使用预期的内核时钟。 |
|
中断不触发 |
TICKINT 位未置 1;SysTick 异常优先级被屏蔽;中断服务函数未实现或未正确链接。 |
检查 CTRL 寄存器 TICKINT 位;确认 NVIC 中 SysTick 异常未被屏蔽;实现并链接 SysTick_Handler(),避免使用弱符号覆盖冲突。 |
|
与 RTOS 冲突(节拍错乱) |
用户代码与 RTOS 同时抢占 SysTick,或修改了 RTOS 已配置的 LOAD/CTRL 寄存器。 |
在 RTOS 环境下不要手动重配 SysTick,统一使用 RTOS 提供的节拍接口;若需额外延时,改用其他定时器或软件延时。 |
|
首次计数周期不完整 |
使能前未清零 VAL,计数器从残留值开始递减,导致第一个周期偏短。 |
配置时先向 VAL 写入任意值(通常写 0)清零计数器,再使能定时器,确保首个周期完整。 |
|
长时间延时失效 |
SysTick 为 24 位计数器,单次最大定时约 233ms(72MHz 下),超过后未做多次中断累计。 |
在中断服务函数中维护全局 Tick 计数器,通过多次中断累计实现长延时;或改用硬件定时器。 |
边界条件与极端情况
除了上述常见问题,SysTick 在低时钟频率、低功耗模式和多核架构等边界条件下还有一些容易被忽略的细节,下面逐一说明。
1. 低时钟频率下的 LOAD 值溢出
SysTick 的 LOAD 寄存器只有 24 位,最大可写入值为 2^24 - 1 = 16,777,215。当时钟频率较低时,同样的定时周期换算出的计数值可能超过该上限。以 32kHz 时钟为例,若要产生 1 秒定时,LOAD = 1 × 32,000 - 1 = 31,999,仍在 24 位范围内;但若要产生 10 分钟(600 秒)定时,LOAD = 600 × 32,000 - 1 = 19,199,999,已超过 16,777,215,写入后会被截断,导致实际定时时间远小于预期。解决思路是:先计算 LOAD 值并判断是否超过 0xFFFFFF,若超限则改用多次中断累计,或换用硬件定时器;也可以适当降低定时周期,通过中断计数累加实现长延时。
2. 与低功耗模式(睡眠/停止)的交互
SysTick 的计数时钟通常来自内核时钟(HCLK)或外部参考时钟(STCLK)。在睡眠模式下,若内核时钟仍在运行,SysTick 可继续计数并正常触发中断唤醒;但在停止模式下,内核时钟通常被关闭,SysTick 会停止计数,此时依赖 SysTick 的延时和 RTOS 节拍都会失效。因此,在进入停止模式前应暂停依赖 SysTick 的任务,改用支持低功耗唤醒的定时器(如 RTC 或 LPTIM)作为唤醒源;若必须保留时间基准,可考虑在唤醒后重新校准 SysTick 的 LOAD 和 VAL 值,避免计数偏差累积。
3. 多核处理器中的 SysTick 归属
在 Cortex-M 多核处理器(如 Cortex-M33 + Cortex-M4 组合)中,每个内核都拥有自己独立的 SysTick 实例,寄存器地址相同但物理上相互独立。因此,各核应分别配置和使用自己的 SysTick,不能跨核访问或共享同一个 SysTick 中断服务函数。若需要跨核同步时间基准,建议通过核间通信机制(如 Mailbox 或共享内存)传递 Tick 值,而不是直接操作对方内核的 SysTick 寄存器。
4. 24 位计数器回绕时的无符号减法处理
SysTick 的 VAL 是 24 位递减计数器,读取到的值会从 LOAD 递减到 0 后重新装载,因此在连续读取时可能出现回绕。利用无符号整数的回绕特性,可以用一次减法安全地计算时间差,而无需判断是否发生回绕。例如:
uint32_t start = SysTick->VAL;
// 执行需要测量的代码段
uint32_t end = SysTick->VAL;
uint32_t elapsed = (start - end) & 0x00FFFFFF; // 取低 24 位,自动处理回绕
由于 start 和 end 都是无符号数,且差值不超过 2^24,直接相减再按位与 0x00FFFFFF 即可得到正确的递减计数差值,无需判断 end 是否大于 start。这一技巧同样适用于基于全局 Tick 计数器的毫秒级延时判断,例如 (GetTick() - start) < ms 的写法正是利用了无符号回绕特性,即使 Tick 计数器溢出也能正确计算时间差。
八、精准上报实验
-
1ms精准上报

-
10ms精准上报


九、总结与展望
SysTick 作为 Cortex-M 内核自带的 24 位递减定时器,凭借配置简单、跨芯片通用、不占用外设定时器资源等优势,已成为 RTOS 时钟节拍、裸机精确延时和时间测量的首选方案。选型时建议优先使用 CMSIS 标准函数,并注意时钟源、重装载值与中断优先级的正确配置。展望未来,随着低功耗设计日益普及,SysTick 在休眠唤醒、动态节拍(tickless)等场景中的作用将更加突出;在 RTOS 集成方面,其与任务调度、功耗管理的协同优化也将成为嵌入式开发的重要方向。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)