文章目录
- 一、发展历史:操作系统催生的内核定时器
-
- [1. 诞生:为时间片而生](#1. 诞生:为时间片而生)
- [2. 嵌入化:从 `RTOS` 到裸机延时的通用工具](#2. 嵌入化:从
RTOS到裸机延时的通用工具) - [3. 技术演进:从简单定时到系统心跳](#3. 技术演进:从简单定时到系统心跳)
- 二、工作原理:一个永远倒计时的数字沙漏
-
- [1. 最直白的比喻:沙漏](#1. 最直白的比喻:沙漏)
- [2. 核心机制:硬件自动重载与标志轮询](#2. 核心机制:硬件自动重载与标志轮询)
- [3. 关键特性一:`24` 位的限制与溢出处理](#3. 关键特性一:
24位的限制与溢出处理) - [4. 关键特性二:两种时钟源可选](#4. 关键特性二:两种时钟源可选)
- [三、技术规格:`4` 个寄存器管好一个定时器](#三、技术规格:
4个寄存器管好一个定时器) -
- [1. 寄存器一览](#1. 寄存器一览)
- [2. `CTRL(控制与状态寄存器)` 寄存器位域详解](#2.
CTRL(控制与状态寄存器)寄存器位域详解) - [3. `LOAD(重装载值寄存器)` 与 `VAL(当前计数值寄存器)` 的关系](#3.
LOAD(重装载值寄存器)与VAL(当前计数值寄存器)的关系) - [4. 初始化 `SysTick`](#4. 初始化
SysTick)
- [四、硬件实现:`SysTick` 功能框图](#四、硬件实现:
SysTick功能框图) -
- [1. 功能框图](#1. 功能框图)
- [2. 特殊机制:`VAL(当前计数值寄存器)` 写操作即时生效](#2. 特殊机制:
VAL(当前计数值寄存器)写操作即时生效)
- [五、实践指南:用 `SysTick` 实现毫秒和微秒延时](#五、实践指南:用
SysTick实现毫秒和微秒延时) -
- [1. 配置流程三步走](#1. 配置流程三步走)
- [2. 以 `72MHz` `HCLK` 为例实现毫秒延时](#2. 以
72MHzHCLK为例实现毫秒延时) -
- [(1) 参数确定](#(1) 参数确定)
- [(2) 初始化代码](#(2) 初始化代码)
- [(3) 毫秒延时函数](#(3) 毫秒延时函数)
- [(4) 微秒延时函数](#(4) 微秒延时函数)
- [六、多定时器配合:`SysTick` + `TIM` 的典型分工](#六、多定时器配合:
SysTick+TIM的典型分工) - [七、任意平台纯软件延时 vs 硬件 `SysTick`](#七、任意平台纯软件延时 vs 硬件
SysTick) -
- [1. 核心思想](#1. 核心思想)
- [2. 纯软件微秒延时](#2. 纯软件微秒延时)
- [3. 软件方式 vs 硬件 `SysTick`](#3. 软件方式 vs 硬件
SysTick)
- 八、提高延时精度与稳定性的技巧
-
- [1. 延时期间关总中断](#1. 延时期间关总中断)
- [2. 使用 `SysTick_Config` 中断模式替代轮询](#2. 使用
SysTick_Config中断模式替代轮询) - [3. 校准因子按实际频率重算](#3. 校准因子按实际频率重算)
- 九、SysTick这里为什么不初始化GPIO?
-
- [核心答案:`SysTick` 是**内核内部**的定时器,不需要也不存在 `GPIO` 引脚](#核心答案:
SysTick是内核内部的定时器,不需要也不存在GPIO引脚) - [图解:`SysTick` 在内核中的位置](#图解:
SysTick在内核中的位置) - [为什么 `SysTick` 不需要 `GPIO` 初始化?三个根本原因](#为什么
SysTick不需要GPIO初始化?三个根本原因) -
- [1. 物理上不存在引脚](#1. 物理上不存在引脚)
- [2. 时钟来源是内核时钟,不是外部引脚](#2. 时钟来源是内核时钟,不是外部引脚)
- [3. 操作方式是对寄存器读写,不涉及引脚状态](#3. 操作方式是对寄存器读写,不涉及引脚状态)
- 一个类比帮助你理解
- 所以,回到你的问题
- [验证一下:看 `SysTick` 的寄存器地址](#验证一下:看
SysTick的寄存器地址)
- [核心答案:`SysTick` 是**内核内部**的定时器,不需要也不存在 `GPIO` 引脚](#核心答案:
- 十、结语:内核自带的心跳,最轻量的时钟
你可能每天都在用 delay_ms(1000),却不知道是谁在背后帮你精准地数着这 1000 毫秒。它不是某个外设,而是 Cortex-M 内核自带的"心跳"------SysTick,全称 System Tick Timer (系统节拍定时器)。 今天这篇博客,咱们把它从 24 位倒计时器的硬件结构,一路拆解到毫秒/微秒延时的代码实现。读完你会发现,原来 delay_ms 里还藏着计数值溢出的数学游戏。
一、发展历史:操作系统催生的内核定时器
1. 诞生:为时间片而生
在嵌入式实时操作系统(RTOS)中,多个任务并发执行,CPU 必须在任务之间快速切换。这就要求有一个精准的定时器来产生固定间隔的"时间片"中断------时间一到,不管当前任务执行到哪,都必须停下来换下一个任务。 ARM 在设计 Cortex-M 内核时,直接把这样一个定时器做进了内核里,取名 SysTick。它不是挂在 APB 总线上的"外设",而是内核的标配组件 ------任何 Cortex-M 芯片,不管 MCU 厂商是谁,SysTick 都在那里寄存器地址完全一样。

2. 嵌入化:从 RTOS 到裸机延时的通用工具
SysTick 最初为 RTOS 而设计,但裸机开发者很快发现它的另一个绝佳用途------精准延时。传统 for 循环延时受主频、编译器优化影响极大,挪一块板子就得重调循环次数。而 SysTick 直接挂在系统时钟上,72MHz 就每 1/72μs 减一,精度从"大概"变成了"确切"。 STM32F103 的 SysTick 是一个 24 位向下计数器,时钟源可选 HCLK(72MHz)或 HCLK/8(9MHz),每次计数到 0 时产生中断或置标志位,自动从 LOAD(重装载值寄存器) 寄存器重装载,周而复始。
3. 技术演进:从简单定时到系统心跳
| 年代 | 平台 | 定时器类型 | 核心特性 |
|---|---|---|---|
1990s |
51 单片机 |
8/16 位向上计数 |
外设定时器,需手动配置中断 |
2000s |
ARM7TDMI |
32 位外设定时器 |
向量中断,无内核定时器 |
2005s 至今 |
Cortex-M3/M4 |
24 位 SysTick |
内核定時器,所有 Cortex-M 统一地址 |
未来 |
Cortex-M55 |
64 位 SysTick |
支持虚拟化和安全扩展 |
二、工作原理:一个永远倒计时的数字沙漏
1. 最直白的比喻:沙漏
SysTick 就像一个倒计时沙漏:
LOAD(重装载值寄存器)寄存器:你设定沙漏的总沙子量(计数值)。VAL(当前计数值寄存器)寄存器:沙漏里当前剩余的沙子量(每时钟周期减少一粒)。CTRL(控制与状态寄存器)寄存器:沙漏的开关和设置面板。- 倒数到
0:沙子漏完,沙漏自动翻转(从LOAD(重装载值寄存器)重新加载),同时举手报告"漏完了一次"。
每次沙子漏完,CTRL(控制与状态寄存器) 寄存器的第 16 位(COUNTFLAG)被置 1。你只需要读这个标志位,就知道延时到了没有。

2. 核心机制:硬件自动重载与标志轮询
SysTick 的工作流程总共三步:
- 硬件加载 :使能定时器后,硬件自动将
LOAD(重装载值寄存器)寄存器的值拷贝到VAL(当前计数值寄存器)寄存器。- 递减计数 :每个时钟周期
VAL(当前计数值寄存器)减1,直到VAL(当前计数值寄存器)变为0。- 置标志 + 重载 :
VAL(当前计数值寄存器)回到0的瞬间,CTRL(控制与状态寄存器)的第16位(COUNTFLAG)置1,同时如果定时器未停止,硬件再次将LOAD(重装载值寄存器)的值加载到VAL(当前计数值寄存器),继续下一轮倒数。
你只需要在 while 循环里反复读 CTRL(控制与状态寄存器) 的第 16 位,读到 1 就表示一轮计数结束。如果配置了中断,读到 0 后 CPU 自动跳到 SysTick_Handler。
3. 关键特性一:24 位的限制与溢出处理
LOAD(重装载值寄存器) 和 VAL(当前计数值寄存器) 寄存器只有 24 位有效,取值范围 0x00000001 ~ 0x00FFFFFF(1 ~ 16,777,215)。以 HCLK=72MHz、HCLK 不分频作为时钟源为例,一轮计时最多只能跑 16,777,215 / 72MHz ≈ 0.233s,想延时 1 秒就需要多次循环。这就是毫秒延时函数里"先按最大值循环多次,最后再计剩余值"的数学根源。
4. 关键特性二:两种时钟源可选
CTRL(控制与状态寄存器) 寄存器的第 2 位选择时钟源:
CLKSOURCE |
时钟源 | 频率(HCLK=72MHz) |
微秒因子 fac_us |
适用场景 |
|---|---|---|---|---|
1 |
HCLK(不分频) |
72MHz |
72 |
高精度短延时 |
0 |
HCLK/8 |
9MHz |
9 |
低功耗长延时,省时钟功耗 |
选择 HCLK 不分频时精度最高,单步 1/72μs ≈ 13.9ns,但单轮计时最多 0.233s。选择 HCLK/8 时单步 1/9μs ≈ 111ns,但单轮计时可达 1.86s。
三、技术规格:4 个寄存器管好一个定时器
1. 寄存器一览
SysTick 只有 4 个寄存器,地址在 Cortex-M3 内核中固定不变:
|------------------|--------------|----------|-------------|
| 寄存器 | 地址偏移 | 宽度 | 功能 |
| CTRL(控制与状态寄存器) | 0xE000E010 | 32 位 | 控制与状态寄存器 |
| LOAD(重装载值寄存器) | 0xE000E014 | 24 位有效 | 重装载值寄存器 |
| VAL(当前计数值寄存器) | 0xE000E018 | 24 位有效 | 当前计数值寄存器 |
| CALIB(校准信息寄存器) | 0xE000E01C | 32 位 | 校准信息寄存器(只读) |
2. CTRL(控制与状态寄存器) 寄存器位域详解
CTRL(控制与状态寄存器) 是 SysTick 的"控制面板",STM32 标准库通过 SysTick_Config() 函数间接操作它:
|--------|-------------|--------------------------------------------------------------------------|
| 位 | 名称 | 功能说明 |
| [16] | COUNTFLAG | 到期标志。当 VAL(当前计数值寄存器) 从 1 减到 0 时硬件自动置 1,读该位或写 VAL(当前计数值寄存器) 会清零 |
| [2] | CLKSOURCE | 时钟源选择:0 = HCLK/8,1 = HCLK |
| [1] | TICKINT | 中断使能:0 = 禁用中断(裸机轮询模式),1 = 使能中断(RTOS 模式) |
| [0] | ENABLE | 定时器使能:0 = 停止,1 = 启动 |
3. LOAD(重装载值寄存器) 与 VAL(当前计数值寄存器) 的关系
LOAD(重装载值寄存器) 是预设值,VAL(当前计数值寄存器) 是当前值。使能定时器后:
- 硬件自动将
LOAD(重装载值寄存器)的值加载到VAL(当前计数值寄存器)。- 每个时钟周期
VAL(当前计数值寄存器)减1。VAL(当前计数值寄存器)减到0时,COUNTFLAG置1,同时LOAD(重装载值寄存器)的值再次加载到VAL(当前计数值寄存器)。- 写
VAL(当前计数值寄存器)寄存器会立即清零COUNTFLAG并重新从新值开始计数。
单次计数的配置:LOAD(重装载值寄存器) = N(N 个时钟周期后置标志)。连续计数的配置:LOAD(重装载值寄存器) = N - 1(因为自动重载后 VAL(当前计数值寄存器) 从 N 开始递减,实际首次是 N 个周期,后续都是 N-1 个周期)。
4. 初始化 SysTick
初始化过程非常简单,核心就是配置时钟源并计算微秒/毫秒因子:
c
uint32_t fac_us = 9; /*CN:微秒因子,HCLK/8=9MHz时每微秒9个时钟--EN:Microsecond factor, 9 clocks per μs when HCLK/8=9MHz*/
uint32_t fac_ms = 9000; /*CN:毫秒因子,HCLK/8=9MHz时每毫秒9000个时钟--EN:Millisecond factor, 9000 clocks per ms when HCLK/8=9MHz*/
/**
* Function: SysTick_Init
* Description: CN:初始化SysTick定时器,选择时钟源并计算延时因子--EN:Initialize SysTick timer, select clock source and calculate delay factors
* Parameters: SysTick_CLKSource - CN:时钟源选择,SysTick_CLKSource_HCLK_Div8 或 SysTick_CLKSource_HCLK--EN:Clock source, SysTick_CLKSource_HCLK_Div8 or SysTick_CLKSource_HCLK
* Return VAL(当前计数值寄存器)ue:无
*/
void SysTick_Init(uint32_t SysTick_CLKSource)
{
SysTick->CTRL(控制与状态寄存器) = 0; /*CN:清除所有标志,选择HCLK/8时钟,禁用中断,关闭定时器--EN:Clear all flags, select HCLK/8 clock, disable interrupt, stop timer*/
if (SysTick_CLKSource_HCLK == SysTick_CLKSource) /*CN:如果选择HCLK不分频--EN:If HCLK selected (no division)*/
{
SysTick->CTRL(控制与状态寄存器) |= SysTick_CLKSource_HCLK; /*CN:时钟源设为HCLK(72MHz)--EN:Set clock source to HCLK (72MHz)*/
fac_us = 72; /*CN:72MHz时钟,每微秒72个时钟周期--EN:72MHz clock, 72 cycles per μs*/
fac_ms = 72000; /*CN:72MHz时钟,每毫秒72000个时钟周期--EN:72MHz clock, 72000 cycles per ms*/
}
else /*CN:否则选择HCLK/8--EN:Else select HCLK/8*/
{
fac_us = 9; /*CN:9MHz时钟,每微秒9个时钟周期--EN:9MHz clock, 9 cycles per μs*/
fac_ms = 9000; /*CN:9MHz时钟,每毫秒9000个时钟周期--EN:9MHz clock, 9000 cycles per ms*/
}
}
四、硬件实现:SysTick 功能框图
1. 功能框图
SysTick 的内部结构极其精简------时钟源选择器 → 24 位向下计数器(VAL(当前计数值寄存器))→ 零检测器 → COUNTFLAG 标志和中断请求。没有预分频器(因为 CLKSOURCE 已经提供了 HCLK 和 HCLK/8 两个选项),没有捕获/比较通道。它是 Cortex-M 内核里最简单、最纯粹的一个定时器。

2. 特殊机制:VAL(当前计数值寄存器) 写操作即时生效
往 VAL(当前计数值寄存器) 写入任意值会立即产生两个效果:COUNTFLAG 被硬件清零,同时定时器从新值开始向下计数。这意味着如果你在延时中途写了 VAL(当前计数值寄存器),前一段进度直接作废。这是裸机延时函数开头总要写 SysTick->VAL(当前计数值寄存器) = 0 的原因------确保计时从零开始,不受上一轮遗留值影响。
五、实践指南:用 SysTick 实现毫秒和微秒延时
1. 配置流程三步走
SysTick 的配置是所有外设中最简洁的之一:
- 关定时器,清标志 :写
CTRL(控制与状态寄存器) = 0,确保干净启动。- 选时钟源,设因子 :根据精度需求选
HCLK或HCLK/8。- 填
LOAD(重装载值寄存器),清VAL(当前计数值寄存器),启动 :写入目标计数值,清零当前计数器,拉高ENABLE位。
2. 以 72MHz HCLK 为例实现毫秒延时
(1) 参数确定
- 时钟源:
HCLK(72MHz,不分频),微秒因子fac_us = 72,毫秒因子fac_ms = 72000。LOAD(重装载值寄存器)最大值:0x00FFFFFF = 16,777,215。- 单轮最大延时:
16,777,215 / 72,000 ≈ 233ms。超过233ms需分段循环。
(2) 初始化代码
初始化函数调用一次即可,后续 fac_us 和 fac_ms 全局生效:
c
SysTick_Init(SysTick_CLKSource_HCLK); /*CN:选择HCLK不分频,72MHz,最高精度--EN:Select HCLK no division, 72MHz, highest precision*/
(3) 毫秒延时函数
用到的库函数/寄存器操作 API 及其功能说明如下:
|---------------------------|------------------------------------------------------|
| 函数名/寄存器操作 | 功能说明 |
| SysTick->CTRL(控制与状态寄存器) | 读写 SysTick 的控制/状态寄存器(位0=使能, 位1=中断, 位2=时钟源, 位16=到期标志) |
| SysTick->LOAD(重装载值寄存器) | 写入定时器重装载值(24 位有效,取值范围 1~16,777,215) |
| SysTick->VAL(当前计数值寄存器) | 写入任意值可清零当前计数值和 COUNTFLAG 标志 |
c
/**
* Function: SysTick_Delay_Ms
* Description: CN:SysTick毫秒延时,处理计数值超过24位上限的溢出情况--EN:SysTick millisecond delay, handles overflow when count exceeds 24-bit limit
* Parameters: ms - CN:延时的毫秒数--EN:Delay in milliseconds
* Return VAL(当前计数值寄存器)ue:无
*/
void SysTick_Delay_Ms(uint32_t ms)
{
uint32_t total = ms * fac_ms; /*CN:总时钟周期数--EN:Total clock cycles*/
uint32_t max_VAL(当前计数值寄存器) = 0x00FFFFFF; /*CN:24位最大值16,777,215--EN:24-bit max VAL(当前计数值寄存器)ue 16,777,215*/
uint32_t count = total / max_VAL(当前计数值寄存器); /*CN:需要按最大值计数的轮数--EN:Number of full max-VAL(当前计数值寄存器)ue rounds*/
uint32_t remain = total % max_VAL(当前计数值寄存器); /*CN:最后不满一轮的剩余计数值--EN:Remaining count after full rounds*/
SysTick->CTRL(控制与状态寄存器) &= ~((1 << 16) | 0x3); /*CN:清除到期标志,关闭定时器--EN:Clear count flag, disable timer*/
if (count) /*CN:需要多次按最大值计数--EN:Need multiple max-VAL(当前计数值寄存器)ue rounds*/
{
SysTick->LOAD(重装载值寄存器) = max_VAL(当前计数值寄存器); /*CN:加载24位最大值--EN:LOAD(重装载值寄存器) 24-bit max VAL(当前计数值寄存器)ue*/
SysTick->VAL(当前计数值寄存器) = 0; /*CN:清零当前计数值--EN:Clear current VAL(当前计数值寄存器)ue*/
SysTick->CTRL(控制与状态寄存器) |= 0x1; /*CN:启动定时器--EN:Start timer*/
while (count--)
{
while (!(SysTick->CTRL(控制与状态寄存器) & (1 << 16))); /*CN:等待一轮到期--EN:Wait for one round to expire*/
SysTick->CTRL(控制与状态寄存器) &= ~(1 << 16); /*CN:写该位清零--EN:Write to clear flag*/
}
SysTick->CTRL(控制与状态寄存器) &= ~0x1; /*CN:停止定时器--EN:Stop timer*/
}
if (remain) /*CN:处理剩余不满一轮的计数值--EN:Handle remaining count*/
{
SysTick->LOAD(重装载值寄存器) = remain; /*CN:加载剩余值--EN:LOAD(重装载值寄存器) remaining VAL(当前计数值寄存器)ue*/
SysTick->VAL(当前计数值寄存器) = 0;
SysTick->CTRL(控制与状态寄存器) |= 0x1;
while (!(SysTick->CTRL(控制与状态寄存器) & (1 << 16)));
SysTick->CTRL(控制与状态寄存器) &= ~((1 << 16) | 0x1); /*CN:清除标志并停止定时器--EN:Clear flag and stop timer*/
}
}
以延时 5000ms、HCLK=72MHz 为例,总时钟数 = 5000 × 72000 = 360,000,000。单轮最大 16,777,215,需跑 360,000,000 / 16,777,215 ≈ 21 轮完整最大值,再加一轮剩余值 7,678,485。这就是 count 和 remain 的由来。
(4) 微秒延时函数
c
/**
* Function: SysTick_Delay_Us
* Description: CN:SysTick微秒延时,适用于小于1864ms的短延时(无溢出处理)--EN:SysTick microsecond delay, for short delays under 1864ms (no overflow handling)
* Parameters: us - CN:延时的微秒数,注意不要超过 16,777,215 / fac_us--EN:Delay in μs, not exceeding 16,777,215 / fac_us
* Return VAL(当前计数值寄存器)ue:无
*/
void SysTick_Delay_Us(uint32_t us)
{
SysTick->CTRL(控制与状态寄存器) &= ~((1 << 16) | 0x3); /*CN:清除到期标志,关闭定时器--EN:Clear count flag, disable timer*/
SysTick->LOAD(重装载值寄存器) = us * fac_us; /*CN:计算并加载总时钟数--EN:Calculate and LOAD(重装载值寄存器) total clock cycles*/
SysTick->VAL(当前计数值寄存器) = 0; /*CN:清零当前计数值--EN:Clear current count VAL(当前计数值寄存器)ue*/
SysTick->CTRL(控制与状态寄存器) |= 0x1; /*CN:启动定时器--EN:Start timer*/
while (!(SysTick->CTRL(控制与状态寄存器) & (1 << 16))); /*CN:等待到期标志置1--EN:Wait for count flag to be set*/
SysTick->CTRL(控制与状态寄存器) &= ~((1 << 16) | 0x1); /*CN:清除标志并关闭定时器--EN:Clear flag and stop timer*/
}
注意事项:us × fac_us 不能超过 16,777,215,否则溢出。HCLK 不分频时 fac_us = 72,单次最大延时 16,777,215 / 72 ≈ 233,000μs ≈ 233ms。延时超过这个值请调用毫秒版本的 SysTick_Delay_Ms。
六、多定时器配合:SysTick + TIM 的典型分工
裸机项目中 SysTick 通常只干一件事------做 delay_ms/delay_us。而 TIM 则负责 PWM 输出、输入捕获、编码器模式等复杂功能。两者的典型分工如下:
|------------|-------------------|--------------------------------|
| 定时器 | 职责 | 理由 |
| SysTick | 延时函数、系统心跳 | 配置简单,可直接轮询 COUNTFLAG,不占用中断资源 |
| TIM1 | 高级 PWM、互补输出、刹车 | 自带死区、刹车、互补通道 |
| TIM2/3/4 | 通用 PWM、输入捕获、编码器 | 功能全面,通道多,适合各类通用场景 |
| TIM6/7 | 精简单次或周期中断 | 基本定时器,无输入输出引脚,极致精简 |
中断优先级方面:SysTick 的中断号是 -1(SysTick_IRQn),几乎是最高的内核异常之一。RTOS 中 SysTick 中断通常设最高抢占优先级,确保任务调度不被外设中断阻塞;裸机中如果只用来延时,一般不开中断,靠轮询解决。
七、任意平台纯软件延时 vs 硬件 SysTick
如果 MCU 没有 SysTick(比如非 Cortex-M 内核),可以用 NOP 汇编宏模拟一个简单的阻塞延时。原理是数 NOP 个数,但在高频 CPU 上需要精确计算指令周期。
c
/*========================================*/
/*CN:内嵌汇编指令宏--EN:Inline assembly instruction macros*/
/*========================================*/
#define DISI() _asm {disi} /*CN:关总中断--EN:Disable global interrupt*/
#define WDTC() _asm {wdtc} /*CN:清看门狗--EN:Clear watchdog timer*/
#define SLEP() _asm {slep} /*CN:进入休眠模式--EN:Enter sleep mode*/
#define NOP() _asm {nop} /*CN:空操作,单周期延时--EN:No operation, single-cycle delay*/
#define ENI() _asm {eni} /*CN:开总中断--EN:Enable global interrupt*/
1. 核心思想
纯软件延时的本质就是用 for 循环包 NOP 指令,用指令周期数凑延时时间。在 16MHz 主频的 MCU 上,一个 NOP 约 62.5ns,16 个约 1μs,16000 个约 1ms。但任何中断打断都会让 NOP 计数不准,软延时只适合关中断的极短微秒级场景。
2. 纯软件微秒延时
c
/**
* Function: Soft_Delay_Us
* Description: CN:纯软件微秒延时,16MHz下约16个NOP=1μs--EN:Pure software μs delay, ~16 NOPs=1μs at 16MHz
* Parameters: us - CN:延时的微秒数--EN:Delay in μs
* Return VAL(当前计数值寄存器)ue:无
*/
void Soft_Delay_Us(uint16_t us)
{
DISI(); /*CN:关中断,保护时序--EN:Disable interrupt to protect timing*/
while (us--)
{
uint8_t i;
for (i = 0; i < 16; i++) /*CN:16个NOP≈1μs@16MHz--EN:16 NOPs≈1μs@16MHz*/
{
NOP();
}
}
ENI();
}
3. 软件方式 vs 硬件 SysTick
|----------|--------------------------|---------------------------|
| 维度 | 硬件 SysTick | 纯软件 NOP 延时 |
| 精度 | 时钟级精度(13.9ns @72MHz) | 受指令周期、中断打断影响,误差大 |
| CPU 占用 | 轮询模式下 CPU 仍被占满,但可用中断解放 | CPU 全阻塞,无法任何其他工作 |
| 跨平台移植 | Cortex-M 系列地址一致,直接复用 | 需按主频重新校准 NOP 个数 |
| 适用场景 | 所有 Cortex-M 裸机与 RTOS | 非 Cortex-M 内核、无任何硬件定时器时 |
八、提高延时精度与稳定性的技巧
1. 延时期间关总中断
微秒级延时对时序极其敏感,如果延时过程中被中断打断,实际延时会拉长。关键通信时序(如软件模拟 SPI 的时钟边沿)必须在 DISI()/ENI() 保护下执行。毫秒级延时一般不需要,留给系统响应其他中断。
2. 使用 SysTick_Config 中断模式替代轮询
裸机中的 while(!(SysTick->CTRL(控制与状态寄存器) & (1<<16))) 让 CPU 空转等标志,白白浪费处理能力。更推荐用中断模式------SysTick_Config() 使能中断,在 SysTick_Handler 里只做全局 tick 计数加一,delay_ms 改成挂起等待 tick 到达目标值,CPU 在此期间可以去执行其他逻辑或进入休眠。
3. 校准因子按实际频率重算
如果你在 SystemInit 中修改了系统时钟频率(比如用内部 HSI 降频到 8MHz),fac_us 和 fac_ms 必须同步重算,否则所有 delay_ms 都是错的。不放心的话可以在初始化里用宏自动根据 SystemCoreClock 计算,避免手动改出 bug。
九、SysTick这里为什么不初始化GPIO?

这是一个非常好的问题!你观察得非常仔细。
核心答案:SysTick 是内核内部 的定时器,不需要也不存在 GPIO 引脚
SysTick 和 SPI、USART、GPIO 这些外设有一个根本性的区别:
| 外设类型 | 位置 | 是否需要 GPIO 初始化 |
原因 |
|---|---|---|---|
SPI、I2C、USART |
芯片外设总线(APB/AHB) |
✅ 需要 | 这些外设需要通过具体的芯片引脚与外部器件物理连接 |
GPIO 本身 |
芯片边界 | ✅ 需要 | 引脚就是 GPIO 的本质,必须配置方向、模式 |
SysTick |
Cortex-M 内核内部 |
❌ 不需要 | 它是一个纯内部计数器 ,没有任何外部引脚 |
图解:SysTick 在内核中的位置
下面是一个简化的 STM32 芯片内部框图,注意 SysTick 的位置:

为什么 SysTick 不需要 GPIO 初始化?三个根本原因
1. 物理上不存在引脚
GPIO 初始化本质上是在配置芯片引脚 的电气特性(推挽/开漏、上拉/下拉、速度等)。SysTick 既不输入也不输出任何信号到芯片外部,它只是内核内部的一个计数器,因此无引脚可配。
2. 时钟来源是内核时钟,不是外部引脚
SysTick 的时钟来自内部的 HCLK 或 HCLK/8,这是时钟树内部的信号线,不经过 GPIO 引脚。而 SPI 的 SCK、MOSI 等信号必须通过 GPIO 引脚输出到外部器件,所以需要初始化 GPIO 的复用功能。
3. 操作方式是对寄存器读写,不涉及引脚状态
所有对 SysTick 的操作都是通过读写 CTRL、LOAD、VAL 这几个内存映射寄存器完成的:
- 写
LOAD→ 设置倒计时初值 - 读
CTRL→ 查看计数到零标志 - 写
VAL→ 清零当前计数值
这些操作不经过任何 GPIO 数据寄存器,因此完全不需要 GPIO 初始化。
一个类比帮助你理解
| 外设 | 类比 | 是否需要"接线路"? |
|---|---|---|
SPI 从机 |
📬 邮局(需要收发信件的窗口) | ✅ 需要,窗口就是引脚 |
GPIO 按键 |
🔘 一个按钮(装在面板上) | ✅ 需要,按钮就是引脚 |
SysTick |
⏰ 手机里自带的倒计时闹钟 | ❌ 不需要,完全在手机内部软件运行,没有外接按钮或显示屏 |
所以,回到你的问题
SysTick 不初始化 GPIO,不是代码写漏了,而是因为它根本就不是一个"引脚型"外设。
在配置 SysTick 时,你需要配置的是:
- ✅ 时钟源(
CLKSOURCE) - ✅ 重装载值(
LOAD) - ✅ 是否产生中断(
TICKINT) - ✅ 使能定时器(
ENABLE)
但绝对不需要配置任何 GPIO_InitTypeDef 结构体或调用 GPIO_Init 函数。这是从物理架构上决定的,不是编程习惯问题。
验证一下:看 SysTick 的寄存器地址
SysTick 的寄存器映射在 0xE000E010 -- 0xE000E020 这个地址范围,这是 ARM 内核的系统控制空间(SCS) ,与 GPIO 外设的地址(0x40010800 附近)完全不同。这也从地址空间上证明了:GPIO 和 SysTick 是两套完全独立的硬件模块。
十、结语:内核自带的心跳,最轻量的时钟
SysTick 是 Cortex-M 内核送给你最简单的礼物------4 个寄存器、24 位倒计时、一条轮询标志位,就能让你的 delay_ms 从"大概"进化到"精确"。
下一次当你写下 delay_ms(1000),愿你能想起那个不停倒数的 VAL(当前计数值寄存器)、那个到期就举手报告的 COUNTFLAG,还有那行优雅的溢出分段------把 360,000,000 掰成 21 + 1 轮的数学之美。
扩展阅读:
Cortex-M3 Technical Reference Manual--SysTick章节PM0056:STM32F10xxxCortex-M3programming manual- 《The Definitive Guide to ARM Cortex-M3 and Cortex-M4 Processors》 -- Joseph Yiu