一、引言
在STM32开发中,HAL_Delay() 是HAL库最常用的延时函数之一。它看似简单,却隐藏着一个与SysTick中断优先级相关的致命陷阱:在中断服务函数(ISR)中调用 HAL_Delay(),若该中断的优先级高于SysTick,将导致系统死锁。许多开发者在调试时遇到程序“卡死”却难以定位原因。本文将从原理、复现、分析到修复,完整解析这一陷阱。
二、HAL_Delay与SysTick的工作原理
2.1 HAL_Delay的实现
HAL库中 HAL_Delay() 的源码(简化)如下:
__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中断服务函数 SysTick_Handler() 中递增(通过 HAL_IncTick())。
2.2 SysTick中断与优先级
SysTick是Cortex-M内核的一个系统定时器,通常配置为1ms中断一次。其中断优先级由 HAL_InitTick() 设置,默认使用最低优先级(TICK_INT_PRIORITY,通常为15)。
关键点:HAL_Delay() 依赖SysTick中断来更新 uwTick。如果SysTick中断无法执行,uwTick 停止递增,HAL_Delay() 将永远等待,形成死锁。
三、死锁复现
3.1 硬件与软件环境
- STM32F407 Discovery板
- STM32CubeMX生成HAL库工程
- 系统时钟168MHz,SysTick优先级默认15
3.2 配置一个高优先级外部中断
使用按键EXTI中断,将其优先级设置为高于SysTick(例如优先级5,数值越小优先级越高)。在CubeMX中配置PA0为EXTI0,并使能中断。
3.3 在中断中调用HAL_Delay
在 stm32f4xx_it.c 的EXTI0中断服务函数中调用 HAL_Delay(100):
void EXTI0_IRQHandler(void)
{
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0);
}
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == GPIO_PIN_0)
{
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
HAL_Delay(100); // 死锁发生点
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
}
}
3.4 现象
按下按键后,LED第一次翻转,然后程序卡死,LED不再变化。调试器暂停后发现程序停留在 HAL_Delay() 的while循环中,uwTick 不再变化。
四、根本原因分析
Cortex-M内核的中断优先级规则:高优先级中断可以打断低优先级中断,但低优先级中断不能打断高优先级中断。
- EXTI0中断优先级为5,SysTick优先级为15(数值越大优先级越低)。
- 当EXTI0中断执行时,SysTick中断被挂起(pending),无法抢占EXTI0。
-
HAL_Delay()等待uwTick变化,但uwTick只能在SysTick中断中更新。 - 由于SysTick无法执行,
uwTick恒定不变,HAL_Delay()陷入无限循环。
结论:在任何优先级高于SysTick的中断服务函数中调用 HAL_Delay(),都会导致死锁。
五、修复方案
方案一:调整中断优先级(不推荐)
将EXTI0中断优先级设置为低于SysTick(例如优先级16,但Cortex-M4优先级范围通常为0-15,需确保SysTick优先级高于所有调用HAL_Delay的中断)。但这种方法限制了中断响应实时性,且容易遗漏。
方案二:在中断中使用非阻塞延时(推荐)
利用状态机或定时器实现非阻塞延时,避免在ISR中等待。例如使用一个软件定时器或记录时间戳,在主循环中处理:
volatile uint32_t ledToggleTime = 0;
volatile uint8_t ledState = 0;
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == GPIO_PIN_0)
{
ledToggleTime = HAL_GetTick();
ledState = 1;
}
}
// 主循环中
while (1)
{
if (ledState && (HAL_GetTick() - ledToggleTime >= 100))
{
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
ledState = 0;
}
}
方案三:重写HAL_Delay为不依赖中断的延时(适用于简单场景)
使用DWT周期计数器或简单的循环延时,但精度和功耗较差。不推荐在低功耗应用中使用。
void Delay_NoInterrupt(uint32_t ms)
{
uint32_t start = DWT->CYCCNT;
uint32_t cycles = ms * (SystemCoreClock / 1000);
while ((DWT->CYCCNT - start) < cycles);
}
需先使能DWT:CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
六、最佳实践与注意事项
-
永远不要在中断服务函数中调用
HAL_Delay(),除非你确定该中断优先级低于SysTick。 - 使用CubeMX配置时,检查
TICK_INT_PRIORITY宏定义(在stm32f4xx_hal_conf.h中),确保SysTick优先级高于所有可能调用延时的中断。 - 在中断中尽量只做标记,将耗时操作放到主循环。
- 若必须使用延时,考虑使用硬件定时器或DWT。
- 调试死锁时,检查
uwTick是否停止递增,可快速定位。
七、总结
HAL_Delay() 的SysTick优先级陷阱是STM32 HAL开发中的经典问题。理解其原理后,通过合理的架构设计(中断中避免阻塞)和优先级配置,可以彻底避免死锁。希望本文能帮助你在嵌入式开发中少走弯路。
暂无评论,来抢沙发~