引言

在 STM32F4 开发中,微秒级延时是驱动时序敏感外设(如 DHT11、DS18B20、WS2812)的刚需。传统基于 SysTick 的延时函数(如 HAL_Delay)精度受中断影响大,且最小粒度通常为 1ms,无法满足微秒级需求。而 STM32F4 内核自带的 DWT(Data Watchpoint and Trace)单元提供了 32 位周期计数器,可精确到内核时钟周期,是实现微秒级延时的理想方案。本文将深入讲解 DWT 原理、实现方法,并重点解决中断干扰问题。

DWT 原理简介

DWT 是 Cortex-M4 内核调试组件的一部分,其核心寄存器为 DWT->CYCCNT,一个 32 位向上计数器,每个内核时钟周期(HCLK)递增一次。当计数器溢出(0xFFFFFFFF)后回绕到 0,因此可测量最长约 2^32 / 168MHz ≈ 25.6 秒(以 STM32F407 最大主频 168MHz 为例)。

要使用 DWT,需按以下步骤初始化:

  1. 使能 DWT 单元:设置 CoreDebug->DEMCR 的位 24(TRCENA)。
  2. 复位 CYCCNT 计数器:写 0 到 DWT->CYCCNT
  3. 使能 CYCCNT 计数器:设置 DWT->CTRL 的位 0(CYCCNTENA)。

初始化后,DWT->CYCCNT 会持续递增,我们只需读取其差值即可计算耗时。

实现微秒级延时

核心代码

以下代码基于 STM32F4 系列(HCLK 频率可配置),通过宏定义获取内核时钟频率,实现 delay_usdelay_ms 函数。

#include "stm32f4xx.h"

// 获取内核时钟频率(HCLK),可根据实际配置修改
#define CORE_CLOCK_HZ  168000000UL  // 假设主频168MHz

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

// 微秒延时(不关中断版本,适合非临界场景)
void delay_us_noirq(uint32_t us)
{
    uint32_t start = DWT->CYCCNT;
    uint32_t ticks = us * (CORE_CLOCK_HZ / 1000000UL);
    while ((DWT->CYCCNT - start) < ticks);
}

// 微秒延时(关中断版本,规避中断干扰)
void delay_us(uint32_t us)
{
    uint32_t primask;
    primask = __get_PRIMASK();  // 保存当前中断状态
    __disable_irq();            // 关闭全局中断
    uint32_t start = DWT->CYCCNT;
    uint32_t ticks = us * (CORE_CLOCK_HZ / 1000000UL);
    while ((DWT->CYCCNT - start) < ticks);
    __set_PRIMASK(primask);     // 恢复中断状态
}

// 毫秒延时(基于微秒延时)
void delay_ms(uint32_t ms)
{
    while (ms--)
        delay_us(1000);
}

代码说明

  • DWT_Init() 必须在系统启动后调用一次,建议放在 main 函数开头。
  • delay_us_noirq 不关中断,适用于对延时精度要求不高或中断不频繁的场景。
  • delay_us 通过 __disable_irq() 关闭全局中断,确保延时期间不被中断打断,从而保证精度。但注意,关中断时间过长会影响系统实时性,因此仅建议在短延时(<100us)时使用。
  • 计算 ticks 时,CORE_CLOCK_HZ / 1000000UL 得到每微秒的周期数,乘以 us 得到总周期数。注意防止乘法溢出,us 最大约 25 秒(对于 168MHz)。

中断干扰问题与规避策略

为什么中断会干扰延时?

当延时函数执行期间发生中断,CPU 会跳转到中断服务程序(ISR)执行,导致 DWT->CYCCNT 的读取和比较操作被延迟,实际延时时间会大于设定值。例如,一个 10us 的延时,若期间发生一个 5us 的中断,实际耗时可能变成 15us,导致时序错误。

规避方法

  1. 关中断(临界区保护):在延时期间关闭全局中断,这是最直接的方法。但需注意:

    • 关中断时间过长会影响系统实时性,因此仅适用于短延时。
    • 若延时期间有高优先级中断(如系统时钟)需要响应,则不建议使用。
  2. 使用中断优先级分组:将延时相关的中断优先级设为最低,这样即使发生中断,也会在延时结束后才响应。但这种方法不保证绝对精确,且配置复杂。

  3. 使用 DWT 比较器:DWT 支持比较器功能,当 CYCCNT 达到设定值时触发中断,但这种方法需要额外的中断处理,且不适合简单延时。

  4. 混合策略:对于长延时(>1ms),使用 SysTick 或定时器中断;对于短延时(<100us),使用 DWT 并关中断。这样兼顾精度和实时性。

完整示例:驱动 DHT11 温湿度传感器

DHT11 的时序要求微秒级精度,非常适合用 DWT 延时。以下是一个简化示例:

// 假设已初始化 DWT 和 GPIO
#define DHT11_PIN  GPIO_PIN_0
#define DHT11_PORT GPIOA

void DHT11_Start(void)
{
    // 主机拉低至少 18ms
    HAL_GPIO_WritePin(DHT11_PORT, DHT11_PIN, GPIO_PIN_RESET);
    delay_ms(20);
    // 拉高 20-40us
    HAL_GPIO_WritePin(DHT11_PORT, DHT11_PIN, GPIO_PIN_SET);
    delay_us(30);
    // 释放总线,读取响应
    HAL_GPIO_WritePin(DHT11_PORT, DHT11_PIN, GPIO_PIN_RESET);
}

uint8_t DHT11_ReadByte(void)
{
    uint8_t data = 0;
    for (int i = 0; i < 8; i++)
    {
        // 等待低电平结束(50us)
        while (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) == GPIO_PIN_RESET);
        // 延时 40us,若仍为高电平则为1,否则为0
        delay_us(40);
        if (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) == GPIO_PIN_SET)
            data |= (1 << (7 - i));
        // 等待高电平结束
        while (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) == GPIO_PIN_SET);
    }
    return data;
}

注意事项

  • 时钟频率一致性CORE_CLOCK_HZ 必须与实际 HCLK 一致,否则延时不准。可通过 SystemCoreClock 变量获取,或使用 HAL_RCC_GetHCLKFreq()
  • DWT 初始化时机:必须在启用 DWT 后才能使用,否则 DWT->CYCCNT 可能为 0 或不可用。
  • 溢出处理DWT->CYCCNT 是 32 位,计算差值时使用无符号减法,可自动处理回绕,但需确保延时时间小于 2^32 周期。
  • 关中断副作用__disable_irq() 会关闭所有可屏蔽中断,若系统依赖中断(如 RTOS 调度),需谨慎使用。在 RTOS 中,建议使用 taskENTER_CRITICAL() 等机制。
  • 编译器优化:延时循环中的变量应使用 volatile 修饰,防止编译器优化导致延时失效。

总结

DWT 提供了高精度的周期计数器,是实现微秒级延时的利器。通过关中断保护,可以规避中断干扰,确保时序准确性。但在实际项目中,需根据延时长度和系统实时性要求,灵活选择延时方案。希望本文能帮助你在 STM32F4 开发中更从容地应对时序挑战。