一、问题现象与背景

在STM32开发中,许多开发者习惯使用HAL_Delay()实现毫秒级延时。但当在中断服务函数(如EXTI、定时器中断)中调用HAL_Delay()时,程序可能永久卡死在延时循环中,表现为系统“死机”。本文从Cortex-M内核与HAL库机制出发,解释死锁成因并给出规避方案。

二、HAL_Delay原理与SysTick机制

HAL_Delay()的实现依赖全局变量uwTick,该变量由SysTick中断每1ms递增一次。简化后的源码如下:

__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) { }
}

关键点:

  • HAL_GetTick()返回uwTick。
  • uwTick仅在SysTick中断服务程序中递增(HAL_IncTick())。
  • SysTick中断的优先级由HAL_InitTick()配置,通常为最低优先级(如TICK_INT_PRIORITY = 0,即最高优先级?实际CubeMX默认15,即最低)。

三、死锁成因分析

3.1 中断优先级与嵌套规则

Cortex-M内核中,高优先级中断可抢占低优先级中断。若当前执行的中断优先级高于SysTick中断优先级,则SysTick中断无法抢占,uwTick停止更新。

3.2 典型死锁场景

假设SysTick优先级配置为15(最低),而某外设中断(如EXTI)优先级配置为5(较高)。在EXTI中断中调用HAL_Delay(100):

  1. 进入EXTI中断,优先级5。
  2. 执行HAL_Delay,循环等待uwTick增加。
  3. SysTick中断优先级15,低于当前中断优先级,无法抢占。
  4. uwTick永不更新,HAL_Delay死循环。

即使SysTick优先级高于当前中断,若在HAL_Delay执行期间发生其他更高优先级中断并长时间占用CPU,也会导致延时不准,但不会死锁。死锁的必要条件是:当前中断优先级高于或等于SysTick优先级(等于时同样无法抢占)。

3.3 特殊情形:在SysTick中断内调用

若在SysTick中断服务程序内调用HAL_Delay,由于SysTick中断自身无法嵌套,uwTick不会递增,必然死锁。

四、规避方案与代码示例

方案一:中断内使用非阻塞延时(查询计数器)

不依赖uwTick,直接使用硬件定时器或DWT周期计数器实现忙等待。示例使用DWT:

__STATIC_INLINE uint32_t DWT_Delay_Init(void)
{
  CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
  DWT->CYCCNT = 0;
  DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
  return 0;
}

__STATIC_INLINE void DWT_Delay_us(uint32_t us)
{
  uint32_t start = DWT->CYCCNT;
  uint32_t cycles = us * (SystemCoreClock / 1000000);
  while ((DWT->CYCCNT - start) < cycles) { }
}

注意:DWT需内核支持,且SystemCoreClock需准确。此方法仍为忙等待,但不受中断优先级影响。

方案二:调整SysTick优先级为最高

在HAL_Init()之后、外设中断使能之前,修改SysTick优先级为0(最高):

HAL_NVIC_SetPriority(SysTick_IRQn, 0, 0);

这样任何中断都无法阻止SysTick更新,HAL_Delay在中断中可正常工作。但需注意:SysTick频繁抢占会影响高优先级中断的实时性,且若在SysTick内调用仍会死锁。不推荐作为通用方案。

方案三:状态机+定时器回调(推荐)

在中断中仅设置标志,在主循环中处理延时逻辑。示例:

volatile uint8_t exti_flag = 0;

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
  if (GPIO_Pin == KEY_Pin) {
    exti_flag = 1;  // 仅置标志,不延时
  }
}

int main(void)
{
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  while (1) {
    if (exti_flag) {
      exti_flag = 0;
      HAL_Delay(100);  // 主循环中安全延时
      // 业务处理
    }
  }
}

此方案彻底避免中断上下文延时,符合RTOS设计原则。

五、配置步骤与注意事项

5.1 检查SysTick优先级

在stm32fxxx_hal_msp.c或main.c中查看HAL_InitTick()调用,确认TICK_INT_PRIORITY宏定义(通常位于stm32fxxx_hal_conf.h)。CubeMX生成的工程默认值为15(最低)。

5.2 避免在中断中调用阻塞函数

  • 禁止在ISR中调用HAL_Delay、HAL_UART_Transmit(阻塞模式)等依赖轮询的函数。
  • 使用HAL_UART_Transmit_IT或DMA替代。

5.3 若必须使用延时

  • 优先采用DWT或硬件定时器实现微秒级延时。
  • 确保延时时间远小于中断间隔,避免影响实时性。

5.4 调试技巧

若怀疑死锁,可在HAL_Delay循环内翻转GPIO,用示波器观察是否停止翻转;或检查uwTick是否变化。

六、总结

HAL_Delay在中断中死锁的根本原因是SysTick中断被更高优先级中断屏蔽,导致uwTick停止更新。规避的核心原则是:不在中断上下文中使用依赖系统节拍的阻塞延时。推荐采用状态机将延时处理移至主循环,或使用DWT等独立计数器实现短延时。正确配置中断优先级并遵循HAL库使用规范,可有效避免此类问题。