# 引言 在 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):当前计数值。 使能步骤: 1. 解锁调试寄存器(可选,但推荐):`CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;` 2. 复位 CYCCNT:`DWT->CYCCNT = 0;` 3. 使能计数器:`DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;` # 2. 实现微秒级延时函数 基于 CYCCNT,我们可以实现一个高精度延时函数。核心思路:计算目标延时对应的周期数,然后循环等待计数器差值达到目标。 ```c // 延时 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. 完整代码示例 以下是一个完整的驱动模块,包含初始化、延时函数和简单测试。 ```c #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 被占用或中断频繁的场景。通过理解其原理和误差边界,开发者可以灵活选用,提升系统实时性。希望本文能为你打开新思路,在嵌入式开发中游刃有余。