STM32F4 上利用 DWT 实现微秒级精确延时并规避中断嵌套风险
👁 1 阅读 · 2026-08-27 · 嵌入式
在嵌入式开发中,精确延时是许多外设时序控制的基础。STM32F4 的 SysTick 常用于毫秒级延时,但在微秒级场景下精度不足且易受中断影响。本文深入讲解如何利用内核调试单元 DWT(Data Watchpoint and Trace)实现高精度微秒延时,并重点分析中断嵌套带来的风险及规避策略,提供完整代码示例,帮助开发者构建可靠的时间基准。
# 引言
在 STM32F4 系列(基于 Cortex-M4 内核)开发中,精确延时常见于传感器读取、通信协议时序(如 DHT11、DS18B20)或 PWM 波形模拟。SysTick 定时器虽然简单,但若在中断服务函数(ISR)中调用延时,或系统时钟频率较高时,其基于中断的计数方式会导致精度下降。DWT 单元提供了一种基于 CPU 周期计数的非侵入式延时方案,精度可达 1 个时钟周期,且不依赖中断,非常适合微秒级延时。
# DWT 原理
DWT 是 Cortex-M3/M4 内核自带的调试组件,其中 CYCCNT 寄存器是一个 32 位自由运行计数器,每个 CPU 时钟周期递增一次。当计数器溢出(从 0xFFFFFFFF 回绕到 0)时,可触发溢出中断,但通常我们只需读取其值计算时间差。
关键寄存器:
- `DWT_CTRL`(地址 0xE0001000):控制 CYCCNT 使能。
- `DWT_CYCCNT`(地址 0xE0001004):当前周期计数。
- `CoreDebug_DEMCR`(地址 0xE000EDFC):需要设置 TRCENA 位以允许访问 DWT。
## 延时原理
延时函数通过读取起始 CYCCNT 值,然后循环等待当前值减去起始值达到所需周期数。由于是纯轮询,不触发中断,因此避免了中断嵌套导致的延时抖动。
# 配置步骤
1. **使能 DWT 访问**:设置 `CoreDebug->DEMCR` 的位 24(TRCENA)。
2. **复位并启动 CYCCNT**:将 `DWT->CYCCNT` 清零,然后设置 `DWT->CTRL` 的位 0(CYCCNTENA)为 1。
3. **编写延时函数**:根据系统时钟频率(如 168MHz)计算周期数,并处理计数器回绕。
# 代码实现
以下代码适用于 STM32F4 系列,系统时钟为 168MHz(可通过 `SystemCoreClock` 获取)。
```c
#include "stm32f4xx.h"
// 初始化 DWT 延时功能
void DWT_Init(void) {
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; // 使能 DWT 访问
DWT->CYCCNT = 0; // 清零计数器
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; // 使能 CYCCNT
}
// 微秒延时函数,us 范围 1~1000000
void DWT_Delay_us(uint32_t us) {
uint32_t start = DWT->CYCCNT;
uint32_t ticks = us * (SystemCoreClock / 1000000); // 计算所需周期数
while ((DWT->CYCCNT - start) < ticks); // 等待,注意无符号减法处理回绕
}
// 毫秒延时(基于微秒延时)
void DWT_Delay_ms(uint32_t ms) {
for (uint32_t i = 0; i < ms; i++) {
DWT_Delay_us(1000);
}
}
```
**使用示例**:
```c
int main(void) {
DWT_Init();
while (1) {
GPIO_ToggleBits(GPIOA, GPIO_Pin_5); // 翻转 LED
DWT_Delay_us(500); // 精确延时 500us
}
}
```
# 中断嵌套风险与规避
## 风险分析
- **中断抢占**:若在低优先级中断中调用 DWT_Delay_us,高优先级中断可能抢占执行,导致延时时间被拉长(因为轮询循环被暂停)。
- **计数器溢出**:CYCCNT 是 32 位,在 168MHz 下约 25.6 秒回绕一次。若延时期间发生回绕,无符号减法仍能正确计算差值,但若中断导致延时超过回绕周期,则结果错误。
- **临界区问题**:若在延时过程中被中断打断,且中断内也调用延时,可能造成嵌套,增加不确定性。
## 规避策略
1. **避免在 ISR 中调用延时**:尽量将延时操作移至主循环或任务中。
2. **使用临界区保护**:若必须在 ISR 中延时,可暂时屏蔽高优先级中断(但需谨慎,避免影响实时性)。
3. **处理回绕**:使用无符号减法(如 `(uint32_t)(DWT->CYCCNT - start)`)自动处理回绕,但需确保延时时间小于回绕周期。
4. **校准时钟**:若系统时钟不是整数 MHz,需用浮点或定点计算周期数,避免累积误差。
**改进版延时函数(支持任意时钟频率)**:
```c
void DWT_Delay_us_accurate(uint32_t us) {
uint32_t start = DWT->CYCCNT;
uint64_t ticks = (uint64_t)us * SystemCoreClock / 1000000; // 使用 64 位避免溢出
while ((uint32_t)(DWT->CYCCNT - start) < (uint32_t)ticks);
}
```
# 注意事项
- **初始化时机**:DWT_Init() 应在系统时钟配置完成后调用,且只需调用一次。
- **调试影响**:若使用调试器(如 ST-Link)暂停程序,CYCCNT 会停止计数,导致延时失效,因此发布版本应禁用调试。
- **功耗**:DWT 计数器在睡眠模式下可能停止,不适合低功耗场景。
- **替代方案**:对于长时间延时(>1ms),建议使用 SysTick 或定时器,以降低 CPU 占用。
# 总结
利用 DWT 的 CYCCNT 实现微秒延时,精度高、代码简洁,且不依赖中断,有效规避了中断嵌套风险。通过合理设计,可确保在复杂中断环境下仍能获得稳定时序。开发者应根据实际需求选择延时方案,并注意初始化与边界条件,以构建可靠的嵌入式系统。