74_SysTick滴答定时器中断

文章目录

  • 一、发展历史:操作系统催生的内核定时器
    • [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. 以 72MHz HCLK 为例实现毫秒延时)
      • [(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 的寄存器地址)
  • 十、结语:内核自带的心跳,最轻量的时钟

你可能每天都在用 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 减一,精度从"大概"变成了"确切"。 STM32F103SysTick 是一个 24 位向下计数器,时钟源可选 HCLK72MHz)或 HCLK/89MHz),每次计数到 0 时产生中断或置标志位,自动从 LOAD(重装载值寄存器) 寄存器重装载,周而复始。

3. 技术演进:从简单定时到系统心跳

年代 平台 定时器类型 核心特性
1990s 51 单片机 8/16 位向上计数 外设定时器,需手动配置中断
2000s ARM7TDMI 32 位外设定时器 向量中断,无内核定时器
2005s 至今 Cortex-M3/M4 24SysTick 内核定時器,所有 Cortex-M 统一地址
未来 Cortex-M55 64SysTick 支持虚拟化和安全扩展

二、工作原理:一个永远倒计时的数字沙漏

1. 最直白的比喻:沙漏

SysTick 就像一个倒计时沙漏

  • LOAD(重装载值寄存器) 寄存器:你设定沙漏的总沙子量(计数值)。
  • VAL(当前计数值寄存器) 寄存器:沙漏里当前剩余的沙子量(每时钟周期减少一粒)。
  • CTRL(控制与状态寄存器) 寄存器:沙漏的开关和设置面板。
  • 倒数到 0 :沙子漏完,沙漏自动翻转(从 LOAD(重装载值寄存器) 重新加载),同时举手报告"漏完了一次"。

每次沙子漏完,CTRL(控制与状态寄存器) 寄存器的第 16 位(COUNTFLAG)被置 1。你只需要读这个标志位,就知道延时到了没有。

2. 核心机制:硬件自动重载与标志轮询

SysTick 的工作流程总共三步:

  1. 硬件加载 :使能定时器后,硬件自动将 LOAD(重装载值寄存器) 寄存器的值拷贝到 VAL(当前计数值寄存器) 寄存器。
  2. 递减计数 :每个时钟周期 VAL(当前计数值寄存器)1,直到 VAL(当前计数值寄存器) 变为 0
  3. 置标志 + 重载VAL(当前计数值寄存器) 回到 0 的瞬间,CTRL(控制与状态寄存器) 的第 16 位(COUNTFLAG)置 1,同时如果定时器未停止,硬件再次将 LOAD(重装载值寄存器) 的值加载到 VAL(当前计数值寄存器),继续下一轮倒数。

你只需要在 while 循环里反复读 CTRL(控制与状态寄存器) 的第 16 位,读到 1 就表示一轮计数结束。如果配置了中断,读到 0CPU 自动跳到 SysTick_Handler

3. 关键特性一:24 位的限制与溢出处理

LOAD(重装载值寄存器)VAL(当前计数值寄存器) 寄存器只有 24 位有效,取值范围 0x00000001 ~ 0x00FFFFFF1 ~ 16,777,215)。以 HCLK=72MHzHCLK 不分频作为时钟源为例,一轮计时最多只能跑 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/81 = HCLK |
| [1] | TICKINT | 中断使能:0 = 禁用中断(裸机轮询模式),1 = 使能中断(RTOS 模式) |
| [0] | ENABLE | 定时器使能:0 = 停止,1 = 启动 |

3. LOAD(重装载值寄存器)VAL(当前计数值寄存器) 的关系

LOAD(重装载值寄存器) 是预设值,VAL(当前计数值寄存器) 是当前值。使能定时器后:

  • 硬件自动将 LOAD(重装载值寄存器) 的值加载到 VAL(当前计数值寄存器)
  • 每个时钟周期 VAL(当前计数值寄存器)1
  • VAL(当前计数值寄存器) 减到 0 时,COUNTFLAG1,同时 LOAD(重装载值寄存器) 的值再次加载到 VAL(当前计数值寄存器)
  • VAL(当前计数值寄存器) 寄存器会立即清零 COUNTFLAG 并重新从新值开始计数。

单次计数的配置:LOAD(重装载值寄存器) = NN 个时钟周期后置标志)。连续计数的配置: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 已经提供了 HCLKHCLK/8 两个选项),没有捕获/比较通道。它是 Cortex-M 内核里最简单、最纯粹的一个定时器。

2. 特殊机制:VAL(当前计数值寄存器) 写操作即时生效

VAL(当前计数值寄存器) 写入任意值会立即产生两个效果:COUNTFLAG 被硬件清零,同时定时器从新值开始向下计数。这意味着如果你在延时中途写了 VAL(当前计数值寄存器),前一段进度直接作废。这是裸机延时函数开头总要写 SysTick->VAL(当前计数值寄存器) = 0 的原因------确保计时从零开始,不受上一轮遗留值影响。

五、实践指南:用 SysTick 实现毫秒和微秒延时

1. 配置流程三步走

SysTick 的配置是所有外设中最简洁的之一:

  1. 关定时器,清标志 :写 CTRL(控制与状态寄存器) = 0,确保干净启动。
  2. 选时钟源,设因子 :根据精度需求选 HCLKHCLK/8
  3. LOAD(重装载值寄存器),清 VAL(当前计数值寄存器),启动 :写入目标计数值,清零当前计数器,拉高 ENABLE 位。

