# 引言 在 STM32F4 系列(如 STM32F407)开发中,微秒级延时常见于传感器读取、通信协议时序(如 DHT11、DS18B20)或 PWM 信号模拟。SysTick 虽然简单,但若被中断频繁抢占,会导致延时误差累积。而 DWT 的 CYCCNT 是一个 32 位自由运行计数器,每个内核时钟周期递增一次,不受普通中断影响,非常适合精确延时。但若在中断服务函数(ISR)中使用 DWT 延时,且该中断可被更高优先级中断嵌套,则可能产生冲突。本文将深入剖析原理,并给出规避冲突的完整方案。 # DWT 工作原理 DWT 是 Cortex-M4 内核提供的调试组件,其中 CYCCNT 计数器用于统计 CPU 周期数。其工作特性: - 32 位计数器,从 0 到 0xFFFFFFFF 循环计数。 - 每个 HCLK(内核时钟)周期加 1,例如 STM32F407 主频 168MHz 时,1 微秒对应 168 个周期。 - 可通过 DWT_CTRL 寄存器的 CYCCNTENA 位使能。 实现延时的核心思路:读取当前 CYCCNT 值,计算目标周期数,轮询直到差值达到目标。但需注意计数器回绕(wrap-around)问题,需用无符号减法处理。 # 基础 DWT 延时实现 首先,使能 DWT 并校准周期数。代码如下: ```c // 初始化 DWT void DWT_Init(void) { CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; // 使能调试跟踪 DWT->CYCCNT = 0; // 清零计数器 DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; // 使能 CYCCNT } // 获取当前周期计数 static inline uint32_t DWT_GetCycles(void) { return DWT->CYCCNT; } // 微秒延时(基础版,不考虑中断) void DWT_Delay_us(uint32_t us) { uint32_t cycles = us * (SystemCoreClock / 1000000); // 每微秒周期数 uint32_t start = DWT_GetCycles(); while ((DWT_GetCycles() - start) < cycles); // 无符号减法处理回绕 } ``` 此代码在无中断或低中断频率下工作良好。但若在 ISR 中使用,且该 ISR 可被更高优先级中断抢占,则可能出现问题: - 问题 1:延时期间被高优先级中断打断,导致实际延时时间变长(因为 CPU 执行中断服务程序,CYCCNT 仍在计数,但延时循环被暂停)。 - 问题 2:若在延时循环中发生嵌套中断,且嵌套中断中也调用 DWT_Delay_us,则内层延时可能覆盖外层延时的起始值,导致外层延时计算错误。 # 中断嵌套冲突分析 假设有一个中等优先级中断 A,其 ISR 中调用 DWT_Delay_us(50)。在延时期间,高优先级中断 B 触发,B 的 ISR 中也调用 DWT_Delay_us(10)。此时: - 中断 B 的延时基于当前 CYCCNT 值计算,执行 10us 后返回。 - 但中断 A 的延时循环仍在继续,其起始值 start 是 B 执行前的值,而 B 执行期间 CYCCNT 增加了 10us 的周期数,因此 A 的延时总时间变为 50us + 10us = 60us,超出预期。 此外,若 B 的延时时间较长,甚至可能导致 A 的延时循环提前结束(因为 CYCCNT 回绕),但概率极低。 # 规避冲突的解决方案 核心思路:在 DWT 延时期间,临时屏蔽所有可嵌套的中断,确保延时循环不被抢占。但完全屏蔽中断会影响系统实时性,因此需权衡。以下提供两种策略: ## 策略一:临界区保护(推荐) 在延时函数入口关闭中断,出口恢复。使用 `__disable_irq()` 和 `__enable_irq()`,但需注意保存和恢复 PRIMASK 状态,以支持嵌套调用。 ```c // 微秒延时(临界区保护版) void DWT_Delay_us_safe(uint32_t us) { uint32_t primask = __get_PRIMASK(); // 保存当前中断屏蔽状态 __disable_irq(); // 关闭所有可屏蔽中断 uint32_t cycles = us * (SystemCoreClock / 1000000); uint32_t start = DWT_GetCycles(); while ((DWT_GetCycles() - start) < cycles); __set_PRIMASK(primask); // 恢复原状态 } ``` 此方法确保延时期间无中断打断,但会牺牲中断响应时间。适用于延时较短(<100us)且对实时性要求不高的场景。 ## 策略二:基于优先级分组屏蔽 若系统使用 NVIC 优先级分组,可仅屏蔽优先级高于当前中断的中断,但实现复杂。简单做法是使用 BASEPRI 寄存器屏蔽优先级低于某值的所有中断,但需确保当前中断优先级高于该值。 ```c // 仅屏蔽优先级低于 threshold 的中断(threshold 为优先级数值,数值越大优先级越低) void DWT_Delay_us_basepri(uint32_t us, uint32_t threshold) { uint32_t basepri = __get_BASEPRI(); __set_BASEPRI(threshold << 4); // 设置 BASEPRI,屏蔽优先级数值大于等于 threshold 的中断 uint32_t cycles = us * (SystemCoreClock / 1000000); uint32_t start = DWT_GetCycles(); while ((DWT_GetCycles() - start) < cycles); __set_BASEPRI(basepri); } ``` 注意:BASEPRI 只屏蔽优先级数值大于等于设定值的中断,且需在中断中调用时确保当前中断优先级高于 threshold,否则可能死锁。 ## 策略三:使用 DWT 比较器(高级) DWT 具有比较器功能,可设置 CYCCNT 匹配值并触发事件,但需配合中断使用,不适合纯延时。此处不展开。 # 完整示例代码 以下是一个综合示例,展示如何在中断中使用安全延时函数,并测试其准确性。 ```c #include "stm32f4xx.h" void DWT_Init(void); void DWT_Delay_us_safe(uint32_t us); // 测试:定时器触发中断,中断中调用延时 void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update)) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); // 模拟需要精确延时的操作 DWT_Delay_us_safe(50); GPIO_ToggleBits(GPIOA, GPIO_Pin_0); } } int main(void) { // 初始化时钟、GPIO、定时器等(省略) DWT_Init(); // 配置 TIM2 中断优先级为 2,允许被优先级 1 的中断嵌套 NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); while (1) { // 主循环 } } ``` # 注意事项 - **时钟频率**:`SystemCoreClock` 需正确反映实际 HCLK,否则延时不准。可在系统初始化后获取。 - **计数器溢出**:32 位计数器在 168MHz 下约 25.6 秒回绕一次,使用无符号减法可正确处理。 - **临界区开销**:关闭中断会引入少量延迟(约几个周期),对于极短延时(<5us)可能影响精度,需评估。 - **嵌套调用**:使用 `__get_PRIMASK` 保存状态,可支持延时函数嵌套调用(如一个 ISR 中调用另一个延时函数)。 - **DWT 初始化**:必须在系统启动后尽早调用 `DWT_Init()`,否则 CYCCNT 未使能。 - **调试影响**:若使用调试器,CYCCNT 可能受调试暂停影响,但在正常运行中无影响。 # 总结 利用 DWT 的 CYCCNT 计数器可实现微秒级精确延时,但中断嵌套会引入冲突。通过临界区保护或 BASEPRI 屏蔽,可有效规避。本文提供的方案适用于大多数场景,开发者可根据系统实时性要求选择合适策略。掌握这一技巧,能显著提升嵌入式时序控制的可靠性。