引言
在 STM32 开发中,微秒级延时是驱动外设(如传感器、通信协议)的常见需求。SysTick 作为系统滴答定时器,通常被用作操作系统的心跳或基准延时,但在以下场景中力不从心:
- 高频中断频繁抢占,SysTick 的更新被延迟;
- 需要同时兼顾多个定时任务,SysTick 被占用;
- 延时精度要求达到亚微秒级,SysTick 的时钟源(通常为 HCLK/8)分辨率不足。
此时,DWT(Data Watchpoint and Trace) 单元中的周期计数器(CYCCNT)提供了一个优雅的替代方案。它直接以 CPU 内核时钟(HCLK)计数,分辨率可达 1 个时钟周期,且几乎零开销。本文将带你从原理到实战,掌握这一技巧。
1. DWT 周期计数器原理
DWT 是 Cortex-M3/M4/M7 内核中的调试组件,其中 CYCCNT 是一个 32 位自由运行计数器,每个 CPU 时钟周期(HCLK)自动加 1。它不受中断影响,除非显式复位或溢出(约 2^32 个周期,在 72MHz 下约 59.6 秒)。
关键寄存器:
-
DWT_CTRL(地址 0xE0001000):控制 CYCCNT 使能。 -
DWT_CYCCNT(地址 0xE0001004):当前计数值。
使能步骤:
- 解锁调试寄存器(可选,但推荐):
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; - 复位 CYCCNT:
DWT->CYCCNT = 0; - 使能计数器:
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
2. 实现微秒级延时函数
基于 CYCCNT,我们可以实现一个高精度延时函数。核心思路:计算目标延时对应的周期数,然后循环等待计数器差值达到目标。
// 延时 us 微秒,系统时钟 HCLK 频率(Hz)需外部传入
void DWT_Delay_us(uint32_t us, uint32_t hclk_hz) {
uint32_t start = DWT->CYCCNT;
uint32_t ticks = us * (hclk_hz / 1000000); // 微秒转周期数
while ((DWT->CYCCNT - start) < ticks); // 无符号减法处理溢出
}
注意:hclk_hz 需根据实际系统时钟设置,例如 STM32F103 默认 72MHz,则 hclk_hz = 72000000。
3. 边界场景与误差分析
3.1 中断干扰
场景:延时期间发生中断,CPU 跳转执行 ISR。
- 误差来源:CYCCNT 在中断期间依然计数,但延时循环被暂停,导致实际耗时 = 目标延时 + 中断处理时间。
- 分析:若中断频繁,误差累积明显。但相比 SysTick,DWT 不会因中断而丢失计数,因此误差是“增加”而非“漂移”,可预测。
-
规避:在实时性要求高的场景,可临时关闭可屏蔽中断(
__disable_irq()),但需权衡中断响应延迟。
3.2 时钟频率变化
场景:系统动态调频(如低功耗模式切换 HCLK)。
- 误差来源:DWT 计数基于 HCLK,若频率改变,则延时时间变化。
- 分析:例如从 72MHz 降至 36MHz,同样周期数耗时翻倍。
-
规避:在延时前读取当前时钟频率(如通过
RCC_GetClocksFreq),或使用固定频率的时钟源(如 HSE)。
3.3 计数器溢出
场景:延时时间过长,超过 CYCCNT 溢出周期。
- 误差来源:32 位计数器溢出后回绕,若未处理,延时可能提前结束。
- 分析:72MHz 下溢出周期约 59.6 秒,一般微秒延时不会触及,但若延时数秒级需注意。
-
规避:使用无符号减法
(DWT->CYCCNT - start)可自动处理回绕,只要延时周期数小于 2^32。
3.4 指令开销
场景:循环判断和减法操作本身消耗周期。
- 误差来源:每次循环迭代有 2-3 个周期开销,导致实际延时略大于目标。
- 分析:对于微秒级延时(如 10us),误差约 0.1us,可忽略;但若追求极致,可校准。
- 规避:在初始化时测量空循环的周期数,并调整目标 ticks。
4. 完整代码示例
以下是一个完整的驱动模块,包含初始化、延时函数和简单测试。
#include "stm32f1xx.h"
// 初始化 DWT 周期计数器
void DWT_Init(void) {
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; // 使能调试跟踪
DWT->CYCCNT = 0; // 清零计数器
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; // 使能 CYCCNT
}
// 微秒延时,hclk_hz 为 CPU 时钟频率
void DWT_Delay_us(uint32_t us, uint32_t hclk_hz) {
uint32_t start = DWT->CYCCNT;
uint32_t ticks = us * (hclk_hz / 1000000);
while ((DWT->CYCCNT - start) < ticks);
}
// 毫秒延时(基于微秒延时,注意溢出)
void DWT_Delay_ms(uint32_t ms, uint32_t hclk_hz) {
for (uint32_t i = 0; i < ms; i++) {
DWT_Delay_us(1000, hclk_hz);
}
}
// 测试:主函数中初始化并延时
int main(void) {
DWT_Init();
while (1) {
DWT_Delay_us(10, 72000000); // 延时 10us
// 其他操作
}
}
5. 注意事项
- 内核支持:仅 Cortex-M3/M4/M7 支持 DWT,Cortex-M0/M0+ 无此功能。
- 调试器影响:使用调试器时,CYCCNT 可能受调试暂停影响,但正常运行无碍。
- 功耗模式:在睡眠模式下,HCLK 可能停止,DWT 不计数,需唤醒后重新校准。
- 替代方案:若需更高精度(纳秒级),可考虑使用硬件定时器(如 TIM2)的输入捕获模式,但开销更大。
结语
DWT 周期计数器为 STM32 微秒级延时提供了一种轻量、精准的解决方案,尤其适合 SysTick 被占用或中断频繁的场景。通过理解其原理和误差边界,开发者可以灵活选用,提升系统实时性。希望本文能为你打开新思路,在嵌入式开发中游刃有余。