2. 以 72MHz HCLK 为例实现毫秒延时

(1) 参数确定

  • 时钟源:HCLK72MHz,不分频),微秒因子 fac_us = 72,毫秒因子 fac_ms = 72000
  • LOAD(重装载值寄存器) 最大值:0x00FFFFFF = 16,777,215
  • 单轮最大延时:16,777,215 / 72,000 ≈ 233ms。超过 233ms 需分段循环。

(2) 初始化代码

初始化函数调用一次即可,后续 fac_usfac_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*/
    }
}

以延时 5000msHCLK=72MHz 为例,总时钟数 = 5000 × 72000 = 360,000,000。单轮最大 16,777,215,需跑 360,000,000 / 16,777,215 ≈ 21 轮完整最大值,再加一轮剩余值 7,678,485。这就是 countremain 的由来。

(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 的中断号是 -1SysTick_IRQn),几乎是最高的内核异常之一。RTOSSysTick 中断通常设最高抢占优先级,确保任务调度不被外设中断阻塞;裸机中如果只用来延时,一般不开中断,靠轮询解决。

七、任意平台纯软件延时 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 上,一个 NOP62.5ns16 个约 1μs16000 个约 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_usfac_ms 必须同步重算,否则所有 delay_ms 都是错的。不放心的话可以在初始化里用宏自动根据 SystemCoreClock 计算,避免手动改出 bug

九、SysTick这里为什么不初始化GPIO?

这是一个非常好的问题!你观察得非常仔细。

核心答案:SysTick内核内部 的定时器,不需要也不存在 GPIO 引脚

SysTickSPIUSARTGPIO 这些外设有一个根本性的区别:

外设类型 位置 是否需要 GPIO 初始化 原因
SPII2CUSART 芯片外设总线(APB/AHB 需要 这些外设需要通过具体的芯片引脚与外部器件物理连接
GPIO 本身 芯片边界 需要 引脚就是 GPIO 的本质,必须配置方向、模式
SysTick Cortex-M 内核内部 不需要 它是一个纯内部计数器没有任何外部引脚

图解:SysTick 在内核中的位置

下面是一个简化的 STM32 芯片内部框图,注意 SysTick 的位置:

为什么 SysTick 不需要 GPIO 初始化?三个根本原因

1. 物理上不存在引脚

GPIO 初始化本质上是在配置芯片引脚 的电气特性(推挽/开漏、上拉/下拉、速度等)。SysTick 既不输入也不输出任何信号到芯片外部,它只是内核内部的一个计数器,因此无引脚可配

2. 时钟来源是内核时钟,不是外部引脚

SysTick 的时钟来自内部的 HCLKHCLK/8,这是时钟树内部的信号线,不经过 GPIO 引脚。而 SPISCKMOSI 等信号必须通过 GPIO 引脚输出到外部器件,所以需要初始化 GPIO 的复用功能。

3. 操作方式是对寄存器读写,不涉及引脚状态

所有对 SysTick 的操作都是通过读写 CTRLLOADVAL 这几个内存映射寄存器完成的:

  • 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 附近)完全不同。这也从地址空间上证明了:GPIOSysTick 是两套完全独立的硬件模块。

十、结语:内核自带的心跳,最轻量的时钟

SysTickCortex-M 内核送给你最简单的礼物------4 个寄存器、24 位倒计时、一条轮询标志位,就能让你的 delay_ms 从"大概"进化到"精确"。

下一次当你写下 delay_ms(1000),愿你能想起那个不停倒数的 VAL(当前计数值寄存器)、那个到期就举手报告的 COUNTFLAG,还有那行优雅的溢出分段------把 360,000,000 掰成 21 + 1 轮的数学之美。

扩展阅读:

  • Cortex-M3 Technical Reference Manual -- SysTick 章节
  • PM0056: STM32F10xxx Cortex-M3 programming manual
  • 《The Definitive Guide to ARM Cortex-M3 and Cortex-M4 Processors》 -- Joseph Yiu
相关推荐
2501_921960851 小时前
协同本体论 V4.2+:离散关系拓扑涌现连续时空几何的数值验证
数据结构·人工智能·重构
IT_陈寒1 小时前
Redis缓存击穿把我坑惨了,原来这样解决才靠谱
前端·人工智能·后端
科芯创展1 小时前
XZ4058B/C,20V,外置MOS,8.4V/8.7V开关充电芯片 宽范围电源电压:8.9V~20V-(电池充电电压:8.4V/8.7V)
c语言·开发语言
学习论之费曼学习法1 小时前
Agent记忆系统:让AI拥有长期记忆能力
数据库·人工智能·oracle
AI玫瑰助手1 小时前
Python流程控制:break与continue语句的区别与应用
开发语言·python·信息可视化
Bnews1 小时前
机器人轨迹定位设备推荐:高精度动作捕捉系统的科研价值与应用选择
人工智能·机器人
wuxinyan1231 小时前
工业级大模型学习之路012:RAG 零基础入门教程(第七篇):高级检索架构(解决分块不合理问题)
人工智能·学习·rag
pkowner1 小时前
若依分页问题及解决方法
java·前端·算法