STM32(M4)入门:定时器延时与系统滴答(价值 3w + 的嵌入式开发指南)

第 1 章 延时:嵌入式系统的时间控制基石

1.1 延时基础:从概念到硬件实现

1.1.1 什么是延时?

定义 :延时是通过软件或硬件手段,使程序执行过程中暂停指定时间,再继续后续操作的技术。本质是对时间的精确或粗略控制,确保硬件时序、任务调度或通信协议的正确执行。
核心作用:让程序在特定时间点或时间段内等待,满足外设响应、任务协调、时序匹配等需求(如按键消抖、LED 闪烁、传感器初始化等待)。

1.1.2 为什么需要延时?

  1. 硬件稳定需求
    • 按键按下后需等待 5-20ms 消除机械抖动,避免误触发。
    • 外设上电后需等待初始化完成(如 LCD 屏幕背光稳定)。
  2. 周期性任务控制
    • 实现 LED 周期性闪烁(如 1 秒亮灭一次)。
    • 定时采集传感器数据(如每 10ms 读取一次温湿度)。
  3. 通信时序匹配
    • I2C/SPI 通信中需严格遵循时钟周期和信号保持时间。
    • UART 通信中波特率的精确时钟控制。

1.1.3 如何实现延时?两种核心方案

方案 1:软件延时(CPU 空转)

复制代码
// 简单毫秒级延时(依赖CPU主频,STM32F4 168MHz时,每循环约1μs)  
void delay_ms(uint32_t ms) {  
    for (; ms > 0; ms--) {  
        for (uint32_t i = 0; i < 168000; i++);  // 空循环消耗时间  
    }  
}  
  • 优点:实现简单,无需硬件配置。
  • 缺点
    • 阻塞 CPU:延时期间无法处理中断或其他任务,导致系统卡顿。
    • 精度低:受 CPU 主频影响,超频 / 降频时需重新校准。
    • 资源浪费:长延时占用大量 CPU 资源,效率低下。

方案 2:硬件定时器延时(推荐)

核心原理 :利用 STM32 内部定时器的计数功能,通过配置固定频率的时钟源和分频器,实现高精度、非阻塞式延时。
优势

  • 高精度:误差仅由时钟源稳定性决定(如 84MHz 晶振误差 < 0.1%)。
  • 释放 CPU:延时期间 CPU 可执行其他任务(如处理串口数据、刷新屏幕)。
  • 灵活扩展:通过调整分频系数和计数值,支持 μs/ms 级长延时。

1.2 STM32 定时器体系:分类与核心原理

1.2.1 定时器分类与选型

STM32F4 系列包含 3 类 15 个定时器,按功能划分为:

类型 功能特点 包含型号 延时相关特性
基本定时器 16 位,仅支持定时计数和 DAC 触发(STM32F401 无此功能) TIM6、TIM7 配置简单,适合纯延时场景(如 LED 闪烁)
通用定时器 16/32 位,支持输入捕获、输出比较、PWM 生成(4 个独立通道) TIM2~TIM5、TIM9~TIM11 可实现复杂时序控制(如 PWM 调光)
高级定时器 16 位,含互补 PWM 输出、死区控制、刹车功能(电机控制专用) TIM1、TIM8 支持电机驱动的精准时序控制

延时场景选型

  • 基础延时 :选择基本定时器(TIM7),因其功能简洁、资源占用少,只需配置定时计数功能即可满足需求。
  • 复杂场景:通用 / 高级定时器(如 TIM3 实现 PWM 呼吸灯,TIM1 控制电机转速)。

1.2.2 什么是定时器?

本质 :定时器是一个带时钟的计数器,通过对固定频率的时钟信号计数,将计数值转换为时间间隔。
核心组件

  1. 时钟源
    • 定时器 7(TIM7)的时钟源来自 APB1 总线,系统默认配置下:
      • 系统主频 168MHz,APB1 预分频 4→APB1 频率 42MHz。
      • 定时器自动倍频 ×2→84MHz 时钟源(关键:APB 预分频≠1 时,定时器时钟自动翻倍)。
  2. 预分频器(PSC)
    • 16 位寄存器,将 84MHz 时钟分频为更低频率(1~65536 分频)。
    • 例:PSC=8399(8400 分频)→ 计数频率 = 84MHz/8400=10kHz,单次计数周期 0.1ms。
  3. 计数器(CNT)
    • 16 位向上计数器,从 0 开始递增,到达自动重装载值(ARR)时溢出,触发更新事件。
  4. 自动重装载寄存器(ARR)
    • 设置计数器溢出阈值,决定单次定时周期(如 ARR=9 对应 10 次计数,即 1ms)。

