STM32F4 上利用 DWT 实现微秒级精确延时并规避中断干扰的实战方法
👁 2 阅读 · 2026-08-27 · 嵌入式
在嵌入式开发中,微秒级延时是驱动传感器、通信协议时序的关键。STM32F4 的 SysTick 常被占用,而 DWT(Data Watchpoint and Trace)单元提供了高精度周期计数器,可轻松实现非阻塞延时。本文深入剖析 DWT 原理,给出完整配置代码,并重点解决中断嵌套导致的延时误差问题,助你掌握这一高效技巧。
# 引言
在 STM32F4 开发中,精确的微秒级延时(如 1-100us)常用于 I2C 时序模拟、超声波测距、DHT11 读取等场景。SysTick 虽常用,但若被 RTOS 或主循环占用,则难以复用。DWT 作为 Cortex-M4 内核自带的调试组件,提供了 CYCCNT 计数器,可精确记录 CPU 周期,非常适合实现高精度延时。然而,中断处理会打断延时循环,导致时间溢出,本文提供两种规避策略,确保延时可靠性。
# DWT 原理
DWT(Data Watchpoint and Trace)是 Cortex-M 内核的调试单元,其中 CYCCNT 是一个 32 位自由运行计数器,每个 CPU 时钟周期递增。当计数器溢出(0xFFFFFFFF -> 0)时,可配置触发事件。利用 CYCCNT,我们可读取当前周期数,通过计算差值实现延时。
关键寄存器:
- **DWT_CTRL**(地址 0xE0001000):控制 CYCCNT 使能。
- **DWT_CYCCNT**(地址 0xE0001004):当前计数值。
- **CoreDebug_DEMCR**(地址 0xE000EDFC):需置位 TRCENA 位以启用 DWT。
# 硬件与软件准备
- 硬件:任何 STM32F4 系列开发板(如 STM32F407 Discovery)。
- 软件:STM32CubeIDE 或 Keil MDK,使用 HAL 库或标准库均可。
- 时钟:假设系统时钟为 168MHz(F4 最高主频),则 1us = 168 周期。
# 配置步骤
## 1. 使能 DWT 并启动 CYCCNT
在系统初始化时,调用以下函数:
```c
void DWT_Init(void) {
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; // 使能 DWT 访问
DWT->CYCCNT = 0; // 清零计数器
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; // 使能 CYCCNT
}
```
## 2. 实现微秒延时函数(基础版)
```c
void delay_us_basic(uint32_t us) {
uint32_t start = DWT->CYCCNT;
uint32_t ticks = us * (SystemCoreClock / 1000000); // 计算所需周期数
while ((DWT->CYCCNT - start) < ticks); // 等待差值达到目标
}
```
注意:`SystemCoreClock` 是全局变量,由系统时钟初始化设置。若时钟为 168MHz,则 `ticks = us * 168`。
## 3. 中断干扰问题分析
当延时期间发生中断(如 SysTick、UART),CPU 会跳转执行中断服务函数,导致 CYCCNT 计数暂停(因为 CYCCNT 只在 CPU 运行时递增)。若中断耗时较长,延时时间会变长,甚至超出预期。例如,延时 10us,但中断占用了 5us,实际耗时 15us。
## 4. 规避中断干扰的两种策略
### 策略一:临界区保护(简单粗暴)
在延时期间关闭所有中断,但会阻塞实时性,仅适用于短延时。
```c
void delay_us_critical(uint32_t us) {
__disable_irq(); // 关闭全局中断
uint32_t start = DWT->CYCCNT;
uint32_t ticks = us * (SystemCoreClock / 1000000);
while ((DWT->CYCCNT - start) < ticks);
__enable_irq(); // 重新开启
}
```
缺点:若中断有实时要求(如通信),则不可取。
### 策略二:补偿式延时(推荐)
利用 DWT 的溢出中断或周期检测,在中断中记录时间戳,延时函数计算实际消耗并调整。但更简单的方法是:在延时循环中,不断检查是否发生中断,若发生则重新计算剩余时间。
实现思路:
- 使用一个全局变量 `g_irq_occurred`,在中断服务函数中置位。
- 延时函数中,每次循环检查该标志,若置位则重新计算剩余周期。
```c
volatile uint32_t g_irq_count = 0; // 在中断中递增
void SysTick_Handler(void) {
g_irq_count++;
}
void delay_us_compensated(uint32_t us) {
uint32_t start = DWT->CYCCNT;
uint32_t ticks = us * (SystemCoreClock / 1000000);
uint32_t last_irq_count = g_irq_count;
while (1) {
uint32_t elapsed = DWT->CYCCNT - start;
if (elapsed >= ticks) break;
if (g_irq_count != last_irq_count) { // 检测到中断发生
// 中断期间 CYCCNT 未计数,但时间已流逝,需要补偿
// 这里简单处理:重置开始时间,但会导致总时间偏长
// 更精确:记录中断耗时,但需在中断中获取时间戳
start = DWT->CYCCNT; // 重新开始,但会丢失已等待时间
last_irq_count = g_irq_count;
}
}
}
```
但上述补偿并不精确。更优方案:在中断服务函数中,记录中断开始和结束的 CYCCNT 值,然后延时函数减去中断耗时。
```c
volatile uint32_t irq_start_cyccnt, irq_end_cyccnt;
volatile uint32_t irq_duration_cycles = 0;
void SysTick_Handler(void) {
irq_start_cyccnt = DWT->CYCCNT;
// 中断处理...
irq_end_cyccnt = DWT->CYCCNT;
irq_duration_cycles += (irq_end_cyccnt - irq_start_cyccnt);
}
void delay_us_precise(uint32_t us) {
uint32_t start = DWT->CYCCNT;
uint32_t ticks = us * (SystemCoreClock / 1000000);
uint32_t accumulated_irq = 0;
while (1) {
uint32_t elapsed = DWT->CYCCNT - start;
if (elapsed >= ticks + accumulated_irq) break;
// 读取并清零中断累计时间(需原子操作)
__disable_irq();
uint32_t irq_dur = irq_duration_cycles;
irq_duration_cycles = 0;
__enable_irq();
accumulated_irq += irq_dur;
}
}
```
此方法在中断频繁时仍能保持高精度,但需注意中断中读取 CYCCNT 的时机,以及避免死锁。
# 完整代码示例
以下是一个完整的示例,包含初始化、延时函数和测试主函数。
```c
#include "stm32f4xx.h"
// 系统时钟频率(假设 168MHz)
extern uint32_t SystemCoreClock;
// 中断累计周期变量
volatile uint32_t irq_duration_cycles = 0;
// DWT 初始化
void DWT_Init(void) {
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CYCCNT = 0;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
}
// 基础延时(无中断补偿)
void delay_us_basic(uint32_t us) {
uint32_t start = DWT->CYCCNT;
uint32_t ticks = us * (SystemCoreClock / 1000000);
while ((DWT->CYCCNT - start) < ticks);
}
// 精确延时(带中断补偿)
void delay_us_precise(uint32_t us) {
uint32_t start = DWT->CYCCNT;
uint32_t ticks = us * (SystemCoreClock / 1000000);
uint32_t accumulated_irq = 0;
while (1) {
uint32_t elapsed = DWT->CYCCNT - start;
if (elapsed >= ticks + accumulated_irq) break;
// 读取并清零中断累计周期
__disable_irq();
uint32_t irq_dur = irq_duration_cycles;
irq_duration_cycles = 0;
__enable_irq();
accumulated_irq += irq_dur;
}
}
// 示例中断服务函数(以 SysTick 为例)
void SysTick_Handler(void) {
uint32_t start = DWT->CYCCNT;
// 模拟中断处理,如翻转 LED
// ...
uint32_t end = DWT->CYCCNT;
irq_duration_cycles += (end - start);
}
int main(void) {
SystemInit();
DWT_Init();
// 配置 SysTick 1ms 中断(示例)
SysTick_Config(SystemCoreClock / 1000);
while (1) {
// 精确延时 100us
delay_us_precise(100);
// 其他操作
}
}
```
# 注意事项
- **时钟频率**:确保 `SystemCoreClock` 与实际一致,否则延时误差大。
- **CYCCNT 溢出**:32 位计数器在 168MHz 下约 25.6 秒溢出一次,对于微秒延时无影响,但若延时超过此范围需处理溢出。
- **中断补偿的原子性**:读取和清零 `irq_duration_cycles` 时需关中断,防止竞争。
- **DWT 在低功耗模式**:进入停止模式后 CYCCNT 停止,需唤醒后重新初始化。
- **调试器影响**:使用调试器时,CYCCNT 可能受调试暂停影响,但正常运行无碍。
- **替代方案**:若需纳秒级精度,可考虑使用定时器输入捕获,但 DWT 已足够。
# 总结
DWT 提供了轻量级的高精度延时方案,通过合理的中断补偿,可有效规避中断干扰。本文的方法适用于大多数嵌入式场景,且代码可移植到其他 Cortex-M3/M4 系列。掌握这一技巧,能提升你的时序控制能力,为复杂驱动开发打下基础。