引言

在 STM32 开发中,微秒级延时是驱动外设(如传感器、通信协议)的常见需求。SysTick 作为系统滴答定时器,通常被用作操作系统的心跳或基准延时,但在以下场景中力不从心:

  • 高频中断频繁抢占,SysTick 的更新被延迟;
  • 需要同时兼顾多个定时任务,SysTick 被占用;
  • 延时精度要求达到亚微秒级,SysTick 的时钟源(通常为 HCLK/8)分辨率不足。

此时,DWT(Data Watchpoint and Trace) 单元中的周期计数器(CYCCNT)提供了一个优雅的替代方案。它直接以 CPU 内核时钟(HCLK)计数,分辨率可达 1 个时钟周期,且几乎零开销。本文将带你从原理到实战,掌握这一技巧。

1. DWT 周期计数器原理

DWT 是 Cortex-M3/M4/M7 内核中的调试组件,其中 CYCCNT 是一个 32 位自由运行计数器,每个 CPU 时钟周期(HCLK)自动加 1。它不受中断影响,除非显式复位或溢出(约 2^32 个周期,在 72MHz 下约 59.6 秒)。

关键寄存器:

  • DWT_CTRL(地址 0xE0001000):控制 CYCCNT 使能。
  • DWT_CYCCNT(地址 0xE0001004):当前计数值。

使能步骤:

  1. 解锁调试寄存器(可选,但推荐):CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
  2. 复位 CYCCNT:DWT->CYCCNT = 0;
  3. 使能计数器:DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;

2. 实现微秒级延时函数

基于 CYCCNT,我们可以实现一个高精度延时函数。核心思路:计算目标延时对应的周期数,然后循环等待计数器差值达到目标。

// 延时 us 微秒,系统时钟 HCLK 频率(Hz)需外部传入
void DWT_Delay_us(uint32_t us, uint32_t hclk_hz) {
    uint32_t start = DWT->CYCCNT;
    uint32_t ticks = us * (hclk_hz / 1000000);  // 微秒转周期数
    while ((DWT->CYCCNT - start) < ticks);      // 无符号减法处理溢出
}

注意:hclk_hz 需根据实际系统时钟设置,例如 STM32F103 默认 72MHz,则 hclk_hz = 72000000

3. 边界场景与误差分析

3.1 中断干扰

场景:延时期间发生中断,CPU 跳转执行 ISR。

  • 误差来源:CYCCNT 在中断期间依然计数,但延时循环被暂停,导致实际耗时 = 目标延时 + 中断处理时间。
  • 分析:若中断频繁,误差累积明显。但相比 SysTick,DWT 不会因中断而丢失计数,因此误差是“增加”而非“漂移”,可预测。
  • 规避:在实时性要求高的场景,可临时关闭可屏蔽中断(__disable_irq()),但需权衡中断响应延迟。

3.2 时钟频率变化

场景:系统动态调频(如低功耗模式切换 HCLK)。

  • 误差来源:DWT 计数基于 HCLK,若频率改变,则延时时间变化。
  • 分析:例如从 72MHz 降至 36MHz,同样周期数耗时翻倍。
  • 规避:在延时前读取当前时钟频率(如通过 RCC_GetClocksFreq),或使用固定频率的时钟源(如 HSE)。

3.3 计数器溢出

场景:延时时间过长,超过 CYCCNT 溢出周期。

  • 误差来源:32 位计数器溢出后回绕,若未处理,延时可能提前结束。
  • 分析:72MHz 下溢出周期约 59.6 秒,一般微秒延时不会触及,但若延时数秒级需注意。
  • 规避:使用无符号减法 (DWT->CYCCNT - start) 可自动处理回绕,只要延时周期数小于 2^32。

3.4 指令开销

场景:循环判断和减法操作本身消耗周期。

  • 误差来源:每次循环迭代有 2-3 个周期开销,导致实际延时略大于目标。
  • 分析:对于微秒级延时(如 10us),误差约 0.1us,可忽略;但若追求极致,可校准。
  • 规避:在初始化时测量空循环的周期数,并调整目标 ticks。

4. 完整代码示例

以下是一个完整的驱动模块,包含初始化、延时函数和简单测试。

#include "stm32f1xx.h"

// 初始化 DWT 周期计数器
void DWT_Init(void) {
    CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; // 使能调试跟踪
    DWT->CYCCNT = 0;                                // 清零计数器
    DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;            // 使能 CYCCNT
}

// 微秒延时,hclk_hz 为 CPU 时钟频率
void DWT_Delay_us(uint32_t us, uint32_t hclk_hz) {
    uint32_t start = DWT->CYCCNT;
    uint32_t ticks = us * (hclk_hz / 1000000);
    while ((DWT->CYCCNT - start) < ticks);
}

// 毫秒延时(基于微秒延时,注意溢出)
void DWT_Delay_ms(uint32_t ms, uint32_t hclk_hz) {
    for (uint32_t i = 0; i < ms; i++) {
        DWT_Delay_us(1000, hclk_hz);
    }
}

// 测试:主函数中初始化并延时
int main(void) {
    DWT_Init();
    while (1) {
        DWT_Delay_us(10, 72000000);  // 延时 10us
        // 其他操作
    }
}

5. 注意事项

  • 内核支持:仅 Cortex-M3/M4/M7 支持 DWT,Cortex-M0/M0+ 无此功能。
  • 调试器影响:使用调试器时,CYCCNT 可能受调试暂停影响,但正常运行无碍。
  • 功耗模式:在睡眠模式下,HCLK 可能停止,DWT 不计数,需唤醒后重新校准。
  • 替代方案:若需更高精度(纳秒级),可考虑使用硬件定时器(如 TIM2)的输入捕获模式,但开销更大。

结语

DWT 周期计数器为 STM32 微秒级延时提供了一种轻量、精准的解决方案,尤其适合 SysTick 被占用或中断频繁的场景。通过理解其原理和误差边界,开发者可以灵活选用,提升系统实时性。希望本文能为你打开新思路,在嵌入式开发中游刃有余。