为什么HAL_Delay不能在中断中调用?

1. HAL_Delay的底层实现

HAL_Delay依赖SysTick中断递增的全局变量uwTick实现延时:

__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_Delay,且当前中断优先级高于或等于SysTick优先级,SysTick中断无法抢占,uwTick停止更新,导致死循环。

2. 中断上下文调用的三大隐患

2.1 系统死锁

当调用HAL_Delay的中断优先级数值小于等于SysTick优先级(即优先级更高或相等)时,SysTick被屏蔽,uwTick冻结,while循环永不退出。实测:在优先级为0的中断中调用HAL_Delay(100),系统立即挂死。

2.2 破坏实时性

即使SysTick能抢占(中断优先级低于SysTick),HAL_Delay也会让CPU空转等待,阻塞同优先级及更低优先级中断,严重破坏系统实时响应。

2.3 优先级反转风险

若在中断中调用HAL_Delay等待某个由低优先级任务释放的资源,而该任务又被当前中断阻塞,形成死锁。

3. 替代方案与代码实现

3.1 方案一:DWT周期计数器(微秒级忙等)

利用Cortex-M内核的DWT->CYCCNT寄存器实现精准忙等,不依赖任何中断:

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

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

适用场景:中断中需要极短延时(<100us),如传感器时序控制。

3.2 方案二:硬件定时器单次触发(非阻塞)

配置一个基本定时器(如TIM6)为单次模式,在中断中启动定时器并立即返回,在定时器中断中执行后续操作:

// 初始化TIM6为单次触发模式
void TIM6_Init_OnePulse(uint32_t delay_us)
{
  __HAL_RCC_TIM6_CLK_ENABLE();
  TIM6->PSC = SystemCoreClock / 1000000U - 1;  // 1MHz计数
  TIM6->ARR = delay_us - 1;
  TIM6->CR1 |= TIM_CR1_OPM;  // 单次模式
  TIM6->DIER |= TIM_DIER_UIE;
  HAL_NVIC_SetPriority(TIM6_IRQn, 1, 0);
  HAL_NVIC_EnableIRQ(TIM6_IRQn);
}

// 在中断中调用:启动定时器后立即返回
void EXTI0_IRQHandler(void)
{
  TIM6->CNT = 0;
  TIM6->CR1 |= TIM_CR1_CEN;  // 启动定时器
  // 立即返回,不阻塞
}

// 定时器中断中执行后续操作
void TIM6_IRQHandler(void)
{
  TIM6->SR &= ~TIM_SR_UIF;
  // 执行延时后的操作
}

适用场景:中断中需要较长延时且不能阻塞CPU。

3.3 方案三:状态机+软件定时器(推荐)

在SysTick中断中维护软件定时器链表,中断中注册回调,主循环或低优先级中断中处理:

typedef struct {
  uint32_t timeout;
  void (*callback)(void);
  uint8_t active;
} SoftTimer_t;

SoftTimer_t timer1;

void SoftTimer_Start(SoftTimer_t *t, uint32_t ms, void (*cb)(void))
{
  t->timeout = HAL_GetTick() + ms;
  t->callback = cb;
  t->active = 1;
}

// 在SysTick中断中调用(优先级需低于SysTick)
void EXTI0_IRQHandler(void)
{
  SoftTimer_Start(&timer1, 100, MyCallback);
}

// 主循环中轮询
void Main_Loop(void)
{
  if (timer1.active && HAL_GetTick() >= timer1.timeout) {
    timer1.active = 0;
    timer1.callback();
  }
}

适用场景:需要多个延时任务且对实时性要求不苛刻。

4. 注意事项

  • 优先级配置:SysTick优先级应设为最低(数值最大),确保任何中断都能抢占它,避免uwTick冻结。
  • DWT精度:DWT_DelayUs受CPU频率影响,需确保SystemCoreClock正确;中断中调用时注意关中断时间不宜过长。
  • 硬件定时器资源:单次触发模式会占用一个定时器,多任务时需合理分配。
  • 软件定时器溢出:使用HAL_GetTick()差值比较可避免溢出问题。
  • 调试建议:在中断中调用HAL_Delay前,用逻辑分析仪观察SysTick波形,确认是否被屏蔽。

5. 总结

HAL_Delay在中断上下文中调用是嵌入式开发中的经典陷阱。理解其依赖SysTick的机制后,应根据延时长短和实时性要求选择DWT忙等、硬件定时器或软件状态机方案。推荐在工程中封装Delay_Us()和Delay_Ms_Async()接口,从源头杜绝误用。