本章讲解系统定时器(SysTick),主要分成三部分内容。首先是简单介绍一下系统定时器,然后是分析它的功能框图,最后是设计一个实验。系统定时器属于 Cortex-M 内核中的一个定时器,只要你是 Cortex-M 内核的,不管是 M3、M4 还是 M7,都会有这个系统定时器。因此,本章内容不仅兼容"霸道"和"指南者"开发板,其他开发板也可以参考学习。
一、系统定时器简介
系统定时器英文名为 System Tick Timer,中文译为系统定时器。它是一个 24 位的计数器,只能递减计数,属于 Cortex-M3 内核的一部分。如前所述,无论使用的是 Cortex-M3、M4 还是 M7 内核,都包含这个 SysTick 定时器,并且它嵌套在 NVIC(嵌套向量中断控制器)中。SysTick 中断在 NVIC 的向量表中有固定的中断号(IRQn 为 SysTick_IRQn,负数,属于内核中断),但它的优先级配置不在普通的 NVIC_IPR 寄存器中,而是在 SCB(系统控制块)的 SHPR3 寄存器中完成,这也是后面优先级部分的重点。
简介部分我们主要记住两点:
第一,它存在于内核中
SysTick 属于 Cortex-M3 内核的一部分,在 ARM 官方的 Cortex-M3 编程手册中被划分到 "System timer (SysTick)" 小节;在 STM32 的芯片参考手册中,只会在 "中断和事件" 章节中简单提到其中断号和基本说明,细节都在 ARM 的手册里。
第二,它是 24 位的递减计数器
计数宽度固定为 24 位,最大重装载值 224−1=0xFFFFFF=16,777,215。只能递减计数(从 RELOAD 设定的值往 0 数),不能递增。每次计到 0 时,如果没有关闭 SysTick,会自动从 RELOAD 重新装载,开始下一轮计数。
二、功能框图详解
在 STM32 的官方参考手册中,关于 SysTick 的讲解相对较少。通常在"中断和事件"章节可能只有简要提及。由于其属于内核级外设,详细信息需要查阅 ARM 公司提供的 Cortex-M3 编程手册 (通常是英文版);在 ARM 官方的 Cortex-M3 编程手册中被划分到 "System timer (SysTick)" 小节。 STM32 的芯片参考手册中,只会在 "中断和事件" 章节中简单提到其中断号和基本说明,细节都在 ARM 的手册里。
1. 根据资料,它主要包含三个核心部分:
时钟源
递减计数器:24 位宽度,最大值为 2^24。
重装载寄存器(Reload Register):同样是 24 位,用于设定计数器的初始值。
2. 其工作原理是:
计数器在时钟的驱动下,从重装载寄存器(RELOAD)设定的初值开始递减计数。在递减过程中,可以通过 VAL(Current Value Register)寄存器实时读取当前计数值。当计数器从重装载值递减到 0 时,会触发两个动作:
如果使能了中断,则会产生 SysTick 中断,从而执行相应的中断服务程序。
同时,控制和状态寄存器(CTRL)中的 COUNTFLAG 标志位会被置 1。
此后,如果计数器未被关闭,它会自动从重装载寄存器中重新加载初始值,并开始新一轮的递减计数,如此周而复始。
3. 结合工程代码详解功能框图
根据 core_cm3.h中的定义,可以更直观地看到 SysTick 的寄存器模型:
// 文件: 19-systick/Libraries/CMSIS/core_cm3.h (第365-371行附近)
typedef struct
{
__IO uint32_t CTRL; /*!< Offset: 0x00 SysTick Control and Status Register */
__IO uint32_t LOAD; /*!< Offset: 0x04 SysTick Reload Value Register */
__IO uint32_t VAL; /*!< Offset: 0x08 SysTick Current Value Register */
__I uint32_t CALIB; /*!< Offset: 0x0C SysTick Calibration Register */
} SysTick_Type;
CTRL 寄存器:控制 / 状态
控制定时器是否使能、是否产生中断、时钟源选择。状态位中最关键的是 COUNTFLAG(计数到 0 标志)。
LOAD 寄存器:重装载值
24 位有效,决定了单次计数周期的长度。用于设置计数器递减到 0 后重新装载的数值。每次计数到 0 后,硬件会把此值装入 VAL,开始新一轮倒计时。
VAL 寄存器:当前计数值
24 位有效,当前递减计数器的值。读取时返回计数器的当前值。向该寄存器写入任何值都会将其清零,并同时清除 CTRL 寄存器中的 COUNTFLAG 标志。
CALIB 寄存器:校准值
内部工厂校准值,用于在某个固定频率下生成 10ms/1ms 基准。实际工程中,通常较少使用。例如,手册中提到当时钟为 9MHz 时,校准值固定为 9000,可用于产生 1ms 的时间基准,但最常用的还是前三个寄存器。
4. 结合代码,可以把功能框图总结为一条"工作通路":
-
选择时钟源(
CTRL.CLKSOURCE),例如 AHB = 72MHz 时,SysTick 时钟可为 72MHz 或 AHB/8 = 9MHz。 -
设置
LOAD为希望的计数周期(减到 0 的计数次数)。 -
清零
VAL,清除历史计数。 -
设置
CTRL.ENABLE使能计数。 -
每次计到 0:
-
若
TICKINT=1,产生 SysTick 中断; -
COUNTFLAG被硬件置 1; -
若未关闭 SysTick,自动从
LOAD重新装载。
-
三、寄存器描述
在 core_cm3.h中,对 CTRL、LOAD、VAL、CALIB 的重要位有宏定义,方便固件库代码使用:
// 文件: 19-systick/Libraries/CMSIS/core_cm3.h (第373-402行附近)
/* SysTick Control / Status Register Definitions */
#define SysTick_CTRL_COUNTFLAG_Pos 16 /*!< SysTick CTRL: COUNTFLAG Position */
#define SysTick_CTRL_COUNTFLAG_Msk (1ul << SysTick_CTRL_COUNTFLAG_Pos) /*!< SysTick CTRL: COUNTFLAG Mask */
#define SysTick_CTRL_CLKSOURCE_Pos 2 /*!< SysTick CTRL: CLKSOURCE Position */
#define SysTick_CTRL_CLKSOURCE_Msk (1ul << SysTick_CTRL_CLKSOURCE_Pos) /*!< SysTick CTRL: CLKSOURCE Mask */
#define SysTick_CTRL_TICKINT_Pos 1 /*!< SysTick CTRL: TICKINT Position */
#define SysTick_CTRL_TICKINT_Msk (1ul << SysTick_CTRL_TICKINT_Pos) /*!< SysTick CTRL: TICKINT Mask */
#define SysTick_CTRL_ENABLE_Pos 0 /*!< SysTick CTRL: ENABLE Position */
#define SysTick_CTRL_ENABLE_Msk (1ul << SysTick_CTRL_ENABLE_Pos) /*!< SysTick CTRL: ENABLE Mask */
/* 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_VAL_CURRENT_Pos) /*!< SysTick VAL: CURRENT Mask */
/* SysTick Calibration Register Definitions */
#define SysTick_CALIB_NOREF_Pos 31 /*!< SysTick CALIB: NOREF Position */
#define SysTick_CALIB_NOREF_Msk (1ul << SysTick_CALIB_NOREF_Pos) /*!< SysTick CALIB: NOREF Mask */
#define SysTick_CALIB_SKEW_Pos 30 /*!< SysTick CALIB: SKEW Position */
#define SysTick_CALIB_SKEW_Msk (1ul << SysTick_CALIB_SKEW_Pos) /*!< SysTick CALIB: SKEW Mask */
#define SysTick_CALIB_TENMS_Pos 0 /*!< SysTick CALIB: TENMS Position */
#define SysTick_CALIB_TENMS_Msk (0xFFFFFFul << SysTick_VAL_CURRENT_Pos) /*!< SysTick CALIB: TENMS Mask */
SysTick 主要包含四个寄存器:
CTRL(控制与状态寄存器):仅有部分位有效。
第 16 位(COUNTFLAG):
每次计数器从 1 递减到 0 时由硬件置 1;软件通过读 CTRL寄存器时,读到该位为 1,表示"本周期到 0 过一次";读出该位的同时会自动清 0 ,下次需要再次等到 0 才会重新置 1。工程中常见用法为,轮询 COUNTFLAG,实现"每 N 个时钟周期触发一次"的轮询式延时(就像本例的 SysTick_Delay_us/ms所做的那样)。
第 2 位(CLKSOURCE):时钟源选择位。设置为 0 时,时钟频率 = AHB 总线时钟 / 8(若 AHB 为 72MHz,则 SysTick 时钟为 9MHz);设置为 1 时,时钟频率直接等于 AHB 总线时钟(72MHz)。对于 F103,如果系统时钟 72MHz、AHB 不再分频,则 SysTick 时钟要么是 72MHz,要么是 9MHz。参考 STM32 时钟树,系统时钟经 AHB 预分频后,可选择是否 8 分频后提供给 Cortex 系统定时器。
第 1 位(TICKINT) :中断使能位。设置为 0 时,禁止中断,只是置位 COUNTFLAG;设置为 1 时,使能中断,当计数器递减到 0 时会产生SysTick 中断请求。
第 0 位(ENABLE):定时器使能位。设置为 0 时,关闭定时器;设置为 1 时,使能(启动)定时器。
四、定时时间计算
定时时间 T指的是计数器完成一个完整计数循环(从重装载值递减到 0)所需的时间。它取决于时钟频率(CLK)和重装载值(RELOAD)。计算公式为:
T = reload × (1/CLK)
其中,CLK 可以是 72MHz 或 9MHz(由 CTRL 寄存器的 CLKSOURCE 位决定),RELOAD 是用户配置的 24 位值。
举例说明(当 CLK = 72MHz 时):
当 CLK = 72MHz 时,若设置 RELOAD = 72,则 T=72 × (1/72MHz)=0.000001秒 = 1 微秒 (us)。
当 CLK = 72MHz 时,若设置 RELOAD = 72000,则 T=72000 × (1/72MHz)=0.001秒 = 1 毫秒 (ms)。
**时间单位换算:** 1秒(s) = 1000毫秒(ms) = 1,000,000微秒(us) = 1,000,000,000纳秒(ns)。
在实际编程中,微秒级延时常用于精确时序控制,但若采用中断方式实现微秒延时,会因频繁进入中断而影响程序效率。因此,对于一般应用,毫秒级延时更为常用和实用。例如,要实现 1 秒的延时,可以设置产生 1ms 的中断,然后在中断服务程序中计数 1000 次。
五、固件库中的 SysTick
在 STM32 固件库中,SysTick 作为内核外设,其寄存器定义通常在 core_cm3.h(或对应内核的头文件)中。相关的库函数也在该头文件或对应的源文件中定义。
关键的寄存器定义包括:
SysTick->CTRL(控制与状态寄存器)
SysTick->LOAD(重装载值寄存器)
SysTick->VAL(当前值寄存器)
SysTick->CALIB(校准值寄存器,较少使用)
常用的固件库函数是 SysTick_Config(uint32_t ticks),该函数会:
检查传入的 ticks参数是否超过重装载寄存器的最大值(2^24 - 1)。
将 ticks值设置到 LOAD 寄存器。
初始化 SysTick 计数器并将其清零。
配置中断优先级。
使能 SysTick 定时器和其中断。
结合代码详解固件库中的 SysTick
你在笔记中引用的 SysTick_Config、NVIC_SetPriority、__NVIC_PRIO_BITS,在本工程 core_cm3.h中确实是这样定义的:
1. __NVIC_PRIO_BITS宏定义
// 文件: 19-systick/Libraries/CMSIS/core_cm3.h (第97-99行附近)
#ifndef __NVIC_PRIO_BITS
#define __NVIC_PRIO_BITS 4 /*!< standard definition for NVIC Priority Bits */
#endif
-
含义:本内核实现了 4 位中断优先级位,因此中断优先级数值范围为 0~15。
-
这 4 位既适用于内核中断(通过 SCB->SHP)也适用于外设中断(通过 NVIC->IP)。
2. NVIC_SetPriority函数
// 文件: 19-systick/Libraries/CMSIS/core_cm3.h (第1586-1592行附近)
static __INLINE void NVIC_SetPriority(IRQn_Type IRQn, uint32_t priority)
{
if(IRQn < 0) {
SCB->SHP[((uint32_t)(IRQn) & 0xF)-4] = ((priority << (8 - __NVIC_PRIO_BITS)) & 0xff); } /* set Priority for Cortex-M3 System Interrupts */
else {
NVIC->IP[(uint32_t)(IRQn)] = ((priority << (8 - __NVIC_PRIO_BITS)) & 0xff); } /* set Priority for device specific Interrupts */
}
逐句解释:
-
IRQn < 0:-
中断号为负,表示这是内核中断(比如 SysTick、PendSV、SVCall 等)。
-
对应的优先级寄存器是
SCB->SHP[x](System Handler Priority Registers)。 -
((uint32_t)(IRQn) & 0xF)-4用来把负的中断号映射到 0~11 范围的 SHP 数组下标。
-
-
IRQn >= 0:- 表示这是片上外设中断 ,使用
NVIC->IP[]数组设置优先级。
- 表示这是片上外设中断 ,使用
-
priority << (8 - __NVIC_PRIO_BITS):-
NVIC 的每个优先级字段在 8 位中只实现了高
__NVIC_PRIO_BITS位(这里是 4 位),低位保留不用。 -
因此需要把用户传入的 4 位
priority左移到高位,比如:priority = 0x0F→ 寄存器中写入0xF0。
-
-
& 0xff:- 保证只写入 8 位数据(防御性写法)。
这段和同后文中,"内核中断优先级 vs 外设中断优先级"以及"4 位优先级数值"的解释完全对应。
3. SysTick_Config函数
// 文件: 19-systick/Libraries/CMSIS/core_cm3.h (第1694-1704行附近)
static __INLINE uint32_t SysTick_Config(uint32_t ticks)
{
if (ticks > SysTick_LOAD_RELOAD_Msk) return (1); /* Reload value impossible */
SysTick->LOAD = (ticks & SysTick_LOAD_RELOAD_Msk) - 1; /* set reload 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 SysTick IRQ and SysTick Timer */
return (0); /* Function successful */
}
详细注释版(逻辑解释):
-
if (ticks > SysTick_LOAD_RELOAD_Msk) return (1);SysTick_LOAD_RELOAD_Msk = 0xFFFFFF,若用户传入的ticks超出 24 位范围,说明无法装入 RELOAD,直接返回 1 表示错误。
-
SysTick->LOAD = (ticks & SysTick_LOAD_RELOAD_Msk) - 1;-
把有效的 24 位
ticks值写入 RELOAD。 -
之所以减 1,是因为硬件的计数范围是从这个值计到 0,总共会经历
LOAD + 1个计数周期。- 例如:希望产生 72 个周期,则应写入 71。
-
-
NVIC_SetPriority (SysTick_IRQn, (1<<__NVIC_PRIO_BITS) - 1);-
设置 SysTick 的中断优先级为 0x0F(四位全 1),即最低优先级。
-
这印证了你的笔记:SysTick_Config 默认把 SysTick 优先级设置为最低,不是"天然最高"。
-
-
SysTick->VAL = 0;- 清空当前计数值,保证重新开始计数,且清除
COUNTFLAG。
- 清空当前计数值,保证重新开始计数,且清除
-
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk | SysTick_CTRL_TICKINT_Msk | SysTick_CTRL_ENABLE_Msk;-
CLKSOURCE = 1:时钟源选择 AHB(对于 F103 即 72MHz)。 -
TICKINT = 1:使能中断。 -
ENABLE = 1:启动计数器。
-
六、中断优先级配置详解
SysTick 的中断优先级配置是一个需要特别注意的重点,也是容易产生误解的地方。下面将从优先级表示、配置寄存器、分组影响、比较规则及常见误区等方面进行详细说明。
1. 优先级表示与配置寄存器
-
优先级表示 :在 STM32 中,无论是内核外设(如 SysTick)还是片上外设(如 GPIO、USART),其中断优先级都是用 4 个二进制位来表示的,数值范围为 0-15(0为最高,15为最低)。
-
配置寄存器不同:
-
片上外设 的中断优先级配置在 NVIC_IPRx 寄存器组中。
-
内核外设 (如 SysTick、PendSV)的中断优先级则配置在系统控制块 (SCB) 的 SHPRx (System Handler Priority Registers)寄存器中。例如,SysTick 的中断(中断号为 -1)优先级对应 SHPR3 寄存器的 Bit31:Bit24 这个8位字段的高4位。正因为配置寄存器不同,所以不能使用配置外设中断的
NVIC_Init函数来配置 SysTick 的优先级 ,而应使用NVIC_SetPriority函数。
-
2. 优先级分组的影响与比较规则
-
优先级分组的影响 :中断优先级分组(通过
SCB->AIRCR寄存器的PRIGROUP字段设置)不仅对片上外设有效,同样对内核外设也有效。它决定了4位优先级数值中,多少位表示抢占优先级 (Preemption Priority),多少位表示子优先级(Subpriority)。例如,分组2表示高2位为抢占优先级,低2位为子优先级。 -
比较规则:当比较内核外设(如 SysTick)与片上外设的中断优先级时,需要遵循以下统一规则:
-
首先比较抢占优先级。抢占优先级数值小的中断可以打断抢占优先级数值大的正在执行的中断。
-
如果抢占优先级相同,则比较子优先级。子优先级数值小的中断优先执行。
-
如果抢占优先级和子优先级都相同,则比较它们的硬件中断编号(在中断向量表中的位置)。编号越小的中断,其自然优先级越高。SysTick 的中断编号(-1)非常靠前,这意味着在软件优先级相同的情况下,它的自然优先级高于许多片上外设。
-
3. 关键认识与常见误区纠正
-
最关键的认识:
-
SysTick 不是"天生最高优先级",它的优先级可以设得比外设低。
-
在本工程中,因为用的是库函数
SysTick_Config,其默认优先级为 0x0F(即15),是最低优先级,所以完全可以被任意设置为更高优先级(数值更小)的外设中断打断。
-
-
常见误区纠正:
-
误区:内核外设的中断优先级一定高于片上外设。
-
纠正 :并非如此。例如,如果用户将一个片上外设的中断优先级设置为 0x02(二进制0010),而SysTick保持默认的0x0F(二进制1111),且中断优先级分组设置为2(即高2位是抢占优先级)。那么,片上外设的抢占优先级是
00(0),而SysTick的抢占优先级是11(3)。由于0 < 3,因此这个片上外设中断可以打断SysTick中断。关键在于正确理解并统一用优先级分组规则来解析和比较它们各自的4位优先级数值。
-
4. 工程应用与总结
后续实验工程中,并没有真正使用 SysTick 中断 ,而是通过轮询 COUNTFLAG来实现延时,因此 SysTick_Config中配置的中断优先级在当前实验里不影响功能,但理解它对以后写系统"滴答时基"(如RTOS或系统心跳)至关重要。
理解这一点对于构建稳定的系统非常重要,特别是在使用 SysTick 作为系统时钟基准并同时处理其他中断的应用程序中。如果配置不当,可能会导致时序错乱或中断响应不及时。开发者需要根据实际应用需求,合理配置 SysTick 及其他外设的中断优先级。