为什么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()接口,从源头杜绝误用。