# 引言 在 STM32F4 开发中,微秒级延时是驱动时序敏感外设(如 DHT11、DS18B20、WS2812)的刚需。传统基于 SysTick 的延时函数(如 HAL_Delay)精度受中断影响大,且最小粒度通常为 1ms,无法满足微秒级需求。而 STM32F4 内核自带的 DWT(Data Watchpoint and Trace)单元提供了 32 位周期计数器,可精确到内核时钟周期,是实现微秒级延时的理想方案。本文将深入讲解 DWT 原理、实现方法,并重点解决中断干扰问题。 # DWT 原理简介 DWT 是 Cortex-M4 内核调试组件的一部分,其核心寄存器为 `DWT->CYCCNT`,一个 32 位向上计数器,每个内核时钟周期(HCLK)递增一次。当计数器溢出(0xFFFFFFFF)后回绕到 0,因此可测量最长约 2^32 / 168MHz ≈ 25.6 秒(以 STM32F407 最大主频 168MHz 为例)。 要使用 DWT,需按以下步骤初始化: 1. 使能 DWT 单元:设置 `CoreDebug->DEMCR` 的位 24(TRCENA)。 2. 复位 CYCCNT 计数器:写 0 到 `DWT->CYCCNT`。 3. 使能 CYCCNT 计数器:设置 `DWT->CTRL` 的位 0(CYCCNTENA)。 初始化后,`DWT->CYCCNT` 会持续递增,我们只需读取其差值即可计算耗时。 # 实现微秒级延时 ## 核心代码 以下代码基于 STM32F4 系列(HCLK 频率可配置),通过宏定义获取内核时钟频率,实现 `delay_us` 和 `delay_ms` 函数。 ```c #include "stm32f4xx.h" // 获取内核时钟频率(HCLK),可根据实际配置修改 #define CORE_CLOCK_HZ 168000000UL // 假设主频168MHz // 初始化 DWT void DWT_Init(void) { CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; // 使能DWT DWT->CYCCNT = 0; // 清零计数器 DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; // 使能CYCCNT } // 微秒延时(不关中断版本,适合非临界场景) void delay_us_noirq(uint32_t us) { uint32_t start = DWT->CYCCNT; uint32_t ticks = us * (CORE_CLOCK_HZ / 1000000UL); while ((DWT->CYCCNT - start) < ticks); } // 微秒延时(关中断版本,规避中断干扰) void delay_us(uint32_t us) { uint32_t primask; primask = __get_PRIMASK(); // 保存当前中断状态 __disable_irq(); // 关闭全局中断 uint32_t start = DWT->CYCCNT; uint32_t ticks = us * (CORE_CLOCK_HZ / 1000000UL); while ((DWT->CYCCNT - start) < ticks); __set_PRIMASK(primask); // 恢复中断状态 } // 毫秒延时(基于微秒延时) void delay_ms(uint32_t ms) { while (ms--) delay_us(1000); } ``` ## 代码说明 - `DWT_Init()` 必须在系统启动后调用一次,建议放在 `main` 函数开头。 - `delay_us_noirq` 不关中断,适用于对延时精度要求不高或中断不频繁的场景。 - `delay_us` 通过 `__disable_irq()` 关闭全局中断,确保延时期间不被中断打断,从而保证精度。但注意,关中断时间过长会影响系统实时性,因此仅建议在短延时(<100us)时使用。 - 计算 ticks 时,`CORE_CLOCK_HZ / 1000000UL` 得到每微秒的周期数,乘以 us 得到总周期数。注意防止乘法溢出,us 最大约 25 秒(对于 168MHz)。 # 中断干扰问题与规避策略 ## 为什么中断会干扰延时? 当延时函数执行期间发生中断,CPU 会跳转到中断服务程序(ISR)执行,导致 `DWT->CYCCNT` 的读取和比较操作被延迟,实际延时时间会大于设定值。例如,一个 10us 的延时,若期间发生一个 5us 的中断,实际耗时可能变成 15us,导致时序错误。 ## 规避方法 1. **关中断(临界区保护)**:在延时期间关闭全局中断,这是最直接的方法。但需注意: - 关中断时间过长会影响系统实时性,因此仅适用于短延时。 - 若延时期间有高优先级中断(如系统时钟)需要响应,则不建议使用。 2. **使用中断优先级分组**:将延时相关的中断优先级设为最低,这样即使发生中断,也会在延时结束后才响应。但这种方法不保证绝对精确,且配置复杂。 3. **使用 DWT 比较器**:DWT 支持比较器功能,当 CYCCNT 达到设定值时触发中断,但这种方法需要额外的中断处理,且不适合简单延时。 4. **混合策略**:对于长延时(>1ms),使用 SysTick 或定时器中断;对于短延时(<100us),使用 DWT 并关中断。这样兼顾精度和实时性。 # 完整示例:驱动 DHT11 温湿度传感器 DHT11 的时序要求微秒级精度,非常适合用 DWT 延时。以下是一个简化示例: ```c // 假设已初始化 DWT 和 GPIO #define DHT11_PIN GPIO_PIN_0 #define DHT11_PORT GPIOA void DHT11_Start(void) { // 主机拉低至少 18ms HAL_GPIO_WritePin(DHT11_PORT, DHT11_PIN, GPIO_PIN_RESET); delay_ms(20); // 拉高 20-40us HAL_GPIO_WritePin(DHT11_PORT, DHT11_PIN, GPIO_PIN_SET); delay_us(30); // 释放总线,读取响应 HAL_GPIO_WritePin(DHT11_PORT, DHT11_PIN, GPIO_PIN_RESET); } uint8_t DHT11_ReadByte(void) { uint8_t data = 0; for (int i = 0; i < 8; i++) { // 等待低电平结束(50us) while (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) == GPIO_PIN_RESET); // 延时 40us,若仍为高电平则为1,否则为0 delay_us(40); if (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) == GPIO_PIN_SET) data |= (1 << (7 - i)); // 等待高电平结束 while (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) == GPIO_PIN_SET); } return data; } ``` # 注意事项 - **时钟频率一致性**:`CORE_CLOCK_HZ` 必须与实际 HCLK 一致,否则延时不准。可通过 `SystemCoreClock` 变量获取,或使用 `HAL_RCC_GetHCLKFreq()`。 - **DWT 初始化时机**:必须在启用 DWT 后才能使用,否则 `DWT->CYCCNT` 可能为 0 或不可用。 - **溢出处理**:`DWT->CYCCNT` 是 32 位,计算差值时使用无符号减法,可自动处理回绕,但需确保延时时间小于 2^32 周期。 - **关中断副作用**:`__disable_irq()` 会关闭所有可屏蔽中断,若系统依赖中断(如 RTOS 调度),需谨慎使用。在 RTOS 中,建议使用 `taskENTER_CRITICAL()` 等机制。 - **编译器优化**:延时循环中的变量应使用 `volatile` 修饰,防止编译器优化导致延时失效。 # 总结 DWT 提供了高精度的周期计数器,是实现微秒级延时的利器。通过关中断保护,可以规避中断干扰,确保时序准确性。但在实际项目中,需根据延时长度和系统实时性要求,灵活选择延时方案。希望本文能帮助你在 STM32F4 开发中更从容地应对时序挑战。