为什么HAL_Delay在中断里会“卡死”?

很多开发者习惯在中断服务函数(ISR)里直接调用HAL_Delay(10)来等待某个外设稳定,结果发现程序跑飞或系统无响应。根本原因在于HAL_Delay的实现机制与中断上下文存在天然冲突。

原理剖析:HAL_Delay如何工作

HAL库中HAL_Delay的典型实现如下(以STM32F1为例):

__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中断中递增。
  • HAL_Delay本质是忙等待,依赖SysTick中断持续更新uwTick。

中断上下文中的三大隐患

1. 死锁风险(最严重)

若在SysTick中断同级或更高优先级的中断中调用HAL_Delay,SysTick中断无法抢占,uwTick停止更新,while循环永不退出,系统死锁。

例如:在优先级为0的定时器中断中调用HAL_Delay,而SysTick优先级为15(默认最低),则必然死锁。

2. 优先级反转与实时性破坏

即使SysTick能抢占,忙等待也会浪费CPU周期,阻塞同级中断的响应,破坏系统的实时性。

3. 可重入性问题

HAL_Delay非可重入,若主循环与中断同时调用,可能因uwTick回绕导致延时异常。

替代方案与配置步骤

方案一:DWT周期计数器实现微秒级延时(推荐)

利用Cortex-M内核的DWT(Data Watchpoint and Trace)单元,不依赖任何中断。

配置步骤:

  1. 使能DWT:CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
  2. 复位并启动周期计数器:DWT->CYCCNT = 0; DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
  3. 根据系统主频计算延时循环。

完整代码:

#include "core_cm4.h"  // 根据内核调整

void DWT_Init(void)
{
    CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
    DWT->CYCCNT = 0;
    DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
}

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

注意事项:

  • 需在SystemInit后调用DWT_Init。
  • 部分低端型号(如Cortex-M0)无DWT,需改用方案二。
  • 延时精度受编译器优化影响,建议加__NOP()或使用volatile。

方案二:硬件定时器单次延时

使用一个基本定时器(如TIM6)产生精确延时,不依赖SysTick。

配置步骤(CubeMX):

  1. 启用TIM6,预分频器设为SystemCoreClock/1000000 - 1(1MHz计数)。
  2. 自动重装载值设为us - 1,使能单次模式。
  3. 启动定时器并轮询更新标志。

代码示例:

void TIM6_DelayUs(uint16_t us)
{
    __HAL_TIM_SET_AUTORELOAD(&htim6, us - 1);
    __HAL_TIM_SET_COUNTER(&htim6, 0);
    HAL_TIM_Base_Start(&htim6);
    while (__HAL_TIM_GET_FLAG(&htim6, TIM_FLAG_UPDATE) == RESET) { }
    __HAL_TIM_CLEAR_FLAG(&htim6, TIM_FLAG_UPDATE);
    HAL_TIM_Base_Stop(&htim6);
}

注意事项:

  • 该函数不可重入,需确保调用时无其他中断使用同一TIM。
  • 适合微秒级延时,毫秒级建议用方案三。

方案三:非阻塞状态机(最佳实践)

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

volatile uint32_t delay_start_tick;
volatile uint8_t delay_flag = 0;

void EXTI_IRQHandler(void)
{
    delay_start_tick = HAL_GetTick();
    delay_flag = 1;
    HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0);
}

void main_loop(void)
{
    if (delay_flag && (HAL_GetTick() - delay_start_tick >= 10))
    {
        delay_flag = 0;
        // 执行延时后的操作
    }
}

总结与建议

  • 绝对禁止在优先级高于SysTick的中断中调用HAL_Delay。
  • 微秒级延时优先使用DWT(Cortex-M3/M4/M7)。
  • 毫秒级延时在中断中应改用非阻塞状态机。
  • 若必须阻塞延时,使用独立硬件定时器并确保中断优先级低于SysTick。

遵循以上原则,可彻底规避HAL_Delay在中断上下文中的隐患,提升系统稳定性与实时性。