一、问题现象与背景

在STM32开发中,许多开发者习惯使用HAL_Delay()实现毫秒级延时。然而,当在中断服务函数(ISR)中调用HAL_Delay()时,系统可能出现死锁——程序卡死在延时循环中,看门狗复位或系统完全挂起。本文将从HAL库底层实现出发,分析死锁成因,并提供三种可靠的替代方案。

二、HAL_Delay实现原理与死锁成因

2.1 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_Handler()中递增:

void SysTick_Handler(void)
{
  HAL_IncTick();  // uwTick++
  HAL_SYSTICK_IRQHandler();
}

2.2 死锁根本原因:中断优先级冲突

Cortex-M内核中,中断优先级数值越小优先级越高。若某个外设中断(如USART、TIM)的抢占优先级高于或等于SysTick中断,则在该ISR执行期间,SysTick中断无法抢占。此时:

  • ISR中调用HAL_Delay(),进入while循环等待uwTick变化;
  • 但SysTick中断被当前ISR屏蔽(或同优先级无法抢占),uwTick永不递增;
  • HAL_GetTick()始终返回相同值,循环条件永远成立,形成死锁。

关键点:即使SysTick优先级高于当前ISR,若在ISR中调用HAL_Delay(),SysTick仍可抢占并递增uwTick,延时可以完成。但HAL库默认将SysTick优先级设为最低(TICK_INT_PRIORITY = 0x0F),而用户外设中断常配置为更高优先级,导致SysTick无法抢占,从而死锁。

2.3 其他风险

  • 在中断中长时间阻塞会破坏实时性,影响其他中断响应;
  • 若在SysTick_Handler自身中调用HAL_Delay(),则直接死锁(uwTick无法递增)。

三、替代方案与完整代码

3.1 方案一:基于DWT的微秒级忙延时(推荐用于短延时)

Cortex-M3/M4/M7内核自带DWT(Data Watchpoint and Trace)周期计数器,可实现高精度非阻塞延时,不依赖中断。

配置步骤:

  1. 使能DWT:置位CoreDebug->DEMCR的TRCENA位;
  2. 清零DWT->CYCCNT;
  3. 使能周期计数器:置位DWT->CTRL的CYCCNTENA位;
  4. 根据系统主频计算延时周期数。

完整代码:

#include "stm32f4xx.h"  // 根据实际型号调整

void DWT_Delay_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 cycles = us * (SystemCoreClock / 1000000U);
  while ((DWT->CYCCNT - start) < cycles)
  {
    __NOP();
  }
}

使用示例(在ISR中):

void TIM2_IRQHandler(void)
{
  HAL_TIM_IRQHandler(&htim2);
  DWT_Delay_us(10);  // 安全延时10us
}

注意事项:

  • DWT仅Cortex-M3及以上内核支持;
  • 延时期间CPU忙等,不适合长延时(>1ms);
  • 需确保调试器未占用DWT(通常无影响)。

3.2 方案二:硬件定时器单次延时(适合毫秒级)

利用一个基本定时器(如TIM6)实现阻塞延时,不依赖SysTick。

配置步骤(CubeMX):

  1. 启用TIM6,预分频器设为SystemCoreClock/1000000 - 1(1MHz计数);
  2. 自动重装载值设为delay_ms * 1000 - 1;
  3. 生成代码后,在ISR中启动定时器并等待更新标志。

完整代码:

void TIM6_Delay_ms(uint32_t ms)
{
  __HAL_TIM_SET_AUTORELOAD(&htim6, ms * 1000 - 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);
}

注意事项:

  • 需独占一个定时器,避免与其他功能冲突;
  • 若在更高优先级ISR中调用,需确保定时器中断不被屏蔽(此处为轮询标志,不依赖中断,安全);
  • 延时精度取决于定时器时钟。

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

在中断中避免任何阻塞延时,改用状态机+时间戳。

示例:按键消抖

typedef enum { KEY_IDLE, KEY_DEBOUNCE, KEY_PRESSED } KeyState;
KeyState keyState = KEY_IDLE;
uint32_t keyTick = 0;

void EXTI0_IRQHandler(void)
{
  HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0);
  keyTick = HAL_GetTick();
  keyState = KEY_DEBOUNCE;
}

void Key_Process(void)  // 主循环中调用
{
  if (keyState == KEY_DEBOUNCE && (HAL_GetTick() - keyTick) >= 20)
  {
    if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET)
      keyState = KEY_PRESSED;
    else
      keyState = KEY_IDLE;
  }
}

注意事项:

  • 彻底消除中断阻塞,系统实时性最佳;
  • 需合理设计状态机与超时逻辑;
  • 适用于所有需要“延时后执行”的场景。

四、总结与建议

  • 禁止在ISR中调用HAL_Delay(),除非确认SysTick优先级高于当前ISR且延时极短;
  • 短延时(<100us)优先使用DWT;
  • 毫秒级延时且无其他定时器可用时,可用硬件定时器轮询;
  • 最佳实践是重构为非阻塞状态机,从根本上避免中断上下文阻塞;
  • 若必须使用HAL_Delay(),请将SysTick优先级设为最高(数值最小),但会牺牲其他中断的实时性,不推荐。

通过理解HAL_Delay的底层机制并采用上述替代方案,可有效避免死锁,构建稳定可靠的嵌入式系统。