利用 ARM Cortex-M 内核 DWT 计数器实现微秒级精确延时,不占用定时器资源
👁 2 阅读 · 2026-08-27 · 嵌入式
在嵌入式开发中,精确延时是常见需求,但传统定时器延时往往占用宝贵的外设资源。ARM Cortex-M 内核内置的 DWT(Data Watchpoint and Trace)单元提供了一个免费的 32 位周期计数器,可轻松实现微秒级延时,且不占用任何定时器。本文将深入剖析 DWT 的工作原理,并给出基于 STM32 的完整实现代码,助你提升代码效率与实时性。
# 利用 ARM Cortex-M 内核 DWT 计数器实现微秒级精确延时
## 为什么需要 DWT 延时?
在嵌入式系统中,延时函数无处不在:传感器采样间隔、通信协议时序、LED 闪烁等。传统方案通常使用 SysTick 定时器或通用定时器(如 TIMx),但存在以下痛点:
- **SysTick 被 RTOS 占用**:在 FreeRTOS 等系统中,SysTick 已被系统节拍占用,无法直接用于延时。
- **通用定时器资源紧张**:每个定时器都有特定功能(PWM、输入捕获等),用于延时是一种浪费。
- **软件循环精度差**:简单的 `for` 循环延时受编译器优化、中断影响,误差可达数十微秒。
ARM Cortex-M3/M4/M7 内核内置的 DWT 单元提供了一个 32 位自由运行计数器(CYCCNT),它直接以内核时钟频率递增,精度可达 1 个时钟周期。利用它,我们可以实现不占用任何外设的微秒级延时。
## DWT 工作原理
DWT 是 CoreSight 调试组件的一部分,主要用于数据观察和跟踪。其中,CYCCNT 寄存器是一个 32 位计数器,每个内核时钟周期自动加 1,溢出后回绕(约 2^32 周期)。
要使用 CYCCNT,需完成以下步骤:
1. **使能 DWT 单元**:访问 DWT 控制寄存器(DWT_CTRL)的 CYCCNTENA 位(bit 0)。
2. **使能 CYCCNT 计数器**:访问 DWT_CYCCNT 寄存器,先清零再使能。
3. **读取当前值**:通过 `DWT->CYCCNT` 读取。
注意:DWT 属于调试组件,在正常运行时也可访问,但需确保内核调试接口未被禁用。在大多数 Cortex-M 芯片(如 STM32F1/F4/H7)上,默认即可使用。
## 实现微秒级延时
### 1. 宏定义与初始化
首先,我们需要获取内核时钟频率。在 STM32 中,可通过 `SystemCoreClock` 变量获取(单位 Hz)。延时函数的核心是计算目标周期数:`目标周期 = 微秒数 * (系统时钟 / 1000000)`。
```c
#include "stm32f4xx.h" // 以 STM32F4 为例
// 使能 DWT 计数器
void DWT_Init(void)
{
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; // 使能 DWT 访问
DWT->CYCCNT = 0; // 清零计数器
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; // 使能 CYCCNT
}
// 微秒延时函数
void DWT_Delay_us(uint32_t us)
{
uint32_t start = DWT->CYCCNT;
uint32_t ticks = us * (SystemCoreClock / 1000000);
while ((DWT->CYCCNT - start) < ticks); // 注意无符号减法处理回绕
}
```
### 2. 处理计数器回绕
由于 CYCCNT 是 32 位,当延时较长时可能发生回绕。但利用无符号整数减法特性,`(DWT->CYCCNT - start)` 会自动处理回绕,只要延时周期数小于 2^32,结果就是正确的。例如,在 168MHz 下,最大可延时约 25 秒(2^32 / 168e6 ≈ 25.5s),足以覆盖绝大多数场景。
### 3. 优化:避免除法运算
上述代码中 `SystemCoreClock / 1000000` 是常量,编译器会优化为乘法,但为了更高效,可预计算每微秒的周期数。
```c
static uint32_t us_ticks = 0;
void DWT_Init(void)
{
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CYCCNT = 0;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
us_ticks = SystemCoreClock / 1000000;
}
void DWT_Delay_us(uint32_t us)
{
uint32_t start = DWT->CYCCNT;
uint32_t ticks = us * us_ticks;
while ((DWT->CYCCNT - start) < ticks);
}
```
### 4. 毫秒级延时封装
基于微秒延时,可轻松实现毫秒延时:
```c
void DWT_Delay_ms(uint32_t ms)
{
while (ms--) {
DWT_Delay_us(1000);
}
}
```
## 完整示例代码
以下是一个完整的 STM32F4 示例,演示 DWT 延时的使用:
```c
#include "stm32f4xx.h"
static uint32_t us_ticks;
void DWT_Init(void)
{
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CYCCNT = 0;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
us_ticks = SystemCoreClock / 1000000;
}
void DWT_Delay_us(uint32_t us)
{
uint32_t start = DWT->CYCCNT;
uint32_t ticks = us * us_ticks;
while ((DWT->CYCCNT - start) < ticks);
}
void DWT_Delay_ms(uint32_t ms)
{
while (ms--) DWT_Delay_us(1000);
}
int main(void)
{
DWT_Init();
// 初始化 GPIO 等...
while (1) {
// 精确延时 500us
DWT_Delay_us(500);
// 精确延时 2ms
DWT_Delay_ms(2);
}
}
```
## 注意事项
- **内核时钟频率**:`SystemCoreClock` 必须在系统时钟初始化后正确设置,否则延时不准。若使用外部晶振或 PLL,请确保时钟配置正确。
- **中断影响**:DWT 延时是忙等待,期间会阻塞 CPU,无法响应中断(除非中断优先级高于当前执行)。若需在中断中延时,请确保延时时间远小于中断周期。
- **低功耗模式**:在睡眠或停止模式下,内核时钟可能停止,DWT 计数器也会停止,因此延时函数不适用于低功耗场景。
- **调试器干扰**:使用调试器时,若设置了硬件断点,DWT 计数器可能受影响,但一般正常运行无碍。
- **芯片差异**:部分 Cortex-M0/M0+ 内核没有 DWT 单元,需确认芯片手册。Cortex-M3/M4/M7 均支持。
## 总结
DWT 计数器是 Cortex-M 内核的隐藏宝藏,利用它可实现高精度、零外设占用的延时功能。相比 SysTick 和通用定时器,DWT 延时更灵活,尤其适合 RTOS 环境或资源紧张的项目。掌握这一技巧,能让你的嵌入式代码更加高效、专业。
在实际项目中,建议将 DWT 延时封装成独立模块,便于复用。同时,结合 `volatile` 关键字和编译器优化选项,确保代码行为符合预期。希望本文能帮助你提升嵌入式开发效率!