关键公式

复制代码
计数周期(Tclk)= 1 / 时钟源频率 = 1/84MHz ≈ 0.0119μs  
分频后周期(Tpre)= Tclk × (PSC + 1)  // 分频值=PSC+1  
延时时间(Tdelay)= Tpre × (ARR + 1)  // 计数值=ARR+1(从0开始计数)  

1.2.3 定时器如何实现延时?

  1. 设定目标延时:例如需要 1ms 延时。
  2. 计算参数
    • 选择分频系数 8400(PSC=8399),得到计数周期 0.1ms。
    • 计算 ARR:1ms / 0.1ms / 次 = 10 次计数 → ARR=10-1=9(计数器从 0 开始)。
  3. 启动计数:计数器从 0 递增,到达 ARR=9 时溢出,触发延时完成标志。
  4. 检测溢出:通过标志位判断延时是否到达,清除标志位后可重复使用。

1.3 定时器 7 深度解析:从时钟源到计数逻辑

1.3.1 时钟源定位:揭秘 TIM7 的 "时间脉搏"

时钟源追踪:从系统时钟到定时器时钟

STM32 的时钟体系如同精密齿轮组,TIM7 的时钟源需从 RCC 时钟树逐层定位:

  1. 系统时钟(SYSCLK):作为整个系统的核心,默认频率 168MHz,为 AHB 总线、内核及存储器提供时钟。

  2. AHB 总线分频:系统时钟经 AHB 预分频器(默认 1 分频)直接驱动 AHB 总线,频率保持 168MHz。

  3. APB1 总线分频 :AHB 总线信号经 APB1 预分频器(文档中配置为 4 分频),得到 APB1 总线频率:

    复制代码
    APB1频率 = 系统时钟 / 预分频值 = 168MHz / 4 = 42MHz  
  4. 定时器时钟倍频 :STM32 硬件特性规定,当 APB 预分频值≠1 时,定时器时钟自动倍频 ×2(提升精度):

    复制代码
    TIM7时钟源 = APB1频率 × 2 = 42MHz × 2 = 84MHz  

关键结论 :TIM7 的时钟源最终为 84MHz,是 APB1 总线频率的 2 倍,这是理解后续计时计算的核心前提。

时钟源的作用:定义计数的 "最小时间单位"

  • 84MHz 意味着时钟周期为:

    复制代码
    时钟周期 = 1 / 84MHz ≈ 0.0119μs(即每0.0119微秒产生一个计数脉冲)  
  • 定时器的所有计时功能,本质是对这个高频脉冲的计数累积。

1.3.2 分频器机制:让高频时钟 "慢下来"

分频器工作原理:高频信号的 "减速器"

  • 分频概念:将 84MHz 的高频时钟按固定比例降低频率,例如 84 分频后,频率降至 1MHz(84MHz/84=1MHz)。
  • 实现方式 :通过定时器的预分频寄存器TIM7->PSC设置分频系数(0~65535),实际分频值为PSC+1
    • PSC=83时,分频值 = 84,对应 84 分频。
    • PSC=8399时,分频值 = 8400,对应 8400 分频。
  • 核心作用:降低计数频率,使单次计数周期变长,从而适配不同延时精度需求。

分频前后对比:从 μs 级到 ms 级的灵活切换

分频配置 分频值 计数频率 单次计数周期 最大计时时间(16 位计数器)
默认配置 84 1MHz(84MHz/84) 1μs 65536μs=65.536ms
加大分频 8400 10kHz(84MHz/8400) 0.1ms 65536×0.1ms=6.5536s

分频系数计算公式:

