一、为什么需要 DWT 延时?
在 STM32F4 开发中,常见的延时方案有:
- 软件循环延时:简单但精度极差,受编译器优化影响大。
- SysTick 延时:精度尚可,但 SysTick 常被操作系统或中断服务程序占用,且延时过程中若发生中断,会暂停计时,导致时间偏长。
- 通用定时器延时:占用外设资源,且同样存在中断抢占问题。
核心痛点:任何依赖中断或外设的延时,在中断频繁触发时都会产生累积误差。而 DWT 的 CYCCNT 是内核级计数器,不依赖外设时钟,直接以 CPU 主频计数,且不受中断影响(除非内核停止),因此能实现真正的微秒级精确延时。
二、DWT 原理简介
DWT(Data Watchpoint and Trace)是 Cortex-M4 内核自带的调试组件,其中包含一个 32 位的自由运行计数器 CYCCNT,每个 CPU 时钟周期自动加 1。当计数溢出(0xFFFFFFFF)后回绕到 0,周期约为 4.29 秒(以 168MHz 主频计算)。
要使用 CYCCNT,需完成三步初始化:
- 使能 DWT 单元(
DWT->CTRL的NOCYCCNT位清零)。 - 使能 CYCCNT 计数器(
DWT->CTRL的CYCCNTENA位置 1)。 - 复位计数值(
DWT->CYCCNT = 0)。
关键寄存器:
-
DWT->CTRL:控制寄存器,位 0 为 CYCCNTENA,位 25 为 NOCYCCNT。 -
DWT->CYCCNT:当前计数值(只读)。
三、实现方案
1. 初始化 DWT
#include "stm32f4xx.h"
/**
* @brief 初始化 DWT 延时功能
* @param 无
* @retval 无
*/
void DWT_Init(void)
{
// 使能 DWT 访问(若未使能)
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
// 复位 CYCCNT 计数器
DWT->CYCCNT = 0;
// 使能 CYCCNT 计数器(清零 NOCYCCNT,置位 CYCCNTENA)
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
}
2. 微秒延时函数
/**
* @brief 微秒延时(基于 DWT,抗中断干扰)
* @param us: 延时微秒数(最大约 4.29e6 / 主频MHz,以 168MHz 为例约 25500us)
* @retval 无
*/
void DWT_Delay_us(uint32_t us)
{
uint32_t startTick = DWT->CYCCNT;
uint32_t delayTicks = us * (SystemCoreClock / 1000000); // 微秒转时钟周期数
uint32_t currentTick;
// 循环直到达到目标周期数(处理溢出回绕)
do {
currentTick = DWT->CYCCNT;
// 计算已流逝的周期数,考虑回绕
if (currentTick >= startTick) {
// 未回绕
if ((currentTick - startTick) >= delayTicks) break;
} else {
// 已回绕,则流逝周期 = (0xFFFFFFFF - startTick) + currentTick + 1
if ((0xFFFFFFFF - startTick + currentTick + 1) >= delayTicks) break;
}
} while (1);
}
优化版本(更简洁,利用无符号减法自动处理回绕):
void DWT_Delay_us(uint32_t us)
{
uint32_t startTick = DWT->CYCCNT;
uint32_t delayTicks = us * (SystemCoreClock / 1000000);
uint32_t elapsed = 0;
while (elapsed < delayTicks) {
uint32_t currentTick = DWT->CYCCNT;
elapsed = currentTick - startTick; // 无符号减法,自动处理回绕
}
}
注意:currentTick - startTick 在回绕时依然正确,因为无符号整数减法遵循模运算规则。
3. 毫秒延时(可选)
void DWT_Delay_ms(uint32_t ms)
{
while (ms--) {
DWT_Delay_us(1000);
}
}
4. 完整示例(以 LED 翻转测试)
int main(void)
{
// 初始化系统时钟(默认 168MHz)
SystemInit();
// 初始化 LED GPIO(略)
LED_Init();
// 初始化 DWT 延时
DWT_Init();
while (1) {
LED_Toggle();
DWT_Delay_us(500); // 精确延时 500us
}
}
四、注意事项与避坑指南
-
确保 SystemCoreClock 正确:
SystemCoreClock变量在system_stm32f4xx.c中定义,若手动修改时钟配置,需同步更新该变量,否则延时时间会偏差。 - 最大延时限制:CYCCNT 为 32 位,在 168MHz 下最大可延时约 25.5 毫秒(0xFFFFFFFF / 168e6 ≈ 25.5ms)。若需更长延时,请循环调用或改用其他方案。
- 中断干扰:DWT 延时不受中断影响,但若中断服务函数执行时间过长,会延迟主循环中延时后的代码执行,这属于正常现象,延时本身依然准确。
- 低功耗模式:在睡眠或停止模式下,CPU 时钟停止,CYCCNT 也会停止,因此 DWT 延时在低功耗唤醒后需重新校准。
- 调试器影响:若使用调试器(如 ST-Link)进行单步调试,CYCCNT 会随 CPU 暂停而暂停,这可能导致延时异常,属正常现象。
-
代码优化:建议将
DWT_Delay_us函数定义为内联函数或使用__attribute__((always_inline)),以减少函数调用开销,提高精度。
五、总结
DWT 延时方案利用内核级计数器,实现了微秒级高精度延时,且不受中断干扰,是嵌入式开发中非常实用的技巧。通过简单的初始化,即可替代传统的 SysTick 或定时器方案,尤其适用于对时序要求严格的传感器驱动和通信协议实现。希望本文能帮助你解决延时不准的困扰,提升代码的可靠性。