一、问题引入:一个看似无害的调用
在STM32开发中,HAL_Delay()是最常用的延时函数。很多开发者习惯在中断服务函数(ISR)中直接调用它,例如:
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
HAL_Delay(10); // 等待按键消抖
// ... 处理逻辑
}
这段代码在多数情况下“看起来能用”,但在特定条件下会导致系统卡死。本文将从原理出发,复现该问题并给出规避方案。
二、原理剖析:HAL_Delay为什么不能在中断里用
2.1 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中断中的HAL_IncTick()递增:
void SysTick_Handler(void)
{
HAL_IncTick();
}
2.2 死锁的形成条件
关键点在于:SysTick中断的优先级通常是最低的(CubeMX默认生成HAL_InitTick(TICK_INT_PRIORITY),其值为15,即最低优先级)。
当某个外设中断(如EXTI、USART)的抢占优先级高于SysTick时:
- 外设ISR执行,调用
HAL_Delay进入忙等; - 此时SysTick中断无法抢占当前ISR(优先级更低);
-
uwTick停止递增,HAL_GetTick()永远返回同一值; -
while循环永不退出,系统死锁。
这就是“隐性”所在——代码编译无警告,单步调试时若SysTick恰好已更新也可能“碰巧”通过,但在高优先级中断中必然死锁。
三、复现步骤与完整代码
3.1 环境配置
- STM32F407 + STM32CubeMX + HAL库
- 配置一个EXTI中断(按键PA0),抢占优先级设为2
- SysTick保持默认优先级15
3.2 复现代码
/* main.c */
volatile uint32_t isr_enter_cnt = 0;
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == GPIO_PIN_0)
{
isr_enter_cnt++;
HAL_Delay(10); // 死锁点
HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_12);
}
}
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
while (1)
{
HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_13);
HAL_Delay(500);
}
}
3.3 现象验证
- 按下按键后,LED13停止闪烁,LED12不再翻转;
- 调试器暂停,PC停在
HAL_Delay的while循环内; - 查看
uwTick,值不再变化,确认死锁。
四、规避方案
方案一:中断中只置标志,主循环处理(推荐)
volatile uint8_t key_flag = 0;
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == GPIO_PIN_0)
key_flag = 1;
}
/* 主循环 */
while (1)
{
if (key_flag)
{
key_flag = 0;
HAL_Delay(10); // 主上下文安全
HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_12);
}
}
这是最符合“中断快进快出”原则的做法,也避免了在ISR中做耗时操作。
方案二:使用DWT周期计数器做忙等延时
若确实需要在ISR中短延时,可用内核的DWT(Data Watchpoint and Trace)周期计数器,它不依赖任何中断:
void DWT_Delay_Init(void)
{
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CYCCNT = 0;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
}
static inline void DWT_Delay_us(uint32_t us)
{
uint32_t start = DWT->CYCCNT;
uint32_t ticks = us * (SystemCoreClock / 1000000U);
while ((DWT->CYCCNT - start) < ticks) { }
}
注意:DWT延时是纯忙等,会占用CPU,仅适合微秒级短延时,且需确认芯片支持DWT。
方案三:基于定时器的非阻塞状态机
对于需要“等待一段时间再执行”的逻辑,用硬件定时器+状态机替代阻塞延时:
typedef enum { IDLE, WAIT_DEBOUNCE, DONE } key_state_t;
static key_state_t state = IDLE;
static uint32_t t_start;
/* 1ms定时器中断中调用 */
void Key_Process(void)
{
switch (state)
{
case IDLE:
if (key_pressed) { t_start = HAL_GetTick(); state = WAIT_DEBOUNCE; }
break;
case WAIT_DEBOUNCE:
if (HAL_GetTick() - t_start >= 10)
{
HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_12);
state = DONE;
}
break;
default: break;
}
}
五、注意事项
-
优先级检查:若必须在ISR中使用
HAL_Delay,可将SysTick优先级设为最高(0),但这会破坏RTOS的时基设计,不推荐。 -
RTOS场景:在FreeRTOS中,
HAL_Delay同样不可在中断中使用,应改用vTaskDelay(任务上下文)或xTimer机制。 -
代码审查:建议在团队规范中明确“ISR内禁止调用任何阻塞型HAL函数”,包括
HAL_Delay、HAL_UART_Transmit(阻塞模式)等。 -
调试技巧:遇到系统卡死时,先查看
uwTick是否停止,可快速定位此类问题。
六、总结
HAL_Delay在中断上下文的死锁源于其对SysTick中断的隐式依赖与中断优先级的冲突。理解uwTick的更新机制是排查关键。工程实践中应遵循“中断只做标记、耗时逻辑放主循环”的原则,必要时用DWT或定时器状态机替代阻塞延时,从根源上规避此类隐性缺陷。
暂无评论,来抢沙发~