复制代码
计数频率 = 时钟源频率 / 分频值 = 84MHz / (PSC+1)  
单次计数周期 = 1 / 计数频率 = (PSC+1) / 84MHz  

示例计算

  • PSC=83(84 分频):

    复制代码
    计数频率 = 84MHz / 84 = 1MHz,单次计数周期=1μs  
  • PSC=8399(8400 分频):

    复制代码
    计数频率 = 84MHz / 8400 = 10kHz,单次计数周期=0.1ms  

1.3.3 最大计时时间扩展:在精度与范围间找到平衡

为什么需要扩展计时范围?

  • 默认 84 分频时,最大计时仅 65.536ms,无法满足 LED 闪烁(1s)、传感器采集(10ms)等常见需求。
  • 通过加大分频系数,可显著延长计时范围,例如 8400 分频时最大计时达 6.5536s,覆盖多数基础延时场景。

扩展方法:调整 PSC 寄存器

  1. 步骤 1:确定目标计数频率

    • 若需要 0.1ms 的单次计数周期(10kHz 频率),则分频值 = 84MHz / 10kHz = 8400,对应PSC=8399
  2. 步骤 2:计算最大计时时间

    复制代码
    最大计时时间 = 单次计数周期 × 最大计数值(65536)  
    = 0.1ms × 65536 = 6553.6ms = 6.5536s  
  3. 关键权衡

    • 分频系数↑:计数频率↓,单次计数周期↑,最大计时时间↑,但精度↓(如 0.1ms 精度 vs 1μs 精度)。
    • 分频系数↓:计数频率↑,单次计数周期↓,最大计时时间↓,但精度↑。

应用场景匹配:

  • μs 级高精度延时:选择小分频系数(如 84 分频),用于 I2C 通信的时序等待(需 μs 级精度)。
  • ms 级长延时:选择大分频系数(如 8400 分频),用于 LED 闪烁、按键消抖(允许 ms 级误差)。

1.3.4 实战:计算不同分频下的计时参数

案例 1:实现 100ms 延时(8400 分频)

  1. 确定参数

    • 目标延时 = 100ms,单次计数周期 = 0.1ms(10kHz 频率)。
    • 需计数次数 = 100ms / 0.1ms=1000 次,对应ARR=999(计数器从 0 开始)。
  2. 公式验证

    复制代码
    延时时间 = (ARR+1) × 单次计数周期 = 1000 × 0.1ms = 100ms  

案例 2:极限最大计时(8400 分频)

  • 最大计数值 = 65536,单次计数周期 = 0.1ms:

    复制代码
    最大计时=65536 × 0.1ms=6553.6ms≈6.55s  
  • 若需更长延时(如 10s),需结合软件循环或中断(见后续章节)。

1.3.5 分频器配置注意事项

  1. 寄存器范围
    • TIMx_PSC为 16 位寄存器,分频值范围 1~65536(对应 PSC=0~65535)。
  2. 时钟使能
    • 配置前需通过RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM7, ENABLE);使能 TIM7 时钟,否则分频配置无效。
  3. 动态调整
    • 可在运行中修改PSCARR寄存器,实现延时参数的动态切换(如按键调节 LED 闪烁频率)。

1.4 定时器 7 配置实战:三步实现精准延时

1.4.1 硬件准备

  • 开发板:STM32F407ZET6(TIM7 挂载 APB1 总线)。
  • 工具:Keil MDK + ST-Link。
  • 系统配置:默认 168MHz 主频,APB1 预分频 4,TIM7 时钟源 84MHz。

1.4.2 配置步骤详解(以 1s 延时为例)

步骤 1:开启定时器时钟

复制代码
#include "stm32f4xx_rcc.h"  
#include "stm32f4xx_tim.h"  

// 使能APB1总线下的TIM7时钟(必做!外设默认时钟关闭)  
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM7, ENABLE);  
  • RCC_APB1Periph_TIM7:指定操作 TIM7 的时钟控制寄存器。
  • 注意:未开启时钟时,定时器完全不工作。

步骤 2:初始化定时器参数

复制代码
// 定义定时器初始化结构体  
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;  

