一、为什么需要 DWT 延时?

在 STM32F4 开发中,常见的延时方案有:

  • 软件循环延时:简单但精度极差,受编译器优化影响大。
  • SysTick 延时:精度尚可,但 SysTick 常被操作系统或中断服务程序占用,且延时过程中若发生中断,会暂停计时,导致时间偏长。
  • 通用定时器延时:占用外设资源,且同样存在中断抢占问题。

核心痛点:任何依赖中断或外设的延时,在中断频繁触发时都会产生累积误差。而 DWT 的 CYCCNT 是内核级计数器,不依赖外设时钟,直接以 CPU 主频计数,且不受中断影响(除非内核停止),因此能实现真正的微秒级精确延时。

二、DWT 原理简介

DWT(Data Watchpoint and Trace)是 Cortex-M4 内核自带的调试组件,其中包含一个 32 位的自由运行计数器 CYCCNT,每个 CPU 时钟周期自动加 1。当计数溢出(0xFFFFFFFF)后回绕到 0,周期约为 4.29 秒(以 168MHz 主频计算)。

要使用 CYCCNT,需完成三步初始化:

  1. 使能 DWT 单元(DWT->CTRLNOCYCCNT 位清零)。
  2. 使能 CYCCNT 计数器(DWT->CTRLCYCCNTENA 位置 1)。
  3. 复位计数值(DWT->CYCCNT = 0)。

关键寄存器

  • DWT->CTRL:控制寄存器,位 0 为 CYCCNTENA,位 25 为 NOCYCCNT。
  • DWT->CYCCNT:当前计数值(只读)。

三、实现方案

1. 初始化 DWT

#include "stm32f4xx.h"

/**
 * @brief 初始化 DWT 延时功能
 * @param 无
 * @retval 无
 */
void DWT_Init(void)
{
    // 使能 DWT 访问(若未使能)
    CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;

    // 复位 CYCCNT 计数器
    DWT->CYCCNT = 0;

    // 使能 CYCCNT 计数器(清零 NOCYCCNT,置位 CYCCNTENA)
    DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
}

2. 微秒延时函数

/**
 * @brief 微秒延时(基于 DWT,抗中断干扰)
 * @param us: 延时微秒数(最大约 4.29e6 / 主频MHz,以 168MHz 为例约 25500us)
 * @retval 无
 */
void DWT_Delay_us(uint32_t us)
{
    uint32_t startTick = DWT->CYCCNT;
    uint32_t delayTicks = us * (SystemCoreClock / 1000000);  // 微秒转时钟周期数
    uint32_t currentTick;

    // 循环直到达到目标周期数(处理溢出回绕)
    do {
        currentTick = DWT->CYCCNT;
        // 计算已流逝的周期数,考虑回绕
        if (currentTick >= startTick) {
            // 未回绕
            if ((currentTick - startTick) >= delayTicks) break;
        } else {
            // 已回绕,则流逝周期 = (0xFFFFFFFF - startTick) + currentTick + 1
            if ((0xFFFFFFFF - startTick + currentTick + 1) >= delayTicks) break;
        }
    } while (1);
}

优化版本(更简洁,利用无符号减法自动处理回绕):

void DWT_Delay_us(uint32_t us)
{
    uint32_t startTick = DWT->CYCCNT;
    uint32_t delayTicks = us * (SystemCoreClock / 1000000);
    uint32_t elapsed = 0;

    while (elapsed < delayTicks) {
        uint32_t currentTick = DWT->CYCCNT;
        elapsed = currentTick - startTick;  // 无符号减法,自动处理回绕
    }
}

注意currentTick - startTick 在回绕时依然正确,因为无符号整数减法遵循模运算规则。

3. 毫秒延时(可选)

void DWT_Delay_ms(uint32_t ms)
{
    while (ms--) {
        DWT_Delay_us(1000);
    }
}

4. 完整示例(以 LED 翻转测试)

int main(void)
{
    // 初始化系统时钟(默认 168MHz)
    SystemInit();

    // 初始化 LED GPIO(略)
    LED_Init();

    // 初始化 DWT 延时
    DWT_Init();

    while (1) {
        LED_Toggle();
        DWT_Delay_us(500);  // 精确延时 500us
    }
}

四、注意事项与避坑指南

  • 确保 SystemCoreClock 正确SystemCoreClock 变量在 system_stm32f4xx.c 中定义,若手动修改时钟配置,需同步更新该变量,否则延时时间会偏差。
  • 最大延时限制:CYCCNT 为 32 位,在 168MHz 下最大可延时约 25.5 毫秒(0xFFFFFFFF / 168e6 ≈ 25.5ms)。若需更长延时,请循环调用或改用其他方案。
  • 中断干扰:DWT 延时不受中断影响,但若中断服务函数执行时间过长,会延迟主循环中延时后的代码执行,这属于正常现象,延时本身依然准确。
  • 低功耗模式:在睡眠或停止模式下,CPU 时钟停止,CYCCNT 也会停止,因此 DWT 延时在低功耗唤醒后需重新校准。
  • 调试器影响:若使用调试器(如 ST-Link)进行单步调试,CYCCNT 会随 CPU 暂停而暂停,这可能导致延时异常,属正常现象。
  • 代码优化:建议将 DWT_Delay_us 函数定义为内联函数或使用 __attribute__((always_inline)),以减少函数调用开销,提高精度。

五、总结

DWT 延时方案利用内核级计数器,实现了微秒级高精度延时,且不受中断干扰,是嵌入式开发中非常实用的技巧。通过简单的初始化,即可替代传统的 SysTick 或定时器方案,尤其适用于对时序要求严格的传感器驱动和通信协议实现。希望本文能帮助你解决延时不准的困扰,提升代码的可靠性。