news 2026/7/6 14:46:07

HAL_Delay 为什么不能在中断里调?

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 会死锁,原因解释清楚
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/6 14:44:11

KMX62与PIC18F45K22在嵌入式运动控制中的优化应用

1. 为什么选择KMX62与PIC18F45K22组合在嵌入式运动控制领域&#xff0c;传感器与微控制器的选型往往决定了整个系统的性能上限。KMX62作为一款集成三轴加速度计和三轴磁力计的6DOF IMU传感器&#xff0c;与PIC18F45K22这款8位微控制器的组合&#xff0c;在成本、性能和功耗之间…

作者头像 李华
网站建设 2026/7/6 14:40:17

什么是*Bash* *循环*的正确用法?

循环结构存在于每种编程语言中,包括 Bash。循环是 shell 脚本中的重要构建块,它允许迭代执行一段代码。Bash 循环结构包括 for 循环、while 循环和 until 循环。 👉 有时,你可能会看到 bash 中 select 循环 的引用。它不属于 bash 循环结构 的一部分。你可以在我的博文 《…

作者头像 李华
网站建设 2026/7/6 14:37:26

模板基础与 SFINAE

模板基础与 SFINAE 1. 函数模板与类模板的区别 面试题&#xff1a;函数模板和类模板在使用上有什么关键区别&#xff1f; 函数模板支持模板参数推导——调用时不需要指定模板参数&#xff0c;编译器根据实参类型自动推导&#xff1a; #include <iostream> #include <t…

作者头像 李华
网站建设 2026/7/6 14:37:21

git的分支介绍

当你在做一个新功能的时候&#xff0c;最好是在一个独立的区域上开发&#xff0c;通常称之为分支。分支之间相互独立&#xff0c;并且拥有自己的历史记录。这样做的原因是&#xff1a;稳定版本的代码不会被破坏不同的功能可以由不同开发者同时开发。开发者可以专注于自己的分支…

作者头像 李华
网站建设 2026/7/6 14:36:39

MyBatisPlus--简介+入门案例

一、简介入门案例MyBatisPlus 主要是对 MyBatis 的简化MybatisPlus(简称 MP)是基于 MyBatis 框架基础上开发的增强型工具&#xff0c;旨在简化开发、提高效率。开发方式&#xff1a;基于 SpringBoot 使用 MyBatisPlusMyBatisPlus 的官网为:https://baomidou.com/introduce/2、入…

作者头像 李华