// 配置1s延时参数(84MHz→8400分频→10kHz,计数值10000次)  
TIM_TimeBaseStructure.TIM_Prescaler = 8399;       // 预分频值(8400-1)  
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;  // 向上计数模式  
TIM_TimeBaseStructure.TIM_Period = 9999;         // 自动重装载值(10000-1)  
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;  // 时钟不分频  

// 初始化TIM7  
TIM_TimeBaseInit(TIM7, &TIM_TimeBaseStructure);  
  • TIM_Prescaler:预分频系数,实际分频 = 值 + 1(8399→8400 分频)。
  • TIM_Period:重装载值,计数器到达此值时溢出(10000 次计数对应 1s)。

步骤 3:启动定时器并检测溢出

复制代码
while (1) {  
    LED1 ^= 1;  // 翻转LED状态(假设LED1连接到GPIOA5)  

    // 启动定时器  
    TIM_Cmd(TIM7, ENABLE);  
    // 等待溢出标志位(TIM_FLAG_Update)置1(轮询检测)  
    while (TIM_GetFlagStatus(TIM7, TIM_FLAG_Update) == RESET);  
    // 清除溢出标志(否则下次会立即触发)  
    TIM_ClearFlag(TIM7, TIM_FLAG_Update);  
    // 停止定时器(非必要,可保持运行以减少重复配置)  
    TIM_Cmd(TIM7, DISABLE);  
}  
  • TIM_GetFlagStatus:检测定时器溢出标志,返回 SET(已溢出)或 RESET(未溢出)。
  • TIM_ClearFlag:必须手动清除标志位,避免重复检测导致延时失效。

1.4.3 封装通用 ms 级延时函数

复制代码
/**  
  * @brief  TIM7延时函数(支持ms级任意延时)  
  * @param  ms:延时毫秒值(范围:0~65535,受16位计数器限制)  
  * @note   预分频固定为8400(8399),单次计数周期0.1ms  
  */  
void TIME7_Delay(u32 ms) {  
    // 计算重装载值:ms × 10次计数/ms - 1(计数器从0开始)  
    uint16_t reload = ms * 10 - 1;  
    TIM_SetAutoreload(TIM7, reload);  // 设置自动重装载值  
    TIM_Cmd(TIM7, ENABLE);  // 启动定时器  
    while (TIM_GetFlagStatus(TIM7, TIM_FLAG_Update) == RESET);  // 等待溢出  
    TIM_ClearFlag(TIM7, TIM_FLAG_Update);  // 清除标志位  
    TIM_Cmd(TIM7, DISABLE);  // 停止定时器(可选)  
}  
  • 参数范围:因 TIM7 是 16 位计数器,最大计数值 65535,故最大延时为 65535×0.1ms=6553.5ms(约 6.55s)。
  • 灵活性 :通过修改reload值,可动态调整延时时间,无需重新初始化定时器。

1.5 常见问题与避坑指南

1.5.1 延时精度误差大?

  • 原因 1 :时钟源计算错误
    • 未考虑 APB 预分频后的倍频机制(APB1=42MHz 时,TIM7 时钟自动 ×2→84MHz)。
  • 原因 2 :预分频值 / 重装载值未减 1
    • 计数器从 0 开始计数,实际计数值 = 寄存器值 + 1(如 ARR=9 对应 10 次计数)。
  • 解决 :严格按公式计算:ARR = (目标时间 / 计数周期) - 1

1.5.2 标志位不触发?

  • 排查步骤
    1. 是否调用TIM_Cmd(ENABLE)启动定时器(未启动则计数器不工作)。
    2. 标志位类型是否正确:延时检测用TIM_FLAG_Update,而非中断标志TIM_IT_Update
    3. 是否在初始化时遗漏时钟使能(RCC 配置是否正确)。

1.5.3 多定时器冲突?

  • 原则:不同定时器挂载于不同总线(APB1/APB2),可同时工作,但需避免高频溢出导致 CPU 负载过高。
  • 优化:非关键延时用阻塞式(如 LED 闪烁),关键任务用中断式(如传感器数据采集)。

1.6 知识总结与学习路径

1.6.1 核心知识点图谱

