一、问题现象:中断里调用HAL_Delay为何会死锁
许多开发者在使用STM32 HAL库时,习惯在中断服务函数(如EXTI、UART接收中断)中调用HAL_Delay()实现简单延时,却发现程序卡死。例如:
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == KEY_Pin) {
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
HAL_Delay(500); // 死锁!
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
}
}
现象是LED只翻转一次,程序不再响应任何中断。这并非HAL库的bug,而是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()中递增:
void SysTick_Handler(void)
{
HAL_IncTick(); // uwTick++
}
2.2 死锁的根源
Cortex-M内核的中断嵌套规则:高优先级中断可以打断低优先级中断,反之不行。HAL库默认将SysTick优先级配置为最低(TICK_INT_PRIORITY = 0x0F,即15)。当程序运行在某个外设中断(优先级高于15)中调用HAL_Delay时:
- 当前中断优先级高于SysTick,SysTick中断被挂起,无法执行;
-
uwTick停止递增,HAL_GetTick()返回值恒定; -
while循环条件永远为真,形成死锁。
若该外设中断优先级低于SysTick,则SysTick可正常抢占,HAL_Delay反而能工作——但这依赖优先级配置,极不可靠。
三、三种修复方案与对比
方案一:提升SysTick中断优先级
修改HAL_InitTick()中的优先级参数,使SysTick优先级高于所有可能调用HAL_Delay的中断。
/* 在 stm32f1xx_hal_conf.h 中修改 */
#define TICK_INT_PRIORITY 0U // 最高优先级
或在HAL_InitTick中直接指定:
HAL_NVIC_SetPriority(SysTick_IRQn, 0, 0);
优点:改动最小,一行宏定义即可。
缺点:SysTick成为最高优先级,会抢占所有中断,增加系统抖动;若其他中断也调用HAL_Delay且优先级高于SysTick,问题依旧。不推荐用于复杂系统。
方案二:使用硬件定时器实现阻塞延时
配置一个基本定时器(如TIM6),提供独立于SysTick的延时服务。
/* tim_delay.c */
#include "stm32f1xx_hal.h"
extern TIM_HandleTypeDef htim6;
void Delay_us(uint32_t us)
{
__HAL_TIM_SET_COUNTER(&htim6, 0);
HAL_TIM_Base_Start(&htim6);
while (__HAL_TIM_GET_COUNTER(&htim6) < us);
HAL_TIM_Base_Stop(&htim6);
}
void Delay_ms(uint32_t ms)
{
while (ms--) Delay_us(1000);
}
初始化TIM6为1MHz计数频率(预分频72-1,ARR=0xFFFF):
htim6.Instance = TIM6;
htim6.Init.Prescaler = 72 - 1;
htim6.Init.Period = 0xFFFF;
HAL_TIM_Base_Init(&htim6);
优点:延时精度高,不依赖中断,可在任意中断中安全调用。
缺点:占用一个硬件定时器资源;阻塞期间CPU空转。
方案三:改用轮询计数器(非阻塞延时)
在中断中避免阻塞,改用状态机+时间戳的非阻塞方式:
static uint32_t last_tick = 0;
static uint8_t step = 0;
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == KEY_Pin && step == 0) {
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
last_tick = HAL_GetTick();
step = 1;
}
}
/* 在主循环中轮询 */
void App_Loop(void)
{
if (step == 1 && (HAL_GetTick() - last_tick) >= 500) {
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
step = 0;
}
}
优点:不阻塞CPU,实时性最佳,符合嵌入式设计规范。
缺点:需重构业务逻辑,代码量稍大。
方案对比表
| 维度 | 方案一(提优先级) | 方案二(硬件定时器) | 方案三(非阻塞) | |------|-------------------|---------------------|-----------------| | 实时性影响 | 大(SysTick抢占) | 无 | 无 | | 代码侵入性 | 极小 | 中(需定时器) | 大(重构逻辑) | | 可靠性 | 低(依赖优先级) | 高 | 最高 | | 资源占用 | 无 | 一个定时器 | 无 | | 适用场景 | 简单项目 | 需精确延时 | 复杂系统 |
四、注意事项
- 禁止在中断中调用HAL_Delay:这是HAL库的已知限制,应作为编码规范强制执行。
-
检查所有中断优先级:使用
HAL_NVIC_SetPriority时确保SysTick优先级不低于调用延时函数的中断。 -
FreeRTOS用户注意:若使用FreeRTOS,
HAL_Delay可能被重定向为osDelay,在中断中调用同样非法,应使用vTaskDelayFromISR或非阻塞方案。 -
调试技巧:若怀疑死锁,可在
SysTick_Handler中翻转GPIO,用示波器观察是否停止翻转。 - 推荐方案三:在资源允许的情况下,中断中只做标记,延时逻辑放到主循环,这是最稳健的嵌入式设计模式。
五、总结
HAL_Delay死锁的本质是SysTick中断被更高优先级中断屏蔽,导致uwTick停止更新。三种修复方案各有取舍:提优先级治标不治本,硬件定时器延时灵活可靠,非阻塞设计则是根治之道。理解Cortex-M中断嵌套机制,才能从根源上避免此类陷阱。
暂无评论,来抢沙发~