STM32F4 上利用 DWT 实现微秒级精确延时并规避中断嵌套冲突
👁 1 阅读 · 2026-08-27 · 嵌入式
在嵌入式开发中,精确的微秒级延时是许多外设时序控制的基础。STM32F4 的 SysTick 常用于毫秒延时,但受中断影响较大。本文深入讲解如何利用内核调试单元 DWT(Data Watchpoint and Trace)的 CYCCNT 计数器实现高精度微秒延时,并重点分析中断嵌套场景下的冲突问题,提供一套安全可靠的实现方案,帮助开发者提升时序控制的精准度与鲁棒性。
# 引言
在 STM32F4 系列(如 STM32F407)开发中,微秒级延时常见于传感器读取、通信协议时序(如 DHT11、DS18B20)或 PWM 信号模拟。SysTick 虽然简单,但若被中断频繁抢占,会导致延时误差累积。而 DWT 的 CYCCNT 是一个 32 位自由运行计数器,每个内核时钟周期递增一次,不受普通中断影响,非常适合精确延时。但若在中断服务函数(ISR)中使用 DWT 延时,且该中断可被更高优先级中断嵌套,则可能产生冲突。本文将深入剖析原理,并给出规避冲突的完整方案。
# DWT 工作原理
DWT 是 Cortex-M4 内核提供的调试组件,其中 CYCCNT 计数器用于统计 CPU 周期数。其工作特性:
- 32 位计数器,从 0 到 0xFFFFFFFF 循环计数。
- 每个 HCLK(内核时钟)周期加 1,例如 STM32F407 主频 168MHz 时,1 微秒对应 168 个周期。
- 可通过 DWT_CTRL 寄存器的 CYCCNTENA 位使能。
实现延时的核心思路:读取当前 CYCCNT 值,计算目标周期数,轮询直到差值达到目标。但需注意计数器回绕(wrap-around)问题,需用无符号减法处理。
# 基础 DWT 延时实现
首先,使能 DWT 并校准周期数。代码如下:
```c
// 初始化 DWT
void DWT_Init(void) {
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; // 使能调试跟踪
DWT->CYCCNT = 0; // 清零计数器
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; // 使能 CYCCNT
}
// 获取当前周期计数
static inline uint32_t DWT_GetCycles(void) {
return DWT->CYCCNT;
}
// 微秒延时(基础版,不考虑中断)
void DWT_Delay_us(uint32_t us) {
uint32_t cycles = us * (SystemCoreClock / 1000000); // 每微秒周期数
uint32_t start = DWT_GetCycles();
while ((DWT_GetCycles() - start) < cycles); // 无符号减法处理回绕
}
```
此代码在无中断或低中断频率下工作良好。但若在 ISR 中使用,且该 ISR 可被更高优先级中断抢占,则可能出现问题:
- 问题 1:延时期间被高优先级中断打断,导致实际延时时间变长(因为 CPU 执行中断服务程序,CYCCNT 仍在计数,但延时循环被暂停)。
- 问题 2:若在延时循环中发生嵌套中断,且嵌套中断中也调用 DWT_Delay_us,则内层延时可能覆盖外层延时的起始值,导致外层延时计算错误。
# 中断嵌套冲突分析
假设有一个中等优先级中断 A,其 ISR 中调用 DWT_Delay_us(50)。在延时期间,高优先级中断 B 触发,B 的 ISR 中也调用 DWT_Delay_us(10)。此时:
- 中断 B 的延时基于当前 CYCCNT 值计算,执行 10us 后返回。
- 但中断 A 的延时循环仍在继续,其起始值 start 是 B 执行前的值,而 B 执行期间 CYCCNT 增加了 10us 的周期数,因此 A 的延时总时间变为 50us + 10us = 60us,超出预期。
此外,若 B 的延时时间较长,甚至可能导致 A 的延时循环提前结束(因为 CYCCNT 回绕),但概率极低。
# 规避冲突的解决方案
核心思路:在 DWT 延时期间,临时屏蔽所有可嵌套的中断,确保延时循环不被抢占。但完全屏蔽中断会影响系统实时性,因此需权衡。以下提供两种策略:
## 策略一:临界区保护(推荐)
在延时函数入口关闭中断,出口恢复。使用 `__disable_irq()` 和 `__enable_irq()`,但需注意保存和恢复 PRIMASK 状态,以支持嵌套调用。
```c
// 微秒延时(临界区保护版)
void DWT_Delay_us_safe(uint32_t us) {
uint32_t primask = __get_PRIMASK(); // 保存当前中断屏蔽状态
__disable_irq(); // 关闭所有可屏蔽中断
uint32_t cycles = us * (SystemCoreClock / 1000000);
uint32_t start = DWT_GetCycles();
while ((DWT_GetCycles() - start) < cycles);
__set_PRIMASK(primask); // 恢复原状态
}
```
此方法确保延时期间无中断打断,但会牺牲中断响应时间。适用于延时较短(<100us)且对实时性要求不高的场景。
## 策略二:基于优先级分组屏蔽
若系统使用 NVIC 优先级分组,可仅屏蔽优先级高于当前中断的中断,但实现复杂。简单做法是使用 BASEPRI 寄存器屏蔽优先级低于某值的所有中断,但需确保当前中断优先级高于该值。
```c
// 仅屏蔽优先级低于 threshold 的中断(threshold 为优先级数值,数值越大优先级越低)
void DWT_Delay_us_basepri(uint32_t us, uint32_t threshold) {
uint32_t basepri = __get_BASEPRI();
__set_BASEPRI(threshold << 4); // 设置 BASEPRI,屏蔽优先级数值大于等于 threshold 的中断
uint32_t cycles = us * (SystemCoreClock / 1000000);
uint32_t start = DWT_GetCycles();
while ((DWT_GetCycles() - start) < cycles);
__set_BASEPRI(basepri);
}
```
注意:BASEPRI 只屏蔽优先级数值大于等于设定值的中断,且需在中断中调用时确保当前中断优先级高于 threshold,否则可能死锁。
## 策略三:使用 DWT 比较器(高级)
DWT 具有比较器功能,可设置 CYCCNT 匹配值并触发事件,但需配合中断使用,不适合纯延时。此处不展开。
# 完整示例代码
以下是一个综合示例,展示如何在中断中使用安全延时函数,并测试其准确性。
```c
#include "stm32f4xx.h"
void DWT_Init(void);
void DWT_Delay_us_safe(uint32_t us);
// 测试:定时器触发中断,中断中调用延时
void TIM2_IRQHandler(void) {
if (TIM_GetITStatus(TIM2, TIM_IT_Update)) {
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
// 模拟需要精确延时的操作
DWT_Delay_us_safe(50);
GPIO_ToggleBits(GPIOA, GPIO_Pin_0);
}
}
int main(void) {
// 初始化时钟、GPIO、定时器等(省略)
DWT_Init();
// 配置 TIM2 中断优先级为 2,允许被优先级 1 的中断嵌套
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
while (1) {
// 主循环
}
}
```
# 注意事项
- **时钟频率**:`SystemCoreClock` 需正确反映实际 HCLK,否则延时不准。可在系统初始化后获取。
- **计数器溢出**:32 位计数器在 168MHz 下约 25.6 秒回绕一次,使用无符号减法可正确处理。
- **临界区开销**:关闭中断会引入少量延迟(约几个周期),对于极短延时(<5us)可能影响精度,需评估。
- **嵌套调用**:使用 `__get_PRIMASK` 保存状态,可支持延时函数嵌套调用(如一个 ISR 中调用另一个延时函数)。
- **DWT 初始化**:必须在系统启动后尽早调用 `DWT_Init()`,否则 CYCCNT 未使能。
- **调试影响**:若使用调试器,CYCCNT 可能受调试暂停影响,但在正常运行中无影响。
# 总结
利用 DWT 的 CYCCNT 计数器可实现微秒级精确延时,但中断嵌套会引入冲突。通过临界区保护或 BASEPRI 屏蔽,可有效规避。本文提供的方案适用于大多数场景,开发者可根据系统实时性要求选择合适策略。掌握这一技巧,能显著提升嵌入式时序控制的可靠性。