1.6.2 实战建议

  1. 基础练习
    • 编写 TIM7 延时函数,实现 LED 1s 闪烁(结合 GPIO 输出)。
    • 测试不同分频系数对延时的影响(如 84 分频实现 65ms 延时,8400 分频实现 6.5s 延时)。
  2. 进阶应用
    • 用 TIM7 实现按键消抖(延时 10ms 后检测按键电平)。
    • 结合通用定时器(如 TIM3)实现 PWM 调光,理解定时器的输出比较功能。
  3. 调试工具
    • 使用 STM32CubeMX 图形化配置 TIM7,对比手动代码配置的差异。
    • 通过串口打印延时时间,验证精度(如延时 1s 后打印 "Delay 完成")。

通过掌握定时器 7 的原理与配置,你将建立嵌入式时间控制的核心能力,为后续学习 PWM、输入捕获等高级功能奠定基础。下一章将深入探讨系统滴答定时器(SysTick),实现更高效的 μs 级延时与系统心跳功能。

第二章:系统滴答(SysTick)------ 内核级延时与心跳定时器

2.1 系统滴答基础:Cortex-M 内核的 "心跳引擎"

2.1.1 什么是 SysTick?

定义:SysTick(系统滴答定时器)是 Cortex-M 内核集成的 24 位向下计数器,专为实时操作系统(RTOS)和精准延时设计,支持 μs/ms 级高精度定时,是 STM32 嵌入式开发的核心时间管理工具。

核心特性

  • 内核级外设:直接由 Cortex-M 内核控制,独立于 STM32 片上外设,兼容性强(所有 Cortex-M4 芯片均支持)。
  • 双时钟源:可选择系统时钟(SYSCLK,168MHz)或外部时钟(HCLK/8,21MHz),平衡精度与最大计时范围。
  • 低功耗模式:支持在睡眠模式下运行,适合电池供电设备。

2.1.2 为什么选择 SysTick?

对比项 SysTick 基本定时器(如 TIM7)
位数 24 位(最大计数值 16777216) 16 位(最大计数值 65536)
时钟源 系统时钟 / 外部时钟(可选) APB1 总线倍频后时钟(固定 84MHz)
中断支持 内置系统滴答中断(SysTick_IRQ) 需要配置 NVIC 中断控制器
典型场景 精准短延时、RTOS 心跳时钟 长延时、周期性任务触发
代码复杂度 仅操作 3 个寄存器,简单高效 需要初始化结构体,配置步骤较多

结论 :SysTick 适合μs/ms 级精准短延时系统级时间管理(如 RTOS 任务调度),而 TIM7 更适合 ms 级长延时(见第一章)。

2.2 关键寄存器:SysTick 的 "时间控制中心"

2.2.1 控制寄存器(SysTick->CTRL)

位域 功能 操作示例
BIT0(ENABLE) 定时器使能位:1 = 启动,0 = 停止 `SysTick->CTRL= 1<<0;`(启动)
BIT1(TICKINT) 中断使能位:1 = 允许溢出时产生中断 `SysTick->CTRL= 1<<1;`(使能中断)
BIT2(CLKSOURCE) 时钟源选择:0 = 外部时钟(HCLK/8=21MHz),1 = 系统时钟(SYSCLK=168MHz) SysTick->CTRL &= ~(1<<2);(选择 21MHz)
BIT16(COUNTFLAG) 溢出标志位:计数器从 LOAD 递减到 0 时置 1,需软件清除 if (SysTick->CTRL & (1<<16)) { ... }(检测溢出)

2.2.2 重装载寄存器(SysTick->LOAD)

  • 作用:设置计数器初始值,决定定时周期(向下计数到 0 时溢出)。
  • 范围 :0~16777215(24 位),对应最大定时时间:
    • 系统时钟 168MHz 时:16777216 / 168MHz ≈ 99.86ms
    • 外部时钟 21MHz 时:16777216 / 21MHz ≈ 798.9ms(更适合长延时)

2.2.3 当前值寄存器(SysTick->VAL)

  • 作用:实时显示当前计数值(从 LOAD 递减到 0)。
  • 特性 :读取时返回当前值,写入任意值会立即重置计数器(常用SysTick->VAL = 0;清空计数)。

