# 引言 在 STM32 开发中,`HAL_Delay` 因其简单易用被广泛采用,但不少工程师在调试通信协议或控制时序时发现,延时时间与预期不符,甚至随系统负载变化而漂移。这背后的根源往往不是硬件问题,而是 `HAL_Delay` 的实现机制与中断抢占的相互作用。本文将从原理出发,结合 STM32CubeMX 生成的代码,给出排查步骤和多种替代方案。 # HAL_Delay 的工作原理与缺陷 ## 原理回顾 STM32CubeMX 生成的 HAL 库中,`HAL_Delay` 的实现依赖于 SysTick 定时器。SysTick 被配置为 1ms 中断一次,每次中断中 `uwTick` 变量加 1。`HAL_Delay` 的典型代码如下: ```c __weak void HAL_Delay(uint32_t Delay) { uint32_t tickstart = HAL_GetTick(); uint32_t wait = Delay; if (wait < HAL_MAX_DELAY) { wait += (uint32_t)(uwTickFreq); } while ((HAL_GetTick() - tickstart) < wait) { } } ``` 它通过不断读取 `uwTick` 并计算差值来实现延时。 ## 缺陷所在 - `uwTick` 的更新依赖 SysTick 中断,如果 SysTick 中断被更高优先级的中断(如外部中断、定时器中断)长时间抢占,`uwTick` 就无法及时增加,导致 `HAL_Delay` 的实际等待时间变长。 - 即使 SysTick 中断未被完全阻塞,但若中断频繁,`HAL_Delay` 的 while 循环可能错过多次 `uwTick` 更新,造成延时偏大。 - 在临界区(如关闭中断)内调用 `HAL_Delay`,会导致死锁,因为 `uwTick` 永远不更新。 # 排查方法 ## 1. 检查中断优先级分组 使用 STM32CubeMX 时,默认将 SysTick 中断优先级设为最低(数值最大)。如果其他中断优先级更高且执行时间较长,就会抢占 SysTick。检查 NVIC 配置: ```c HAL_NVIC_SetPriority(SysTick_IRQn, 15, 0); // 默认最低优先级 ``` ## 2. 使用逻辑分析仪或示波器 在延时前后翻转一个 GPIO,观察实际波形。如果高电平时间明显大于预期,且波动明显,基本可判定为 SysTick 被抢占。 ## 3. 统计中断耗时 在中断服务函数中记录进入和退出时间,计算总耗时。若某中断耗时超过 1ms,则必然影响 `HAL_Delay`。 ## 4. 测试临界区调用 在 `__disable_irq()` 后调用 `HAL_Delay(1)`,若程序卡死,则证实了其依赖中断的本质。 # 替代方案 ## 方案一:使用硬件定时器(TIM) TIM 定时器独立于 CPU,不依赖中断,延时精度高。配置一个基本定时器,如 TIM6,在 STM32CubeMX 中设置预分频和自动重载值,使其溢出周期为 1ms,然后通过查询标志位实现延时。 ### 配置步骤 1. 在 STM32CubeMX 中启用 TIM6,时钟源选择 Internal Clock。 2. 设置 Prescaler 为 72-1(假设系统时钟 72MHz),Period 为 1000-1,得到 1ms 中断周期。 3. 生成代码后,编写延时函数: ```c void TIM_Delay_us(uint16_t us) { __HAL_TIM_SET_COUNTER(&htim6, 0); HAL_TIM_Base_Start(&htim6); while (__HAL_TIM_GET_COUNTER(&htim6) < us); HAL_TIM_Base_Stop(&htim6); } void TIM_Delay_ms(uint16_t ms) { for (uint16_t i = 0; i < ms; i++) { TIM_Delay_us(1000); } } ``` 注意:此方法在延时时占用 CPU,但不受中断影响,适合短延时。 ## 方案二:使用 DWT 内核定时器 Cortex-M 内核自带 DWT(Data Watchpoint and Trace)单元,其中的 CYCCNT 计数器可以精确记录 CPU 周期数,且不受中断影响。 ### 初始化代码 ```c void DWT_Init(void) { CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; DWT->CYCCNT = 0; DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; } void DWT_Delay_us(uint32_t us) { uint32_t start = DWT->CYCCNT; uint32_t ticks = us * (SystemCoreClock / 1000000); while ((DWT->CYCCNT - start) < ticks); } ``` - 优点:精度高,不依赖中断,代码简洁。 - 缺点:需要确保 DWT 单元已使能,且 SystemCoreClock 正确。 ## 方案三:裸机忙等待(NOP 循环) 对于极短延时(微秒级),可以使用简单的 NOP 循环,但需注意编译器优化。 ```c void NOP_Delay_us(uint32_t us) { uint32_t n = us * (SystemCoreClock / 1000000) / 4; // 粗略估算 while (n--) { __NOP(); } } ``` - 优点:零依赖,任何环境可用。 - 缺点:精度受编译器优化影响,不推荐用于精确延时。 # 完整示例:基于 TIM6 的精确延时 以下是一个完整的示例,展示如何在 STM32F103 上使用 TIM6 实现毫秒级延时,并验证其稳定性。 ```c #include "main.h" TIM_HandleTypeDef htim6; void TIM6_Init(void) { __HAL_RCC_TIM6_CLK_ENABLE(); htim6.Instance = TIM6; htim6.Init.Prescaler = 72 - 1; // 72MHz/72 = 1MHz htim6.Init.CounterMode = TIM_COUNTERMODE_UP; htim6.Init.Period = 1000 - 1; // 1ms htim6.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; HAL_TIM_Base_Init(&htim6); } void TIM_Delay_ms(uint16_t ms) { for (uint16_t i = 0; i < ms; i++) { __HAL_TIM_SET_COUNTER(&htim6, 0); HAL_TIM_Base_Start(&htim6); while (__HAL_TIM_GET_COUNTER(&htim6) < 1000); HAL_TIM_Base_Stop(&htim6); } } int main(void) { HAL_Init(); SystemClock_Config(); TIM6_Init(); GPIO_Init(); // 配置LED引脚 while (1) { HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); TIM_Delay_ms(500); // 精确延时500ms } } ``` # 注意事项 - 使用 TIM 延时函数时,需确保 TIM 时钟已使能,且预分频值根据实际时钟频率计算。 - DWT 延时在低功耗模式下可能失效,因为 CYCCNT 可能停止计数。 - 在中断服务函数中,尽量避免使用阻塞延时,可改用状态机或定时器回调。 - 如果必须使用 `HAL_Delay`,建议将 SysTick 优先级提升至最高,并确保其他中断执行时间远小于 1ms。 # 总结 `HAL_Delay` 的时序漂移源于其对 SysTick 中断的依赖。通过理解其原理,我们可以根据应用场景选择合适的替代方案:TIM 定时器适合中等精度延时,DWT 适合高精度微秒延时,NOP 循环适合简单场景。在实际项目中,建议优先使用硬件定时器,并避免在中断中调用阻塞延时。希望本文能帮助你彻底解决延时精度问题,提升嵌入式开发的可靠性。