一、引言:一个隐蔽的陷阱

在STM32开发中,HAL_Delay() 因其简单易用而备受青睐。然而,当你在中断服务函数(ISR)中调用 HAL_Delay() 时,系统可能会莫名其妙地卡死。这个问题的根源在于SysTick中断优先级与HAL_Delay的依赖关系。本文将带你从原理到实践,彻底理解并规避这一陷阱。

二、HAL_Delay的实现原理

HAL库的延时函数基于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,该变量在SysTick中断服务函数 SysTick_Handler() 中递增:

void SysTick_Handler(void)
{
  HAL_IncTick();
}

__weak void HAL_IncTick(void)
{
  uwTick += uwTickFreq;
}

因此,HAL_Delay 的正常工作依赖于SysTick中断能够及时执行,从而更新 uwTick。

三、SysTick优先级与中断嵌套

Cortex-M内核中,SysTick中断的优先级通过 HAL_InitTick() 配置,通常设置为最低优先级(数值最大)。例如在 stm32f1xx_hal.c 中:

__weak HAL_StatusTypeDef HAL_InitTick(uint32_t TickPriority)
{
  if (HAL_SYSTICK_Config(SystemCoreClock / (1000U / uwTickFreq)) > 0U)
  {
    return HAL_ERROR;
  }

  HAL_NVIC_SetPriority(SysTick_IRQn, TickPriority, 0U);
  uwTickPrio = TickPriority;
  return HAL_OK;
}

默认 TickPriority 为 TICK_INT_PRIORITY(通常为15,即最低优先级)。这意味着:当任何优先级高于SysTick的中断正在执行时,SysTick中断无法抢占,uwTick 不会更新。

四、中断上下文中调用HAL_Delay的死锁复现

假设我们在一个优先级高于SysTick的中断(例如外部中断EXTI,优先级设为5)中调用 HAL_Delay(100):

void EXTI0_IRQHandler(void)
{
  HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0);
}

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
  if (GPIO_Pin == GPIO_PIN_0)
  {
    HAL_Delay(100); // 危险!
    HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
  }
}

执行流程分析:

  1. EXTI中断触发,CPU进入中断服务函数,当前优先级为5。
  2. 调用 HAL_Delay(100),进入忙等待循环,等待 uwTick 增加100。
  3. 由于SysTick优先级为15(低于5),SysTick中断被挂起,无法执行。
  4. uwTick 永远不变,HAL_Delay 陷入死循环。
  5. 系统卡死,其他低优先级中断也无法响应。

复现步骤:

  • 配置一个GPIO外部中断,优先级设为5(高于SysTick的15)。
  • 在中断回调中调用 HAL_Delay(100)。
  • 触发中断,观察LED不再闪烁,调试器显示程序卡在 HAL_Delay 的while循环中。

五、解决方案与最佳实践

方案1:避免在中断中调用阻塞延时

这是最根本的解决方案。中断服务函数应尽可能短小快,延时需求应通过状态机或标志位在主循环中处理。

volatile uint8_t exti_flag = 0;

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
  if (GPIO_Pin == GPIO_PIN_0)
  {
    exti_flag = 1;
  }
}

int main(void)
{
  // 初始化...
  while (1)
  {
    if (exti_flag)
    {
      HAL_Delay(100);
      HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
      exti_flag = 0;
    }
  }
}

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

如果确实需要在中断中延时,可以将SysTick优先级设为最高(数值最小),确保其总能抢占:

HAL_NVIC_SetPriority(SysTick_IRQn, 0, 0);

但此方法会破坏系统实时性,且HAL库默认配置不推荐修改,需谨慎评估。

方案3:使用非阻塞延时

基于 HAL_GetTick() 实现非阻塞延时,避免忙等待:

uint32_t start_tick = HAL_GetTick();
while (HAL_GetTick() - start_tick < 100)
{
  // 执行其他任务
}

但在中断中仍不推荐,因为中断应尽快返回。

六、注意事项

  • 永远不要在优先级高于SysTick的中断中调用 HAL_Delay,否则必死锁。
  • 检查所有中断的优先级,确保没有意外高于SysTick的中断调用阻塞函数。
  • 使用RTOS时,HAL_Delay 的行为可能不同,但原理类似,需注意系统节拍优先级。
  • 调试时若发现程序卡在 HAL_Delay,首先检查是否在中断上下文中调用。
  • 推荐使用 HAL_GetTick() 实现超时机制,而非硬延时。

七、总结

HAL_Delay 在中断上下文中的死锁问题源于SysTick优先级配置与忙等待机制的冲突。理解Cortex-M中断嵌套规则和HAL库的tick更新机制,是避免此类陷阱的关键。遵循“中断快进快出”原则,采用状态机或标志位传递事件,才能构建稳定可靠的嵌入式系统。