SysTick寄存器(嘀嗒定时器实现延时)
delay.h
#ifndef __DELAY_H__
#define __DELAY_H__#include "sys.h"void delay_ms(uint32_t nms); /* 延时nms */
void delay_us(uint32_t nus); /* 延时nus */
void delay_s(uint32_t ns); /* 延时ns */
void HAL_Delay(uint32_t nms); /* 延时nms */#endif
delay.c:
#include "delay.h"/*** @brief 微秒级延时* @param nus 延时时长,范围:0~233015* @retval 无*/
void delay_us(uint32_t nus)
{uint32_t temp;SysTick->LOAD = 72 * nus; /* 设置定时器重装值 */SysTick->VAL = 0x00; /* 清空当前计数值 */SysTick->CTRL |= 1 << 2; /* 设置分频系数为1分频 */SysTick->CTRL |= 1 << 0; /* 启动定时器 */do{temp = SysTick->CTRL;} while ((temp & 0x01) && !(temp & (1 << 16))); /* 等待计数到0 */SysTick->CTRL &= ~(1 << 0); /* 关闭定时器 */
}/*** @brief 毫秒级延时* @param nms 延时时长,范围:0~4294967295* @retval 无*/
void delay_ms(uint32_t nms)
{while(nms--)delay_us(1000);
}/*** @brief 秒级延时* @param ns 延时时长,范围:0~4294967295* @retval 无*/
void delay_s(uint32_t ns)
{while(ns--)delay_ms(1000);
}/*** @brief 重写HAL_Delay函数* @param nms 延时时长,范围:0~4294967295* @retval 无*/
void HAL_Delay(uint32_t nms)
{delay_ms(nms);
}
实现原理:
void delay_us(uint32_t nus)
{uint32_t temp;/* SysTick 是 Cortex-M3 内核自带的 24 位递减定时器本函数每次都“一次性装载”一个目标计数值,然后忙等直到计数归零 */SysTick->LOAD = 72 * nus; // 1) 设重装载值// 选择 HCLK=72MHz 做时钟源 → 1us 需要 72 个时钟// (严格讲应写 72*nus-1,差 1 个时钟≃13.9ns,可忽略)SysTick->VAL = 0x00; // 2) 清当前计数值(写任意值清零),确保从 LOAD 开始数SysTick->CTRL |= (1u << 2); // 3) 选择时钟源:CLKSOURCE=1 → HCLK(72MHz)// 0 则为 HCLK/8(F1 上为 AHB/8)SysTick->CTRL |= (1u << 0); // 4) ENABLE=1,启动 SysTick(不使能中断,只是计数)do {temp = SysTick->CTRL; // 5) 读 CTRL 寄存器// 注意:COUNTFLAG(位16)在被读出后自动清零} while ( (temp & 0x01) && // 6) 只要 ENABLE 仍为 1 且!(temp & (1u << 16))); // COUNTFLAG==0(尚未到 0),就一直忙等// COUNTFLAG=1 表示“从 1 计到 0 发生了一次”,// 也就是本次延时到点了SysTick->CTRL &= ~(1u << 0); // 7) 关 SysTick,避免影响别人
}
关键寄存器位(SysTick)
CTRL
bit0 ENABLE
:1=启动;0=停止bit1 TICKINT
:1=到 0 触发异常(中断);本函数没开bit2 CLKSOURCE
:1=HCLK;0=HCLK/8bit16 COUNTFLAG
:从 1 到 0 时置 1,读出后自动清零
LOAD
:24 位重装载值(最大0xFFFFFF
)VAL
:当前计数值(写任意值清零)
执行流程(一步步发生了什么)
写
LOAD
:把需要的“倒计时时钟数”写入(这里按 72*nus 计算)。清
VAL
:让计数器从LOAD
开始递减。选时钟源:置
CLKSOURCE=1
,用 72 MHz HCLK。启动:置
ENABLE=1
。忙等:循环读取
CTRL
,直到COUNTFLAG=1
(说明从 1 数到 0 发生过一次)。停止:清
ENABLE
,退出。
由于 SysTick 是递减计数器,从
LOAD
数到 0 的时间是 (LOAD+1)/时钟频率。
所以严格写法应是LOAD = 72*nus - 1
,你的写法多等了 1 个时钟(≈14 ns),通常可以忽略。
时间上限 & 精度
SysTick 只有 24 位:
LOAD
最大2^24-1=16,777,215
。当
CLKSOURCE=HCLK=72 MHz
:最大延时 ≈
16,777,215 / 72e6
≈ 233,018 µs ≈ 233 ms。超过这个值会溢出,需要分段多次调用或使用更长周期的延时函数(如
delay_ms
循环)。
误差来源:
LOAD
没减 1 带来的 +1 个时钟(≈14 ns);中断抢占:本函数是忙等,若中途有高优先级中断抢占,实际延时会被拉长。
与 HAL/RTOS 的关系与注意点(很重要)
可能破坏 HAL 的系统节拍:HAL 默认使用 SysTick 做 1ms 节拍(
HAL_IncTick()
)。
在此函数里重配/停止了 SysTick,会影响HAL_Delay()
、时间戳、RTOS 等。解决:
不要改
CLKSOURCE/ENABLE
等全局配置,或者单独使用定时器 TIMx/DWT 实现微秒延时,或
进入临界区后恢复原状态(保存/还原
CTRL/LOAD/VAL
)。
RTOS 场景:SysTick 常被 RTOS 占用为系统时钟,更不建议直接改。
推荐改用 DWT->CYCCNT 或 TIM 硬件定时器实现微秒延时。
可选的更稳妥写法
与系统时钟无关的写法(自动适配 8/72/其它频点):
/*** @brief 微秒级延时(忙等法,基于 SysTick 24 位递减计时器)* @param nus 需要延时的微秒数* @note 会“临时接管”SysTick:重新配置 LOAD/VAL/CTRL,并在结束时关闭。* 如果工程里 HAL/RTOS 正在使用 SysTick,请谨慎使用或改用 DWT/TIM。*/
void delay_us(uint32_t nus)
{/* 1) 计算需要的“时钟周期数”SystemCoreClock 是当前 CPU 时钟(HCLK)频率,单位 Hz。例如 F103 72MHz:SystemCoreClock = 72,000,0001us 需要 SystemCoreClock/1,000,000 个时钟周期。*/uint32_t ticks = (SystemCoreClock / 1000000u) * nus;/* 2) 配置 SysTick 的重装载值:SysTick 是 24 位递减计数器,实际延时 = (LOAD + 1) / 时钟频率。因此写入 (ticks - 1)。若 ticks 为 0 会溢出,请保证 nus>0。 */SysTick->LOAD = ticks - 1U;/* 3) 清当前计数器值(写任意值会把 VAL 清零),让计数器从 LOAD 开始递减。 */SysTick->VAL = 0U;/* 4) 选择时钟源并启动:- CLKSOURCE_Msk:选择 HCLK 作为计数源(不分频)- ENABLE_Msk :启动 SysTick(不使能中断 TICKINT) */SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk | SysTick_CTRL_ENABLE_Msk;/* 5) 忙等直到计数归零:- COUNTFLAG 位在从 1 计到 0 时置 1,且“读后自动清零”- 条件含义:当 ENABLE 仍为 1 且 COUNTFLAG 还没置位时,继续等待 */while ( (SysTick->CTRL & SysTick_CTRL_ENABLE_Msk) &&!(SysTick->CTRL & SysTick_CTRL_COUNTFLAG_Msk) ){/* 空循环等待 */}/* 6) 关闭 SysTick,避免影响到其他代码(比如 HAL 的 1ms 节拍) */SysTick->CTRL = 0U;
}
实现原理(怎么工作的)
硬件基础:SysTick 是 Cortex-M3 内核自带的 24 位递减计数器,有三个关键寄存器:
LOAD
:重装载值(最大 2^24−1)。VAL
:当前计数值(写任意值清零)。CTRL
:控制/状态位:CLKSOURCE
选择时钟源(这里选 HCLK,即SystemCoreClock
)。ENABLE
启停计数。COUNTFLAG
从 1 减到 0 时置 1(读出后自动清零)。
时序:
计算需要的时钟周期数
ticks = HCLK(Hz) × 延时(s)
,并写入LOAD = ticks - 1
。清
VAL
,使计数器从LOAD
开始递减。置
CLKSOURCE=HCLK
、ENABLE=1
启动计数。忙等直到
COUNTFLAG=1
(说明从 1 数到 0 发生过一次)→ 延时达成。关闭计数器。
延时公式:
delay = (LOAD + 1) / HCLK
这里LOAD = ticks - 1
,因此理论延时≈ticks / HCLK = nus(µs)
。
重要注意
24 位上限:
LOAD ≤ 0xFFFFFF
。当CLKSOURCE=HCLK=72MHz
时,单次最大延时约0xFFFFFF / 72e6 ≈ 0.233 s
。更长延时需分段或用delay_ms
循环。与 HAL/RTOS 冲突:HAL 默认用 SysTick 产生 1ms 节拍(
HAL_IncTick()
)。这段代码会覆盖并关闭 SysTick 设置,可能影响HAL_Delay()
或 RTOS。更稳妥:改用 DWT->CYCCNT 或 TIMx 定时器做微秒延时。
中断影响:这是忙等方式,若期间有更高优先级中断抢占,实际延时会被拉长。
时钟自适应:用
SystemCoreClock
计算ticks
,能在变频后仍保持正确延时,前提是在变频后调用过SystemCoreClockUpdate()
或 HAL 已正确更新该变量。
2.不影响 SysTick(HAL/RTOS 友好):用 DWT 周期计数器(Cortex-M3 支持)
/* ===================== DWT 初始化 ===================== */
/*** @brief 使能 DWT 的周期计数器 CYCCNT(只需调用一次)* @note DWT 属于 Cortex-M 的调试/跟踪模块。要读写 DWT 寄存器,* 需先在 CoreDebug->DEMCR 中打开 TRCENA(Trace Enable)。* CYCCNT 每个 CPU 时钟周期自增 1(32 位计数器)。*/
static inline void dwt_init(void)
{CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; // 开 DWT/ITM/ETM 访问开关DWT->CYCCNT = 0; // 复位周期计数器DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; // 使能 CYCCNT 自增
}/* ===================== 微秒延时(忙等) ===================== */
/*** @brief 基于 DWT->CYCCNT 的微秒级延时(非侵入 SysTick)* @param us: 需要延时的微秒数* @note 读起始时刻的 CYCCNT,计算目标“时钟周期数”ticks,* 然后忙等直到 (当前CYCCNT - 起始CYCCNT) >= ticks。* 这种写法天然支持 CYCCNT 的 32 位回绕。*/
static inline void delay_us_dwt(uint32_t us)
{uint32_t start = DWT->CYCCNT; // 起始时间戳(CPU 周期计数)uint32_t ticks = (SystemCoreClock / 1000000u) * us; // 需要等待的周期数 = HCLK * us/* 使用无符号减法处理回绕:CYCCNT 是 32 位递增,溢出后从 0 重新开始(模 2^32)。只要等待时长小于一个回绕周期(72MHz 时约 59.6 s),(当前 - 起始) 的结果就是从起始到当前的“正确经过周期数”。 */while ((DWT->CYCCNT - start) < ticks) { /* busy wait */ }
}
这样既不改 SysTick,也更精确(纳秒级分辨率),常用于裸机/RTOS。
实现原理(怎么工作的)
DWT(Data Watchpoint and Trace)CYCCNT
Cortex-M 内核自带的调试/跟踪单元。CYCCNT
是 32 位“CPU 时钟周期计数器”,在使能后,每个 HCLK 周期 +1。
对于 STM32F103C8T6(72 MHz),分辨率是 1/72 MHz ≈ 13.9 ns。延时思路
启用 DWT 并开启
CYCCNT
自增(TRCENA=1
、CYCCNTENA=1
)。读取起始值
start = CYCCNT
。计算目标等待的周期数:
ticks = SystemCoreClock * us / 1e6
。忙等:
while (CYCCNT - start < ticks)
。当差值达到ticks
,说明流逝了指定的微秒数。回绕安全:
CYCCNT
以 2^32 为模,使用无符号减法可自动处理回绕,只要等待时间小于一个回绕周期即可。在 72 MHz 下,回绕周期
2^32 / 72e6 ≈ 59.6 s
。
优点
不改动 SysTick,对 HAL/RTOS 友好;
精度高(周期级),性能开销极小。
使用要点 / 注意事项
请在系统时钟配置完成后调用
dwt_init()
一次;若后续切换系统时钟,确保SystemCoreClock
已更新(SystemCoreClockUpdate()
或 HAL 自动更新)。该函数是忙等,期间若有高优先级中断,会拉长实际延时;若需要可预先临界区保护。
单次延时不应超过一个回绕周期(F103/72 MHz 下 < ~59.6 s,远大于常见微秒/毫秒延时需求)。
个别芯片/安全环境可能默认锁定 DWT(TRCENA 受限),普通 F1/F4/F7 工程一般可直接使用。
总结
delay_us()
是用 SysTick 递减计数器做的忙等延时:
装载计数 → 选 HCLK → 启动 → 等待 COUNTFLAG → 停止。假定 HCLK=72 MHz,
LOAD=72*us
,最大一次能延时约 233 ms。在使用 HAL/RTOS 的工程里,不建议频繁重配 SysTick,更推荐 DWT 或 TIMx 来实现微秒延时。