前言很多 STM32 新手写过这样的代码void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { HAL_Delay(50); // 按键消抖 HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); }下载后发现按键按下去程序卡死了。为什么HAL_Delay在 main 循环里用得好好的进中断就出问题这篇文章彻底讲清楚。一、HAL_Delay 是怎么实现的打开stm32f4xx_hal.c看看HAL_Delay的代码__weak void HAL_Delay(uint32_t Delay) { uint32_t tickstart HAL_GetTick(); uint32_t wait Delay; ​ /* 防止 tick 计数器溢出导致 HAL_Delay 判定异常 */ if (wait HAL_MAX_DELAY) { wait (uint32_t)(uwTickFreq); } ​ while ((HAL_GetTick() - tickstart) wait) { } }核心逻辑就一句话不断读当前 tick等时间到了才返回。而HAL_GetTick()读取的是uwTick这个全局变量__weak void HAL_IncTick(void) { uwTick uwTickFreq; }这个uwTick谁来更新答案是// stm32f4xx_it.c 中 void SysTick_Handler(void) { HAL_IncTick(); }SysTick 中断每 1ms 触发一次uwTick才加 1。二、中断优先级导致死锁问题就在这里。HAL_Delay是一个死等的函数——它在 while 循环里反复读uwTick直到值达到目标。但uwTick是由SysTick 中断更新的。如果发生这种情况main 中正常运行 ↓ 按键按下了 ← 外部中断触发EXTI ↓ CPU 跳进 EXTI 中断服务函数 ↓ 调用 HAL_Delay(50) ← 开始死等 uwTick ↓ SysTick 中断本该在 1ms 后更新 uwTick ↓ 但是SysTick 的中断优先级 EXTI 的中断优先级 ↓ SysTick 中断被 EXTI 挡住了进不去 ↓ uwTick 永远不增加 ↓ HAL_Delay 永远等下去 ↓ 卡死关键是中断优先级的关系中断默认抢占优先级备注EXTI较低数字高优先默认比 SysTick 高通常为 0 或 5取决于 CubeMX 配置SysTick较大数字低优先默认 15高优先级中断在执行时同优先级或更低优先级的中断无法打断它。所以当 EXTI 中断里调HAL_DelaySysTick 中断无法触发uwTick卡住死锁。三、验证方法在 EXTI 回调中试着直接读HAL_GetTick()void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { uint32_t t1 HAL_GetTick(); for (volatile int i 0; i 10000; i); // 磨蹭一会 uint32_t t2 HAL_GetTick(); ​ printf(t1%lu, t2%lu, diff%lu\r\n, t1, t2, t2 - t1); // t1 和 t2 永远相等——SysTick 中断没进来 }四、解决办法4.1 不要在中断里死等这是最根本的解决思路。中断应该快进快出// 错误在中断里等 void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { HAL_Delay(50); // ❌ 不要这样做 } ​ // 正确中断里只置标志位 volatile uint8_t key_flag 0; ​ void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { key_flag 1; // ✅ 标记事件主循环处理 } ​ // main 循环中 while (1) { if (key_flag) { key_flag 0; HAL_Delay(20); // 现在在 main 里等没问题 // ... 处理按键 } }中断只干两件事置标志位、接收数据。剩下的主循环做。4.2 提高 SysTick 优先级不推荐把 SysTick 的中断优先级设得比 EXTI 高这样即使 EXTI 里调HAL_DelaySysTick 也能打断它更新uwTick。HAL_InitTick(TICK_INT_PRIORITY); // 在 main 中调用值越小优先级越高比如设HAL_InitTick(0)SysTick 就是最高优先级。为什么不推荐这只解决了 HAL_Delay 的问题其他在中断中可能被调用的需要时间等待的函数也会遇到同样的坑。而且改变了系统的中断优先级设计可能会引入其他 bug。4.3 自定义非阻塞延时推荐学习写法// 非阻塞延时检查 uint8_t Check_Delay(uint32_t *start_tick, uint32_t delay_ms) { if ((HAL_GetTick() - *start_tick) delay_ms) return 1; // 时间到了 return 0; // 还没到 } // 使用方式 void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { static uint32_t last_tick 0; if (Check_Delay(last_tick, 200)) // 距上次触发超过 200ms 才处理 { last_tick HAL_GetTick(); HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); } }这段代码不阻塞、不进死等在任何上下文中都能安全调用。五、还有哪些函数不能在中断里调HAL 库中带有超时参数、内部有 while 死等的函数都不适合在中断里直接调函数原因HAL_Delay()死等 uwTick 更新HAL_UART_Transmit(..., timeout)等发送完成中断优先级可能导致死锁HAL_SPI_Transmit(..., timeout)同上HAL_I2C_Master_Transmit(..., timeout)同上所有PollFor...函数本质上都是轮询等待这些函数在中断优先级低于 SysTick的情况下调用不会死锁但这不是可靠的设计。中断里能安全调用的置标志位、读写简单变量往队列/缓冲区写入数据HAL_xxx_Transmit_IT()中断版——它只启动传输就返回六、如果必须要延时怎么办有些场景确实需要在中断里等一会儿比如跟特定时序的传感器通信。方案状态机 定时器// 定时器每 100μs 触发一次中断 // 状态机在定时器中断中推进 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { static uint8_t step 0; switch (step) { case 0: // 拉低 CS CS_LOW(); step; break; case 1: // 等 1ms 后发命令 if (delay_check(1)) { // 自制的简易计时器 SPI_Transmit(cmd); step; } break; case 2: // 等等... // ... break; } }不用HAL_Delay用定时器驱动状态机逐步推进每个步骤只做一点点事。七、总结要点一句话为什么死锁高优先级中断里等 uwTick但 SysTick 进不来最佳实践中断只置标志主循环再处理耗时操作不要改 SysTick 优先级来绕过治标不治本非要延时用定时器状态机不用 HAL_Delay面试常问这就是考点——中断里的 HAL_Delay 会死锁原因解释清楚