利用 ARM Cortex-M 内核 DWT 计数器实现微秒级精确延时

为什么需要 DWT 延时?

在嵌入式系统中,延时函数无处不在:传感器采样间隔、通信协议时序、LED 闪烁等。传统方案通常使用 SysTick 定时器或通用定时器(如 TIMx),但存在以下痛点:

  • SysTick 被 RTOS 占用:在 FreeRTOS 等系统中,SysTick 已被系统节拍占用,无法直接用于延时。
  • 通用定时器资源紧张:每个定时器都有特定功能(PWM、输入捕获等),用于延时是一种浪费。
  • 软件循环精度差:简单的 for 循环延时受编译器优化、中断影响,误差可达数十微秒。

ARM Cortex-M3/M4/M7 内核内置的 DWT 单元提供了一个 32 位自由运行计数器(CYCCNT),它直接以内核时钟频率递增,精度可达 1 个时钟周期。利用它,我们可以实现不占用任何外设的微秒级延时。

DWT 工作原理

DWT 是 CoreSight 调试组件的一部分,主要用于数据观察和跟踪。其中,CYCCNT 寄存器是一个 32 位计数器,每个内核时钟周期自动加 1,溢出后回绕(约 2^32 周期)。

要使用 CYCCNT,需完成以下步骤:

  1. 使能 DWT 单元:访问 DWT 控制寄存器(DWT_CTRL)的 CYCCNTENA 位(bit 0)。
  2. 使能 CYCCNT 计数器:访问 DWT_CYCCNT 寄存器,先清零再使能。
  3. 读取当前值:通过 DWT->CYCCNT 读取。

注意:DWT 属于调试组件,在正常运行时也可访问,但需确保内核调试接口未被禁用。在大多数 Cortex-M 芯片(如 STM32F1/F4/H7)上,默认即可使用。

实现微秒级延时

1. 宏定义与初始化

首先,我们需要获取内核时钟频率。在 STM32 中,可通过 SystemCoreClock 变量获取(单位 Hz)。延时函数的核心是计算目标周期数:目标周期 = 微秒数 * (系统时钟 / 1000000)

#include "stm32f4xx.h"  // 以 STM32F4 为例

// 使能 DWT 计数器
void DWT_Init(void)
{
    CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; // 使能 DWT 访问
    DWT->CYCCNT = 0;                                // 清零计数器
    DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;            // 使能 CYCCNT
}

// 微秒延时函数
void DWT_Delay_us(uint32_t us)
{
    uint32_t start = DWT->CYCCNT;
    uint32_t ticks = us * (SystemCoreClock / 1000000);
    while ((DWT->CYCCNT - start) < ticks);  // 注意无符号减法处理回绕
}

2. 处理计数器回绕

由于 CYCCNT 是 32 位,当延时较长时可能发生回绕。但利用无符号整数减法特性,(DWT->CYCCNT - start) 会自动处理回绕,只要延时周期数小于 2^32,结果就是正确的。例如,在 168MHz 下,最大可延时约 25 秒(2^32 / 168e6 ≈ 25.5s),足以覆盖绝大多数场景。

3. 优化:避免除法运算

上述代码中 SystemCoreClock / 1000000 是常量,编译器会优化为乘法,但为了更高效,可预计算每微秒的周期数。

static uint32_t us_ticks = 0;

void DWT_Init(void)
{
    CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
    DWT->CYCCNT = 0;
    DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
    us_ticks = SystemCoreClock / 1000000;
}

void DWT_Delay_us(uint32_t us)
{
    uint32_t start = DWT->CYCCNT;
    uint32_t ticks = us * us_ticks;
    while ((DWT->CYCCNT - start) < ticks);
}

4. 毫秒级延时封装

基于微秒延时,可轻松实现毫秒延时:

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

完整示例代码

以下是一个完整的 STM32F4 示例,演示 DWT 延时的使用:

#include "stm32f4xx.h"

static uint32_t us_ticks;

void DWT_Init(void)
{
    CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
    DWT->CYCCNT = 0;
    DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
    us_ticks = SystemCoreClock / 1000000;
}

void DWT_Delay_us(uint32_t us)
{
    uint32_t start = DWT->CYCCNT;
    uint32_t ticks = us * us_ticks;
    while ((DWT->CYCCNT - start) < ticks);
}

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

int main(void)
{
    DWT_Init();
    
    // 初始化 GPIO 等...
    
    while (1) {
        // 精确延时 500us
        DWT_Delay_us(500);
        // 精确延时 2ms
        DWT_Delay_ms(2);
    }
}

注意事项

  • 内核时钟频率SystemCoreClock 必须在系统时钟初始化后正确设置,否则延时不准。若使用外部晶振或 PLL,请确保时钟配置正确。
  • 中断影响:DWT 延时是忙等待,期间会阻塞 CPU,无法响应中断(除非中断优先级高于当前执行)。若需在中断中延时,请确保延时时间远小于中断周期。
  • 低功耗模式:在睡眠或停止模式下,内核时钟可能停止,DWT 计数器也会停止,因此延时函数不适用于低功耗场景。
  • 调试器干扰:使用调试器时,若设置了硬件断点,DWT 计数器可能受影响,但一般正常运行无碍。
  • 芯片差异:部分 Cortex-M0/M0+ 内核没有 DWT 单元,需确认芯片手册。Cortex-M3/M4/M7 均支持。

总结

DWT 计数器是 Cortex-M 内核的隐藏宝藏,利用它可实现高精度、零外设占用的延时功能。相比 SysTick 和通用定时器,DWT 延时更灵活,尤其适合 RTOS 环境或资源紧张的项目。掌握这一技巧,能让你的嵌入式代码更加高效、专业。

在实际项目中,建议将 DWT 延时封装成独立模块,便于复用。同时,结合 volatile 关键字和编译器优化选项,确保代码行为符合预期。希望本文能帮助你提升嵌入式开发效率!