SysTick寄存器(嘀嗒定时器实现延时)

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:当前计数值(写任意值清零)


执行流程(一步步发生了什么)

  1. LOAD:把需要的"倒计时时钟数"写入(这里按 72*nus 计算)。

  2. VAL :让计数器从 LOAD 开始递减。

  3. 选时钟源 :置 CLKSOURCE=1,用 72 MHz HCLK

  4. 启动 :置 ENABLE=1

  5. 忙等 :循环读取 CTRL,直到 COUNTFLAG=1(说明从 1 数到 0 发生过一次)。

  6. 停止 :清 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 / 72e6233,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->CYCCNTTIM 硬件定时器实现微秒延时。

可选的更稳妥写法

  1. 与系统时钟无关的写法(自动适配 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(读出后自动清零)。

  • 时序

    1. 计算需要的时钟周期数 ticks = HCLK(Hz) × 延时(s),并写入 LOAD = ticks - 1

    2. VAL,使计数器从 LOAD 开始递减。

    3. CLKSOURCE=HCLKENABLE=1 启动计数。

    4. 忙等 直到 COUNTFLAG=1(说明从 1 数到 0 发生过一次)→ 延时达成。

    5. 关闭计数器。

  • 延时公式
    delay = (LOAD + 1) / HCLK

    这里 LOAD = ticks - 1,因此理论延时≈ ticks / HCLK = nus(µs)


重要注意

  1. 24 位上限LOAD ≤ 0xFFFFFF。当 CLKSOURCE=HCLK=72MHz 时,单次最大 延时约
    0xFFFFFF / 72e6 ≈ 0.233 s。更长延时需分段或用 delay_ms 循环。

  2. 与 HAL/RTOS 冲突 :HAL 默认用 SysTick 产生 1ms 节拍(HAL_IncTick())。这段代码会覆盖并关闭 SysTick 设置,可能影响 HAL_Delay() 或 RTOS。

    • 更稳妥:改用 DWT->CYCCNTTIMx 定时器做微秒延时。
  3. 中断影响 :这是忙等方式,若期间有更高优先级中断抢占,实际延时会被拉长。

  4. 时钟自适应 :用 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 内核自带的调试/跟踪单元。CYCCNT32 位 "CPU 时钟周期计数器 ",在使能后,每个 HCLK 周期 +1。

    对于 STM32F103C8T6(72 MHz) ,分辨率是 1/72 MHz ≈ 13.9 ns

  • 延时思路

    1. 启用 DWT 并开启 CYCCNT 自增(TRCENA=1CYCCNTENA=1)。

    2. 读取起始值 start = CYCCNT

    3. 计算目标等待的周期数ticks = SystemCoreClock * us / 1e6

    4. 忙等: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 MHzLOAD=72*us,最大一次能延时约 233 ms

  • 在使用 HAL/RTOS 的工程里,不建议频繁重配 SysTick ,更推荐 DWTTIMx 来实现微秒延时。

相关推荐
globbo1 小时前
【嵌入式STM32】I2C总结
单片机·嵌入式硬件
limitless_peter2 小时前
集成运算放大器(反向比例,同相比例)
嵌入式硬件·硬件工程
Blossom.1183 小时前
把 AI 推理塞进「 8 位 MCU 」——0.5 KB RAM 跑通关键词唤醒的魔幻之旅
人工智能·笔记·单片机·嵌入式硬件·深度学习·机器学习·搜索引擎
桃源学社(接毕设)4 小时前
基于人工智能和物联网融合跌倒监控系统(LW+源码+讲解+部署)
人工智能·python·单片机·yolov8
玖別ԅ(¯﹃¯ԅ)5 小时前
PID学习笔记6-倒立摆的实现
笔记·stm32·单片机
清风66666610 小时前
基于51单片机的手机蓝牙控制8位LED灯亮灭设计
单片机·嵌入式硬件·智能手机·毕业设计·51单片机·课程设计
anghost15017 小时前
基于单片机的超市储物柜设计
单片机·嵌入式硬件·超市储物柜设计
qq_5260991321 小时前
工控机的用途与介绍:工业自动化的重要引擎
嵌入式硬件·自动化·电脑
尘似鹤21 小时前
旋钮键盘项目---foc讲解(开环)
单片机·嵌入式硬件