2.3 三步配置法:从寄存器到精准延时

2.3.1 步骤 1:选择时钟源(关键!影响精度和范围)

复制代码
// 选项1:系统时钟(168MHz,精度高,适合μs级延时)  
SysTick->CTRL |= (1 << 2);  // CLKSOURCE=1,选择系统时钟  
// 选项2:外部时钟(21MHz,计时范围更长,适合ms级延时)  
SysTick->CTRL &= ~(1 << 2); // CLKSOURCE=0,选择HCLK/8=21MHz  
  • 时钟源计算
    • 系统时钟(SYSCLK):默认 168MHz,来自 PLL 锁相环,精度最高(误差 < 0.1%)。
    • 外部时钟(HCLK/8):HCLK=AHB 总线时钟 = 168MHz,分频后 21MHz,最大计时范围扩展 8 倍。

2.3.2 步骤 2:设置重装载值(核心公式推导)

目标:实现 1ms 延时(以系统时钟 168MHz 为例)。

  1. 计算计数值

    复制代码
    计数值 = 系统时钟频率 × 目标时间 = 168MHz × 1ms = 168000  
  2. 写入 LOAD 寄存器

    复制代码
    SysTick->LOAD = 168000;  // 168000次计数对应1ms(168MHz时钟)  

通用公式

复制代码
计数值 = 时钟源频率 × 延时时间  
例如:延时t微秒 → 计数值 = 时钟源频率(MHz) × t(μs)  

2.3.3 步骤 3:启动定时器并检测溢出

复制代码
// 清空当前计数值(可选,确保从0开始计数)  
SysTick->VAL = 0;  
// 启动定时器  
SysTick->CTRL |= (1 << 0);  
// 等待溢出(检测COUNTFLAG位,位16)  
while (!(SysTick->CTRL & (1 << 16)));  
// 停止定时器(非必要,可保持运行用于连续定时)  
SysTick->CTRL &= ~(1 << 0);  
  • 注意:COUNTFLAG 位在溢出后一直保持 1,需通过软件检测,无需手动清除(写入 VAL 寄存器会自动清除)。

2.4 封装通用延时函数:从 μs 到 ms 级全覆盖

2.4.1 系统初始化函数:SysTick_Init

复制代码
#include "SysTick.h"  

float fck_us;  // 微秒级计数参数(21MHz下为21,即1μs=21次计数)  
float fck_ms;  // 毫秒级计数参数(21MHz下为21000,即1ms=21000次计数)  

/**  
 * @brief  系统滴答初始化(默认外部时钟21MHz)  
 * @param  CLK 系统主频(如168MHz)  
 * @note   自动选择外部时钟源(CLK/8),计算微秒/毫秒计数参数  
 */  
void SysTick_Init(u32 CLK) {  
    // 选择外部时钟源(CLK/8=21MHz,当CLK=168MHz时)  
    SysTick->CTRL = 0;  // 复位控制寄存器,默认0=外部时钟源  
    
    // 计算计数参数(核心公式:计数值=时钟频率×时间)  
    fck_us = (float)CLK / 8.0;       // 1μs对应的计数值(21MHz→21次/μs)  
    fck_ms = fck_us * 1000.0;        // 1ms对应的计数值(21000次/ms)  
}  
  • 初始化步骤
    1. 清零控制寄存器,选择外部时钟源
    2. 根据系统主频计算计数参数(关键:21MHz=168MHz/8)

2.4.2 微秒级延时函数:delay_us

复制代码
/**  
 * @brief  微秒级延时(阻塞式)  
 * @param  n 延时时间(μs),最大约798900μs(798ms)  
 * @note   基于外部时钟21MHz,1μs=21次计数  
 */  
void delay_us(u32 n) {  
    // 1. 设置重装载值(计数值=21次/μs × nμs)  
    SysTick->LOAD = (u32)(fck_us * n);  
    // 2. 清空计数器  
    SysTick->VAL = 0;  
    // 3. 启动定时器  
    SysTick->CTRL |= 1 << 0;  
    // 4. 等待溢出标志(COUNTFLAG位16置1)  
    while (!(SysTick->CTRL & (1 << 16)));  
    // 5. 停止定时器(可选,下次使用需重新启动)  
    SysTick->CTRL &= ~(1 << 0);  
}  

