STM32F4 微秒级精确延时:DWT 内核定时器实战与中断干扰规避指南
👁 1 阅读 · 2026-08-27 · 嵌入式
在嵌入式开发中,微秒级延时是驱动时序外设(如 DHT11、DS18B20、WS2812)的刚需。传统 SysTick 延时受中断影响大,而 STM32F4 的 DWT(Data Watchpoint and Trace)单元提供了内核级周期计数器,可实现高精度、低干扰的延时。本文深入剖析 DWT 原理,给出基于 STM32F4 的完整实现代码,并重点讲解如何通过临界区保护规避中断干扰,确保延时准确性。
# 引言
在 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_us` 和 `delay_ms` 函数。
```c
#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 延时。以下是一个简化示例:
```c
// 假设已初始化 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 开发中更从容地应对时序挑战。