4. 配置系统滴答定时器(SysTick):从寄存器到延时函数
系统滴答定时器,圈里人都叫它 SysTick。说白了,它就是 Cortex-M 内核自带的一个 24 位递减计数器。我刚开始接触 STM32 时,总觉得这玩意儿不就是个定时器嘛,有啥特别的?后来做项目多了才明白,它可是整个操作系统的“心跳”,也是我们做裸机延时最趁手的工具。
嗯,咱们这节课就把 SysTick 彻底吃透。从寄存器怎么配,到怎么写出好用的微秒级和毫秒级延时函数,一步到位。
4.1 SysTick 寄存器详解
SysTick 只有 4 个寄存器,不多,但个个关键。我习惯把它们分成三部分:控制寄存器、重装载寄存器、当前值寄存器,还有一个校准寄存器(这个用得少,但了解一下没坏处)。
| 寄存器 | 地址偏移 | 描述 | 复位值 |
|---|---|---|---|
| CTRL | 0x00 | 控制和状态寄存器 | 0x00000000 |
| LOAD | 0x04 | 重装载值寄存器 | 0x00000000 |
| VAL | 0x08 | 当前值寄存器 | 0x00000000 |
| CALIB | 0x0C | 校准值寄存器(只读) | 0x00000000 |
咱们重点看前三个。CTRL 寄存器里最常用的三个位:ENABLE(位 0)使能定时器,TICKINT(位 1)使能中断,CLKSOURCE(位 2)选择时钟源。这里有个坑,我踩过——CLKSOURCE 为 1 时用内核时钟(HCLK),为 0 时用 HCLK/8。不同系列默认值可能不一样,千万别想当然。
核心要点:SysTick 是 24 位计数器,最大值 0xFFFFFF(16,777,215)。重装载值写进 LOAD 寄存器后,计数器从 LOAD 值递减到 0,然后自动重装。VAL 寄存器读的时候返回当前计数值,写任何值都会清零。
4.2 实现微秒级延时函数
微秒级延时,说白了就是“数数”。不用中断,直接轮询 COUNTFLAG 位。我早期做的一个传感器驱动项目,要求时序精度在 5 微秒以内,就是用这个思路搞定的。
实现原理很简单:先把 SysTick 配好,然后根据要延时的微秒数算出需要计多少个时钟周期,写进 LOAD 寄存器,接着等计数器减到 0 就行。
// 微秒级延时(不使能中断,轮询方式)
void delay_us(uint32_t us)
{
uint32_t reload = SystemCoreClock / 1000000; // 1微秒需要的时钟周期数
uint32_t ticks = us * reload; // 总共需要计的数
// 配置 SysTick
SysTick->CTRL = 0; // 先关闭
SysTick->LOAD = ticks & 0x00FFFFFF; // 设置重装载值(24位)
SysTick->VAL = 0; // 清空当前值
SysTick->CTRL = 0x01; // 使能,内核时钟,不使能中断
// 等待计数完成
while ((SysTick->CTRL & 0x00010000) == 0); // 等待 COUNTFLAG 置位
SysTick->CTRL = 0; // 关闭 SysTick
}
注意:这个函数有个限制——延时时间不能超过 0xFFFFFF / (SystemCoreClock / 1000000) 微秒。比如 72MHz 主频下,最大延时约 233 微秒。超过这个值,就得用循环或者毫秒级延时了。
4.3 实现毫秒级延时函数
毫秒级延时,我建议用中断方式。为什么?因为不阻塞 CPU,你可以在中断里做其他事。比如我做过一个 LED 呼吸灯效果,主循环跑逻辑,SysTick 中断里更新 PWM 占空比,配合得天衣无缝。
实现思路:配置 SysTick 每 1 毫秒产生一次中断,在中断服务函数里递减一个全局变量。延时函数就等这个变量减到 0。
static volatile uint32_t delay_ms_count = 0;
// SysTick 中断服务函数
void SysTick_Handler(void)
{
if (delay_ms_count > 0)
{
delay_ms_count--;
}
}
// 毫秒级延时(中断方式)
void delay_ms(uint32_t ms)
{
delay_ms_count = ms;
while (delay_ms_count > 0);
}
// 初始化 SysTick,产生 1ms 中断
void SysTick_Init(void)
{
uint32_t reload = SystemCoreClock / 1000; // 1毫秒需要的时钟周期数
SysTick->CTRL = 0; // 先关闭
SysTick->LOAD = reload - 1; // 设置重装载值(注意减1)
SysTick->VAL = 0; // 清空当前值
SysTick->CTRL = 0x07; // 使能 + 内核时钟 + 使能中断
}
小技巧:LOAD 寄存器为什么要减 1?因为计数器从 LOAD 减到 0 需要 LOAD+1 个时钟周期。比如你想计 1000 个周期,LOAD 就写 999。这个细节我当年调试了半小时才想明白。
4.4 编写 delay.c 与 delay.h
好了,现在咱们把上面这些代码整理成规范的工程文件。我习惯把延时函数单独放一个模块,方便移植。你想想看,换个芯片或者改个主频,只需要改一个头文件里的宏定义就行。
delay.h 头文件
#ifndef __DELAY_H
#define __DELAY_H
#include "stm32f10x.h" // 根据芯片型号调整头文件
void SysTick_Init(void);
void delay_us(uint32_t us);
void delay_ms(uint32_t ms);
#endif
delay.c 源文件
#include "delay.h"
static volatile uint32_t delay_ms_count = 0;
void SysTick_Init(void)
{
uint32_t reload = SystemCoreClock / 1000;
SysTick->CTRL = 0;
SysTick->LOAD = reload - 1;
SysTick->VAL = 0;
SysTick->CTRL = 0x07;
}
void delay_us(uint32_t us)
{
uint32_t reload = SystemCoreClock / 1000000;
uint32_t ticks = us * reload;
if (ticks > 0x00FFFFFF)
{
ticks = 0x00FFFFFF; // 限制最大值
}
SysTick->CTRL = 0;
SysTick->LOAD = ticks & 0x00FFFFFF;
SysTick->VAL = 0;
SysTick->CTRL = 0x01;
while ((SysTick->CTRL & 0x00010000) == 0);
SysTick->CTRL = 0;
}
void delay_ms(uint32_t ms)
{
delay_ms_count = ms;
while (delay_ms_count > 0);
}
void SysTick_Handler(void)
{
if (delay_ms_count > 0)
{
delay_ms_count--;
}
}
嗯,这里有个细节我想强调一下:delay_us 函数里我加了最大值限制。为什么?因为如果传入的 us 太大,计算出来的 ticks 可能超过 24 位能表示的范围。我曾经在一个项目里没加这个限制,结果延时时间完全不对,排查了半天才发现是溢出问题。
使用建议:
- 在 main 函数最开始调用
SysTick_Init()完成初始化 - 微秒延时用于短时间、高精度场景(如传感器时序、通信协议)
- 毫秒延时用于一般场景(如 LED 闪烁、按键消抖)
- 如果同时使用两种延时,确保 SysTick 中断优先级设置合理
4.5 SysTick 工作流程总览
为了让你更直观地理解 SysTick 的工作流程,我画了一张图。从初始化到计数完成,每一步都清清楚楚。
这张图把整个流程串起来了。你写代码的时候,脑子里过一遍这个流程,基本不会出错。
4.6 避坑指南与经验分享
最后,我把自己这些年踩过的坑总结一下,希望能帮你少走弯路。
- 时钟源选择要明确:我曾经在一个项目里,从 F103 换到 F407,没注意 SysTick 时钟源配置,结果延时时间差了一倍。后来养成习惯,每次初始化都显式设置 CLKSOURCE。
- 中断优先级别设太高:SysTick 中断优先级如果设得比某些外设中断还高,可能会影响实时性。我一般设成最低优先级,或者用 PendSV 来处理。
- 微秒延时别在中断里用:这个函数是轮询的,会阻塞 CPU。如果在中断服务函数里调用,轻则影响其他中断响应,重则导致系统卡死。
- 多任务环境下注意重入:如果用了 RTOS,SysTick 通常被操作系统占用。这时候再用自己的延时函数,就会冲突。解决办法是用操作系统提供的延时 API。
调试小技巧:如果你发现延时不准,先检查 SystemCoreClock 这个变量对不对。很多新手移植代码时忘了更新这个值,导致所有延时都按错误的主频计算。我一般会在初始化后先跑一个 1 秒的延时,用示波器量一下 GPIO 翻转的周期,验证延时是否准确。
好了,SysTick 这块的内容就讲到这里。代码已经给你了,原理也讲透了,剩下的就是动手练。把 delay.c 和 delay.h 加到你的工程里,跑几个例子试试,感受一下微秒和毫秒延时的区别。嗯,实践出真知,动手吧。