2.4.3 毫秒级延时函数:delay_ms

复制代码
/**  
 * @brief  毫秒级延时(支持超长延时,自动分段处理)  
 * @param  n 延时时间(ms),最大798ms  
 * @note   分段调用500ms子函数,避免单次计数值超过24位寄存器范围  
 */  
void delay_ms(u32 n) {  
    u32 remainder = n % 500;  // 计算剩余时间  
    u32 cycles = n / 500;      // 计算完整500ms周期数  
    
    // 1. 处理完整500ms周期  
    while (cycles--) {  
        delay_nms(500);  // 调用500ms子函数(见下方)  
    }  
    // 2. 处理剩余时间  
    if (remainder != 0) {  
        delay_nms(remainder);  
    }  
}  

/**  
 * @brief  500ms以内延时子函数(静态函数,内部调用)  
 * @param  n 延时时间(ms),n≤500  
 */  
static void delay_nms(u32 n) {  
    // 1. 设置重装载值(21000次/ms × n ms)  
    SysTick->LOAD = (u32)(fck_ms * n);  
    // 2. 清空计数器并启动  
    SysTick->VAL = 0;  
    SysTick->CTRL |= 1 << 0;  
    // 3. 等待溢出  
    while (!(SysTick->CTRL & (1 << 16)));  
    // 4. 停止定时器  
    SysTick->CTRL &= ~(1 << 0);  
}  
  • 分段策略
    由于 24 位寄存器最大计数值 16777216,21MHz 下最大延时约 798ms,通过分段 500ms 避免单次超限

2.5 实战案例:用 SysTick 实现 LED 呼吸灯(PWM 调光)

2.5.1 硬件连接

  • LED 正极接 PA5(GPIOA5),负极接 GND,串联 220Ω 电阻。
  • 通过改变 LED 亮灭时间比例(占空比)实现亮度渐变。

2.5.2 代码实现(核心逻辑)

复制代码
// 定义占空比数组(0~100,代表亮度百分比)  
uint8_t duty_cycle[101] = {0, 1, 2, ..., 100, 99, ..., 1, 0};  

int main() {  
    // 初始化GPIOA5为推挽输出  
    GPIO_InitTypeDef GPIO_InitStruct;  
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);  
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_5;  
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_OUT_PP;  
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_100MHz;  
    GPIO_Init(GPIOA, &GPIO_InitStruct);  

    while (1) {  
        for (int i = 0; i < 101; i++) {  
            // 点亮LED(高电平)  
            GPIO_SetBits(GPIOA, GPIO_Pin_5);  
            SysTick_Delay_us(duty_cycle[i] * 10);  // 亮的时间  
            // 熄灭LED(低电平)  
            GPIO_ResetBits(GPIOA, GPIO_Pin_5);  
            SysTick_Delay_us((100 - duty_cycle[i]) * 10);  // 灭的时间  
        }  
    }  
}  
  • 原理:通过 SysTick 精确控制 LED 亮灭时间,占空比从 0% 到 100% 再到 0% 循环,实现呼吸灯效果。

2.6 进阶应用:SysTick 作为系统心跳(RTOS 基础)

2.6.1 配置周期性中断(1ms 心跳)

复制代码
// 初始化SysTick中断(1ms周期,系统时钟168MHz)  
void SysTick_Init(void) {  
    // 设置计数值:168000次计数=1ms  
    SysTick->LOAD = 168000;  
    // 使能中断和系统时钟  
    SysTick->CTRL = (1 << 2) | (1 << 1) | (1 << 0);  
    // 配置中断优先级(可选,Cortex-M4默认优先级)  
    NVIC_SetPriority(SysTick_IRQn, 0);  
}  

// 中断服务函数(自动生成,需在startup文件中声明)  
void SysTick_Handler(void) {  
    static uint32_t tick = 0;  
    tick++;  // 每1ms递增,作为系统时间戳  
    if (tick % 1000 == 0) {  
        LED_Toggle();  // 每秒翻转LED状态  
    }  
}  
  • 应用场景
    • RTOS(如 FreeRTOS)用此中断实现任务调度(如每 1ms 切换一次任务)。
    • 记录系统运行时间(通过全局变量tick获取 ms 级时间戳)。

