HAL_Delay 为什么不能在中断里调?
前言
很多 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_Delay,SysTick 中断无法触发,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_Delay,SysTick 也能打断它更新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 会死锁,原因解释清楚 |
