delay.h
cpp
#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:
cpp
#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);
}
实现原理:
cpp
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/8 -
bit16 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/其它频点):
cpp
/**
* @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,000
1us 需要 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 支持)
cpp
/* ===================== 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 来实现微秒延时。