【STM32】时钟摘取法

时钟摘取法------毫秒获取微秒延时的方法介绍

前言

在 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,必须先牢记它的规则:

  1. 24 位递减计数器,最大值 0xFFFFFF

  2. 时钟源 = MCU主频(如 72MHz/168MHz等)

  3. 工作流程:

    LOAD 重装载值→ 自动递减 → 减到 0 → 自动重装回 LOAD 值 → 循环往复

  4. RTOS 中一般配置:LOAD = 主频/1000,实现 1ms 中断一次

以 168MHz 主频为例:

168MHz换算过来,也就是1s运行168000000次,也就是1次消耗1/168000000s,即1/168us一次,1ms则为168000次。

故重装载值LOAD = 168000(1ms 递减完一轮)


三、核心原理

1. 核心逻辑

该方法不直接控制计数器,而是只 "摘取" 时间片段 ,在配置好systick的1ms计数中断后,进行下述步骤实现时钟摘取法获取高精度us延时,即:

  1. 延时开始时,记录初始计数值
  2. 循环获取并记录当前计数值
  3. 计算两次读数的差值,其中差值可能是正常相差数值,也可能是出现重装导致的小于前一次值产生的负差值;(正常递减 + 溢出重装两种情况)
  4. 累计差值达到目标计数值 → 延时结束。

2. 关键换算公式

  1. 1us 对应实际总的计数值(定义全局变量为g_fac_us):

    c 复制代码
    g_fac_us = 系统主频 / 1000000

    主频为168MHz,则g_fac_us = 168(原因:1us 需要计数 168 次);

    主频为72MHz,则g_fac_us = 72(原因:1us计数72次)

  2. 目标微秒延时需要的计数次数(目标计数值):

    c 复制代码
    目标计数值 = 延时us数 × g_fac_us

3. 两种计数情况(重点理解)

SysTick 是递减计数器,只有两种情况:

  1. 正常递减:当前值 < 初始值 → 差值 = 初始值 - 当前值
  2. 溢出重装:当前值 > 初始值(计数器减到 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

六、总结

  1. 时钟摘取法本质:只读 SysTick 计数器,累计差值实现延时,不干扰系统
  2. 两大关键:正常递减计算、溢出重装计算(代码核心)
  3. 最大优势:RTOS 安全、高精度、无资源占用
  4. 使用要求 :SysTick 已启动、g_fac_us 初始化正确

以上便是本次文章的所有内容,欢迎各位朋友在评论区讨论,本人也是一名初学小白,愿大家共同努力,一起进步吧!
鉴于笔者能力有限,难免出现一些纰漏和不足,望大家在评论区批评指正,谢谢!

相关推荐
崇山峻岭之间3 小时前
单片机GPIO配置
单片机·嵌入式硬件
不会武功的火柴3 小时前
SystemVerilog语法(7)-接口(interface)
嵌入式硬件·fpga开发·仿真·ic验证·rtl
深圳英康仕4 小时前
五网口六USB:一台龙芯2K3000工控机的接口配置解读
嵌入式硬件·信创·工控机·工业计算机·龙芯2k3000
lllllllccccc4 小时前
FReeRtos中断管理、临界段保护和任务调度器挂起和恢复学习
单片机·嵌入式硬件
ACP广源盛139246256735 小时前
IX8024 对标 ASM2824 @ACP#搭配昆仑芯 P800 构建 AI 服务器 PCIe4.0 高速互联架构
网络·人工智能·嵌入式硬件·电脑
踏着七彩祥云的小丑5 小时前
嵌入式测试学习第 15 天:逻辑门基础:与或非、简单逻辑电路
单片机·嵌入式硬件
Ligocious6 小时前
stm32---2.按键触发外部中断
stm32·单片机
rit84324996 小时前
STM32F4 USB Host 功能实现
stm32·单片机·嵌入式硬件
金戈鐡馬7 小时前
定时器+中断优化单总线通信
stm32·单片机·嵌入式硬件