2.6.2 中断与轮询对比

模式 优点 缺点 适用场景
轮询 代码简单,无需中断配置 阻塞 CPU,无法处理其他任务 简单延时,无并发需求
中断 非阻塞,支持多任务处理 需配置 NVIC,代码较复杂 系统级时间管理,RTOS 任务调度

2.7 常见问题与避坑指南

2.7.1 延时不准确?

  • 原因 1 :时钟源选择错误
    • 系统时钟下,计数值 = 延时时间 (μs)× 系统时钟 (MHz),如 168MHz 下 1ms 需 168000 次计数,不可直接写固定值。
  • 原因 2 :寄存器溢出
    • 24 位计数器最大计数值 16777216,系统时钟下最大延时约 99.86ms,超过需分多次延时。

2.7.2 中断未触发?

  • 排查步骤
    1. 是否设置TICKINT位(SysTick->CTRL |= 1<<1)。
    2. 中断服务函数名是否正确(必须为SysTick_Handler,与启动文件匹配)。
    3. 计数值是否大于 0(LOAD=0 时定时器不会启动)。

2.7.3 低功耗模式下失效?

  • 解决方案
    • 在睡眠模式下,通过SysTick->CTRL |= 1<<3;使能睡眠模式下的定时器运行。
    • 避免在停机模式(Stop Mode)下使用 SysTick,需切换到唤醒时钟源。

2.8 知识总结与学习路径

2.8.1 核心知识点图谱

2.8.2 学习建议

  1. 基础练习
    • 编写 SysTick 延时函数,实现 LED 以 100μs 间隔快速闪烁,观察示波器波形验证精度。
    • 对比 SysTick 延时与 TIM7 延时的 CPU 占用率(通过串口打印空闲任务执行时间)。
  2. 进阶实践
    • 用 SysTick 中断实现一个简易任务调度器,每隔 50ms 执行一次 LED 翻转,每隔 100ms 打印一次日志。
    • 在低功耗模式下测试 SysTick 延时,验证睡眠模式下的计时准确性。
  3. 调试工具
    • 使用 STM32CubeMX 的图形化配置生成 SysTick 初始化代码,对比手动编写的差异。
    • 通过 Keil MDK 的寄存器视图实时监控 SysTick->VAL 的值,观察计数过程。

掌握 SysTick 后,你将拥有嵌入式系统的 "时间脉搏",无论是精准延时还是系统级任务调度都能游刃有余。下一章我们将进入中断。

相关推荐
普普通通的一名码农1 小时前
ESP32-S3 入门学习笔记(四):LED实验
笔记·单片机·学习
教练、我想打篮球1 小时前
03 基于 STM32 的温度控制系统
stm32·单片机·嵌入式硬件
电鱼智能的电小鱼2 小时前
EFISH-SBC-RK3588 —— 厘米级定位 × 旗舰算力 × 工业级可靠‌
linux·人工智能·嵌入式硬件·边缘计算
ltqshs2 小时前
STM32标准库和HAL库SPI发送数据的区别-即SPI_I2S_SendData()和HAL_SPI_Transmit()互换
stm32·单片机·嵌入式硬件
程序员JerrySUN2 小时前
驱动开发硬核特训 · Day 22(上篇): 电源管理体系完整梳理:I2C、Regulator、PMIC与Power-Domain框架
linux·驱动开发·嵌入式硬件
即安莉4 小时前
STM32 CAN通信 HAL库实战教程:从零到测试成功
stm32·单片机·嵌入式硬件
优信电子4 小时前
STM32 驱动 INA226 测量电流电压功率
stm32·单片机·嵌入式硬件
BW.SU4 小时前
单片机 + 图像处理芯片 + TFT彩屏 复选框控件
单片机·嵌入式硬件·gpu·ra8889·ra6809·液晶控制芯片·图形处理芯片
小智学长 | 嵌入式4 小时前
单片机-89C51部分:5、点亮LED
单片机·嵌入式硬件