# 引言 在 STM32F4 开发中,精确的微秒级延时(如 1-100us)常用于 I2C 时序模拟、超声波测距、DHT11 读取等场景。SysTick 虽常用,但若被 RTOS 或主循环占用,则难以复用。DWT 作为 Cortex-M4 内核自带的调试组件,提供了 CYCCNT 计数器,可精确记录 CPU 周期,非常适合实现高精度延时。然而,中断处理会打断延时循环,导致时间溢出,本文提供两种规避策略,确保延时可靠性。 # DWT 原理 DWT(Data Watchpoint and Trace)是 Cortex-M 内核的调试单元,其中 CYCCNT 是一个 32 位自由运行计数器,每个 CPU 时钟周期递增。当计数器溢出(0xFFFFFFFF -> 0)时,可配置触发事件。利用 CYCCNT,我们可读取当前周期数,通过计算差值实现延时。 关键寄存器: - **DWT_CTRL**(地址 0xE0001000):控制 CYCCNT 使能。 - **DWT_CYCCNT**(地址 0xE0001004):当前计数值。 - **CoreDebug_DEMCR**(地址 0xE000EDFC):需置位 TRCENA 位以启用 DWT。 # 硬件与软件准备 - 硬件:任何 STM32F4 系列开发板(如 STM32F407 Discovery)。 - 软件:STM32CubeIDE 或 Keil MDK,使用 HAL 库或标准库均可。 - 时钟:假设系统时钟为 168MHz(F4 最高主频),则 1us = 168 周期。 # 配置步骤 ## 1. 使能 DWT 并启动 CYCCNT 在系统初始化时,调用以下函数: ```c void DWT_Init(void) { CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; // 使能 DWT 访问 DWT->CYCCNT = 0; // 清零计数器 DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; // 使能 CYCCNT } ``` ## 2. 实现微秒延时函数(基础版) ```c void delay_us_basic(uint32_t us) { uint32_t start = DWT->CYCCNT; uint32_t ticks = us * (SystemCoreClock / 1000000); // 计算所需周期数 while ((DWT->CYCCNT - start) < ticks); // 等待差值达到目标 } ``` 注意:`SystemCoreClock` 是全局变量,由系统时钟初始化设置。若时钟为 168MHz,则 `ticks = us * 168`。 ## 3. 中断干扰问题分析 当延时期间发生中断(如 SysTick、UART),CPU 会跳转执行中断服务函数,导致 CYCCNT 计数暂停(因为 CYCCNT 只在 CPU 运行时递增)。若中断耗时较长,延时时间会变长,甚至超出预期。例如,延时 10us,但中断占用了 5us,实际耗时 15us。 ## 4. 规避中断干扰的两种策略 ### 策略一:临界区保护(简单粗暴) 在延时期间关闭所有中断,但会阻塞实时性,仅适用于短延时。 ```c void delay_us_critical(uint32_t us) { __disable_irq(); // 关闭全局中断 uint32_t start = DWT->CYCCNT; uint32_t ticks = us * (SystemCoreClock / 1000000); while ((DWT->CYCCNT - start) < ticks); __enable_irq(); // 重新开启 } ``` 缺点:若中断有实时要求(如通信),则不可取。 ### 策略二:补偿式延时(推荐) 利用 DWT 的溢出中断或周期检测,在中断中记录时间戳,延时函数计算实际消耗并调整。但更简单的方法是:在延时循环中,不断检查是否发生中断,若发生则重新计算剩余时间。 实现思路: - 使用一个全局变量 `g_irq_occurred`,在中断服务函数中置位。 - 延时函数中,每次循环检查该标志,若置位则重新计算剩余周期。 ```c volatile uint32_t g_irq_count = 0; // 在中断中递增 void SysTick_Handler(void) { g_irq_count++; } void delay_us_compensated(uint32_t us) { uint32_t start = DWT->CYCCNT; uint32_t ticks = us * (SystemCoreClock / 1000000); uint32_t last_irq_count = g_irq_count; while (1) { uint32_t elapsed = DWT->CYCCNT - start; if (elapsed >= ticks) break; if (g_irq_count != last_irq_count) { // 检测到中断发生 // 中断期间 CYCCNT 未计数,但时间已流逝,需要补偿 // 这里简单处理:重置开始时间,但会导致总时间偏长 // 更精确:记录中断耗时,但需在中断中获取时间戳 start = DWT->CYCCNT; // 重新开始,但会丢失已等待时间 last_irq_count = g_irq_count; } } } ``` 但上述补偿并不精确。更优方案:在中断服务函数中,记录中断开始和结束的 CYCCNT 值,然后延时函数减去中断耗时。 ```c volatile uint32_t irq_start_cyccnt, irq_end_cyccnt; volatile uint32_t irq_duration_cycles = 0; void SysTick_Handler(void) { irq_start_cyccnt = DWT->CYCCNT; // 中断处理... irq_end_cyccnt = DWT->CYCCNT; irq_duration_cycles += (irq_end_cyccnt - irq_start_cyccnt); } void delay_us_precise(uint32_t us) { uint32_t start = DWT->CYCCNT; uint32_t ticks = us * (SystemCoreClock / 1000000); uint32_t accumulated_irq = 0; while (1) { uint32_t elapsed = DWT->CYCCNT - start; if (elapsed >= ticks + accumulated_irq) break; // 读取并清零中断累计时间(需原子操作) __disable_irq(); uint32_t irq_dur = irq_duration_cycles; irq_duration_cycles = 0; __enable_irq(); accumulated_irq += irq_dur; } } ``` 此方法在中断频繁时仍能保持高精度,但需注意中断中读取 CYCCNT 的时机,以及避免死锁。 # 完整代码示例 以下是一个完整的示例,包含初始化、延时函数和测试主函数。 ```c #include "stm32f4xx.h" // 系统时钟频率(假设 168MHz) extern uint32_t SystemCoreClock; // 中断累计周期变量 volatile uint32_t irq_duration_cycles = 0; // DWT 初始化 void DWT_Init(void) { CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; DWT->CYCCNT = 0; DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; } // 基础延时(无中断补偿) void delay_us_basic(uint32_t us) { uint32_t start = DWT->CYCCNT; uint32_t ticks = us * (SystemCoreClock / 1000000); while ((DWT->CYCCNT - start) < ticks); } // 精确延时(带中断补偿) void delay_us_precise(uint32_t us) { uint32_t start = DWT->CYCCNT; uint32_t ticks = us * (SystemCoreClock / 1000000); uint32_t accumulated_irq = 0; while (1) { uint32_t elapsed = DWT->CYCCNT - start; if (elapsed >= ticks + accumulated_irq) break; // 读取并清零中断累计周期 __disable_irq(); uint32_t irq_dur = irq_duration_cycles; irq_duration_cycles = 0; __enable_irq(); accumulated_irq += irq_dur; } } // 示例中断服务函数(以 SysTick 为例) void SysTick_Handler(void) { uint32_t start = DWT->CYCCNT; // 模拟中断处理,如翻转 LED // ... uint32_t end = DWT->CYCCNT; irq_duration_cycles += (end - start); } int main(void) { SystemInit(); DWT_Init(); // 配置 SysTick 1ms 中断(示例) SysTick_Config(SystemCoreClock / 1000); while (1) { // 精确延时 100us delay_us_precise(100); // 其他操作 } } ``` # 注意事项 - **时钟频率**:确保 `SystemCoreClock` 与实际一致,否则延时误差大。 - **CYCCNT 溢出**:32 位计数器在 168MHz 下约 25.6 秒溢出一次,对于微秒延时无影响,但若延时超过此范围需处理溢出。 - **中断补偿的原子性**:读取和清零 `irq_duration_cycles` 时需关中断,防止竞争。 - **DWT 在低功耗模式**:进入停止模式后 CYCCNT 停止,需唤醒后重新初始化。 - **调试器影响**:使用调试器时,CYCCNT 可能受调试暂停影响,但正常运行无碍。 - **替代方案**:若需纳秒级精度,可考虑使用定时器输入捕获,但 DWT 已足够。 # 总结 DWT 提供了轻量级的高精度延时方案,通过合理的中断补偿,可有效规避中断干扰。本文的方法适用于大多数嵌入式场景,且代码可移植到其他 Cortex-M3/M4 系列。掌握这一技巧,能提升你的时序控制能力,为复杂驱动开发打下基础。