引言

在 STM32F4 系列(如 STM32F407)开发中,精确延时是许多外设驱动(如 DHT11、DS18B20、WS2812)的关键。传统基于 SysTick 的延时函数在中断频繁时误差可达数十微秒,无法满足微秒级需求。DWT(Data Watchpoint and Trace)单元内置一个 32 位自由运行计数器(CYCCNT),每个内核时钟周期递增一次,可提供纳秒级分辨率,是实现微秒延时的理想选择。

然而,DWT 延时在中断嵌套场景下会面临冲突:若延时过程中发生高优先级中断,计数器仍递增,但延时代码被暂停,导致实际等待时间超出预期。本文将深入探讨如何利用 DWT 实现精确延时,并设计一种中断安全的延时机制。

DWT 原理与配置

1. DWT 硬件基础

DWT 是 ARM Cortex-M4 内核的调试组件,包含多个计数器,其中 CYCCNT 专门用于统计内核时钟周期。它不受 SysTick 影响,独立运行,且无需中断服务程序,非常适合时间测量。

关键寄存器:

  • DWT_CTRL(地址 0xE0001000):控制 CYCCNT 使能。
  • DWT_CYCCNT(地址 0xE0001004):当前计数值,32 位,溢出后回绕。
  • CoreDebug_DEMCR(地址 0xE000EDFC):需置位 TRCENA 位以允许访问 DWT。

2. 使能 CYCCNT

配置步骤:

  1. 使能调试跟踪:CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
  2. 复位 CYCCNT:DWT->CYCCNT = 0;
  3. 使能 CYCCNT:DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;

注意:CYCCNT 频率等于内核时钟(如 168MHz),因此每个周期约 5.95ns,可轻松实现微秒级延时。

基础 DWT 延时实现

以下代码展示最简单的微秒延时函数:

#include "stm32f4xx.h"

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

void delay_us(uint32_t us) {
    uint32_t start = DWT->CYCCNT;
    uint32_t ticks = us * (SystemCoreClock / 1000000);
    while ((DWT->CYCCNT - start) < ticks);
}

此函数在无中断干扰时精度极高,但若延时期间发生中断,则实际耗时 = 延时时间 + 中断处理时间,导致时序错误。

中断嵌套冲突分析

1. 冲突场景

假设系统中有定时器中断(优先级 2)和外部中断(优先级 1),当执行 delay_us(10) 时,若外部中断触发,CPU 跳转执行中断服务程序(ISR),此时 DWT 计数器继续递增,但延时代码暂停。若 ISR 耗时 5us,则实际延时变为 15us,破坏了时序。

2. 冲突本质

DWT 延时基于“绝对时间差”,而中断打断了“等待循环”的连续性。解决思路有两种:

  • 在延时期间屏蔽所有中断(简单但影响实时性)。
  • 记录中断耗时并补偿(复杂但更优雅)。

本文采用“临界区保护”策略,通过短暂关闭中断来确保延时完整性,同时利用 DWT 的高精度特性,将中断关闭时间控制在微秒级,避免系统响应延迟过大。

中断安全的 DWT 延时实现

1. 设计思路

在进入延时前,保存当前中断状态并关闭所有可屏蔽中断(PRIMASK),执行延时循环,然后恢复中断状态。这样,延时期间不会被中断打断,保证了时序精确。但需注意:

  • 延时时间不宜过长(建议 < 100us),否则影响实时性。
  • 若延时期间有高优先级中断(如 NMI)仍会打断,但此类中断极少。

2. 代码实现

#include "stm32f4xx.h"

static inline void DWT_Init(void) {
    CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
    DWT->CYCCNT = 0;
    DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
}

void delay_us_safe(uint32_t us) {
    uint32_t ticks = us * (SystemCoreClock / 1000000);
    uint32_t start;
    uint32_t primask;

    // 保存 PRIMASK 并关闭中断
    primask = __get_PRIMASK();
    __disable_irq();

    start = DWT->CYCCNT;
    while ((DWT->CYCCNT - start) < ticks);

    // 恢复中断状态
    __set_PRIMASK(primask);
}

3. 注意事项

  • 使用 __get_PRIMASK()__set_PRIMASK() 需要包含 core_cm4.h(通常已包含)。
  • 若延时时间超过 65535us(约 65ms),需考虑 CYCCNT 溢出,但 32 位计数器在 168MHz 下溢出周期约 25.6 秒,实际无需担心。
  • 在 FreeRTOS 等 RTOS 环境中,关闭中断可能导致调度延迟,需评估是否可接受。

进阶:中断补偿方案(可选)

若需在延时期间响应中断,可采用“时间戳补偿”方法:在 ISR 中记录中断进入和退出时的 CYCCNT 值,然后调整延时剩余时间。但实现复杂,且需修改所有 ISR,不推荐用于通用场景。

完整示例:驱动 WS2812 灯带

WS2812 需要精确的 800kHz 时序,微秒延时至关重要。以下示例展示如何使用 DWT 安全延时驱动 WS2812:

// 发送 0 码(高电平 0.35us,低电平 0.9us)
void ws2812_send_0(void) {
    GPIO_SetBits(GPIOB, GPIO_Pin_0);
    delay_us_safe(0.35);
    GPIO_ResetBits(GPIOB, GPIO_Pin_0);
    delay_us_safe(0.9);
}

// 发送 1 码(高电平 0.9us,低电平 0.35us)
void ws2812_send_1(void) {
    GPIO_SetBits(GPIOB, GPIO_Pin_0);
    delay_us_safe(0.9);
    GPIO_ResetBits(GPIOB, GPIO_Pin_0);
    delay_us_safe(0.35);
}

注意:delay_us_safe 中的中断关闭时间极短(微秒级),对系统影响可忽略。

总结

DWT 提供了高精度的时间基准,结合临界区保护,可实现微秒级精确延时且避免中断嵌套冲突。本文方法适用于 STM32F4 及所有 Cortex-M3/M4 内核,代码简洁高效。在实际项目中,请根据系统实时性需求权衡中断关闭时间,必要时可考虑中断补偿方案。希望本文能助你在嵌入式开发中游刃有余。