时钟摘取法------毫秒获取微秒延时的方法介绍
前言
在 STM32 开发中,尤其是 带 RTOS(FreeRTOS/UCOS/RT-Thread) 的项目里,SysTick 系统定时器通常被操作系统占用,作为系统时钟节拍源(一般 1ms 中断一次)。
如果我们直接修改 SysTick 配置、关闭中断来实现裸机延时,会直接导致系统时钟紊乱、任务调度异常。而时钟摘取法 就是解决这一问题的最优方案:不修改寄存器、不占用中断、只读计数器实现高精度 us 级延时,兼容操作系统与裸机延时。
一、时钟摘取法
1. 定义
时钟摘取法是仅读取 SysTick 计数器当前值,通过计算计数值差值、累计计时 实现阻塞式高精度延时的方法,整个过程只读不写SysTick 配置寄存器。
2. 核心特点
- 不修改 SysTick->LOAD、CTRL 等寄存器,不干扰操作系统
- 不占用中断,不影响系统任务调度
- 延时精度可达 1us,满足 I2C、SPI、LCD 等外设时序要求
- 代码轻量、无额外硬件定时器占用
3. 适用场景
- RTOS 环境下的高精度微秒级延时
- 外设驱动严格时序控制
- 禁止修改系统时钟配置的项目
二、STM32 SysTick 工作原理
时钟摘取法完全依赖 SysTick,必须先牢记它的规则:
-
24 位递减计数器,最大值 0xFFFFFF
-
时钟源 = MCU主频(如 72MHz/168MHz等)
-
工作流程:
LOAD 重装载值→ 自动递减 → 减到 0 → 自动重装回 LOAD 值 → 循环往复
-
RTOS 中一般配置:
LOAD = 主频/1000,实现 1ms 中断一次
以 168MHz 主频为例:
168MHz换算过来,也就是1s运行168000000次,也就是1次消耗1/168000000s,即1/168us一次,1ms则为168000次。
故重装载值LOAD = 168000(1ms 递减完一轮)
三、核心原理
1. 核心逻辑
该方法不直接控制计数器,而是只 "摘取" 时间片段 ,在配置好systick的1ms计数中断后,进行下述步骤实现时钟摘取法获取高精度us延时,即:
- 延时开始时,记录初始计数值;
- 循环获取并记录当前计数值;
- 计算两次读数的差值,其中差值可能是正常相差数值,也可能是出现重装导致的小于前一次值产生的负差值;(正常递减 + 溢出重装两种情况)
- 累计差值达到目标计数值 → 延时结束。
2. 关键换算公式
-
1us 对应实际总的计数值(定义全局变量为g_fac_us):
cg_fac_us = 系统主频 / 1000000主频为168MHz,则g_fac_us = 168(原因:1us 需要计数 168 次);
主频为72MHz,则g_fac_us = 72(原因:1us计数72次)
-
目标微秒延时需要的计数次数(目标计数值):
c目标计数值 = 延时us数 × g_fac_us
3. 两种计数情况(重点理解)
SysTick 是递减计数器,只有两种情况:
- 正常递减:当前值 < 初始值 → 差值 = 初始值 - 当前值
- 溢出重装:当前值 > 初始值(计数器减到 0 重装了)→ 差值 = 重装载值 + 初始值 - 当前值
四、一般代码介绍
1. 完整代码(初始化 + 延时函数)
C
// 对应主频 1us 对应的 SysTick 实际计数值
static uint32_t g_fac_us = 0;
/**
* @brief 时钟摘取法初始化
* @note 必须在系统时钟初始化后调用
*/
void delay_init(void)
{
// 168MHz 时:g_fac_us = 168
g_fac_us = SystemCoreClock / 1000000;
}
/**
* @brief 时钟摘取法 us级延时
* @param nus: 要延时的微秒数
*/
void delay_us(uint32_t nus)
{
uint32_t target_ticks; // 期望延时需要计数的总数值
uint32_t t_old = 0; // 旧值 => 前一次计数器值
uint32_t t_now = 0; // 当前计数器的数值
uint32_t t_cnt = 0; // 累计计数值 => 从进入循环等待到当前累计经过的数值
uint32_t reload = SysTick->LOAD; // 获取SysTick重装载值LOAD => 一般为1ms对应的计数值
// 计算总目标计数值:延时us × 1us对应计数值
target_ticks = nus * g_fac_us;
// 记录延时开始时的初始计数值
t_old = SysTick->VAL;
// 循环等待计时到达
while(1)
{
// 持续获取并记录当前计数值
t_now = SysTick->VAL;
// 时间不断运行,计数器计数值变化
if(t_now != t_old) // 使得当前计数器计数值与前面记录的计数值出现差值
{
// 场景1-正常递减 => 当前计数器值小于之前记录的计数值
if(t_now < t_old)
{
t_cnt += t_old - t_now;
}
// 场景2-计数器溢出,从0重装回LOAD => 当前计数器值大于之前记录的计数值
else
{
t_cnt += reload + t_old - t_now;
}
// 更新上一次的计数值
t_old = t_now;
// 累计计数值 ≥ 目标值 → 延时完成
if(t_cnt >= target_ticks)
{
break;
}
}
}
}
2. 核心代码重点解析
(1)reload = SysTick->LOAD
- 直接读取系统已配置好的重装载值,不做任何修改
- 168MHz 系统中,值为 168000(1ms 重装一次)
(2)target_ticks = nus * g_fac_us
- 把 "微秒时间" 转换成 "SysTick 计数值"
- 例:延时 10us → 10 × 168 = 1680 个计数值
(3)正常递减逻辑(t_now < t_old)
c
t_cnt += t_old - t_now;
- 计数器从 1000 → 800
- 走过计数:1000 - 800 = 200,直接累加
(4)溢出重装逻辑(t_now > t_old)
C
tcnt += reload + t_old - t_now;
- 计数器:50 → 0 → 重装 → 167950
- 走过计数:50(到 0) + 50(重装后递减)= 100
- 公式计算:168000 - 167950 + 50 = 100,完全准确
五、理解(168MHz 主频为例)
1. 时钟与计数值换算
- 系统主频:168MHz
- 1ms 中断所需计数值:168000000 × 0.001 = 168000
- 1 个计数耗时:1/168 us ≈ 0.006us
- 1us 对应计数值:168 →
g_fac_us = 168
2. 溢出场景模拟理解
模拟简化数值:
reload=168,计数器变化:1 → 0 → 168 → 167
- 初始值
t_old=1 - 当前值
t_now=167 - 计算:168 + 1 - 167 = 2 个计数
- 真实耗时:2 个计数,对应 2/168 us
六、总结
- 时钟摘取法本质:只读 SysTick 计数器,累计差值实现延时,不干扰系统
- 两大关键:正常递减计算、溢出重装计算(代码核心)
- 最大优势:RTOS 安全、高精度、无资源占用
- 使用要求 :SysTick 已启动、
g_fac_us初始化正确
以上便是本次文章的所有内容,欢迎各位朋友在评论区讨论,本人也是一名初学小白,愿大家共同努力,一起进步吧!
鉴于笔者能力有限,难免出现一些纰漏和不足,望大家在评论区批评指正,谢谢!