一、问题现象与背景
在STM32开发中,许多开发者习惯使用HAL_Delay()实现毫秒级延时。但当在中断服务函数(如EXTI、定时器中断)中调用HAL_Delay()时,程序可能永久卡死在延时循环中,表现为系统“死机”。本文从Cortex-M内核与HAL库机制出发,解释死锁成因并给出规避方案。
二、HAL_Delay原理与SysTick机制
HAL_Delay()的实现依赖全局变量uwTick,该变量由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。 -
uwTick仅在SysTick中断服务程序中递增(HAL_IncTick())。 - SysTick中断的优先级由
HAL_InitTick()配置,通常为最低优先级(如TICK_INT_PRIORITY = 0,即最高优先级?实际CubeMX默认15,即最低)。
三、死锁成因分析
3.1 中断优先级与嵌套规则
Cortex-M内核中,高优先级中断可抢占低优先级中断。若当前执行的中断优先级高于SysTick中断优先级,则SysTick中断无法抢占,uwTick停止更新。
3.2 典型死锁场景
假设SysTick优先级配置为15(最低),而某外设中断(如EXTI)优先级配置为5(较高)。在EXTI中断中调用HAL_Delay(100):
- 进入EXTI中断,优先级5。
- 执行
HAL_Delay,循环等待uwTick增加。 - SysTick中断优先级15,低于当前中断优先级,无法抢占。
-
uwTick永不更新,HAL_Delay死循环。
即使SysTick优先级高于当前中断,若在HAL_Delay执行期间发生其他更高优先级中断并长时间占用CPU,也会导致延时不准,但不会死锁。死锁的必要条件是:当前中断优先级高于或等于SysTick优先级(等于时同样无法抢占)。
3.3 特殊情形:在SysTick中断内调用
若在SysTick中断服务程序内调用HAL_Delay,由于SysTick中断自身无法嵌套,uwTick不会递增,必然死锁。
四、规避方案与代码示例
方案一:中断内使用非阻塞延时(查询计数器)
不依赖uwTick,直接使用硬件定时器或DWT周期计数器实现忙等待。示例使用DWT:
__STATIC_INLINE uint32_t DWT_Delay_Init(void)
{
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CYCCNT = 0;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
return 0;
}
__STATIC_INLINE void DWT_Delay_us(uint32_t us)
{
uint32_t start = DWT->CYCCNT;
uint32_t cycles = us * (SystemCoreClock / 1000000);
while ((DWT->CYCCNT - start) < cycles) { }
}
注意:DWT需内核支持,且SystemCoreClock需准确。此方法仍为忙等待,但不受中断优先级影响。
方案二:调整SysTick优先级为最高
在HAL_Init()之后、外设中断使能之前,修改SysTick优先级为0(最高):
HAL_NVIC_SetPriority(SysTick_IRQn, 0, 0);
这样任何中断都无法阻止SysTick更新,HAL_Delay在中断中可正常工作。但需注意:SysTick频繁抢占会影响高优先级中断的实时性,且若在SysTick内调用仍会死锁。不推荐作为通用方案。
方案三:状态机+定时器回调(推荐)
在中断中仅设置标志,在主循环中处理延时逻辑。示例:
volatile uint8_t exti_flag = 0;
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == KEY_Pin) {
exti_flag = 1; // 仅置标志,不延时
}
}
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
while (1) {
if (exti_flag) {
exti_flag = 0;
HAL_Delay(100); // 主循环中安全延时
// 业务处理
}
}
}
此方案彻底避免中断上下文延时,符合RTOS设计原则。
五、配置步骤与注意事项
5.1 检查SysTick优先级
在stm32fxxx_hal_msp.c或main.c中查看HAL_InitTick()调用,确认TICK_INT_PRIORITY宏定义(通常位于stm32fxxx_hal_conf.h)。CubeMX生成的工程默认值为15(最低)。
5.2 避免在中断中调用阻塞函数
- 禁止在ISR中调用
HAL_Delay、HAL_UART_Transmit(阻塞模式)等依赖轮询的函数。 - 使用
HAL_UART_Transmit_IT或DMA替代。
5.3 若必须使用延时
- 优先采用DWT或硬件定时器实现微秒级延时。
- 确保延时时间远小于中断间隔,避免影响实时性。
5.4 调试技巧
若怀疑死锁,可在HAL_Delay循环内翻转GPIO,用示波器观察是否停止翻转;或检查uwTick是否变化。
六、总结
HAL_Delay在中断中死锁的根本原因是SysTick中断被更高优先级中断屏蔽,导致uwTick停止更新。规避的核心原则是:不在中断上下文中使用依赖系统节拍的阻塞延时。推荐采用状态机将延时处理移至主循环,或使用DWT等独立计数器实现短延时。正确配置中断优先级并遵循HAL库使用规范,可有效避免此类问题。
暂无评论,来抢沙发~