一、问题现象:中断里调用HAL_Delay为什么会卡死
在STM32 HAL库开发中,HAL_Delay()是最常用的毫秒延时函数。但如果你在中断服务函数(ISR)里写:
void EXTI0_IRQHandler(void)
{
HAL_Delay(10); // 期望延时10ms
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0);
}
常见结果是:程序卡在中断里出不来,或者延时时间远大于10ms,甚至看门狗复位。这不是HAL库的bug,而是机制决定的必然结果。
二、根因剖析
2.1 HAL_Delay的实现依赖SysTick中断
HAL库的毫秒时基默认由SysTick提供。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变量,只在SysTick_Handler()中递增。也就是说,延时的推进完全依赖SysTick中断能够被响应。
2.2 中断优先级决定SysTick能否抢占
Cortex-M内核中,中断按优先级抢占。SysTick的优先级由HAL_InitTick()设置,通常为最低(数值最大)。当你在某个外设中断里调用HAL_Delay时:
- 当前中断的优先级若高于SysTick(数值更小),SysTick无法抢占;
-
uwTick不再递增,HAL_GetTick()永远返回同一个值; -
while循环条件恒成立,程序死等,形成中断内死锁。
即使当前中断优先级低于SysTick,SysTick能进来,HAL_Delay也会让CPU在中断上下文中空转,阻塞同级和低优先级中断,实时性被破坏。
2.3 结论
HAL_Delay本质是“依赖中断的阻塞式延时”,它只适用于线程上下文(main循环),不应在ISR中使用。
三、三种中断安全的替代延时方案
方案一:基于循环计数的忙等延时(粗延时)
最简单的方式是用空循环消耗时间,不依赖任何中断。缺点是精度受主频、编译优化、Flash等待周期影响,需要实测校准。
/* 粗略微秒延时,需按实际主频校准 */
static inline void delay_us_busy(uint32_t us)
{
/* 72MHz下约循环 12 次为 1us,实测校准 */
uint32_t cnt = us * 12U;
while (cnt--) {
__NOP();
}
}
适用场景:对精度要求不高的短延时(几微秒到几百微秒),如传感器时序中的建立时间。
方案二:基于硬件定时器计数的延时
利用一个已启动的定时器(如TIM2)的计数器寄存器做差值判断,不依赖中断,精度由定时器时钟决定。
/* 假设TIM2已配置为1MHz计数频率并启动 */
static inline void delay_us_tim(uint32_t us)
{
uint32_t start = __HAL_TIM_GET_COUNTER(&htim2);
while ((__HAL_TIM_GET_COUNTER(&htim2) - start) < us) { }
}
要点:
- 定时器需在初始化时
HAL_TIM_Base_Start(),且计数频率已知; - 计数器为16位时注意溢出,差值运算对无符号数天然正确;
- 该方案不产生中断,可在任意优先级ISR中安全使用。
方案三:基于DWT周期计数器的精确延时
Cortex-M3/M4/M7内核自带DWT(Data Watchpoint and Trace)单元,其CYCCNT寄存器按CPU主频递增,精度可达一个时钟周期,是中断内延时的最佳选择。
static inline void dwt_init(void)
{
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CYCCNT = 0;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
}
static inline void delay_us_dwt(uint32_t us)
{
uint32_t start = DWT->CYCCNT;
uint32_t ticks = us * (SystemCoreClock / 1000000U);
while ((DWT->CYCCNT - start) < ticks) { }
}
要点:
- 使用前必须先使能DWT,否则CYCCNT不计数;
- 部分调试器会占用DWT,量产前需验证;
- 延时期间CPU忙等,但不会阻塞中断,适合ISR内的短延时。
四、选型建议
- 微秒级、精度要求高:优先DWT方案;
- 已有空闲定时器且不想占用内核资源:定时器计数方案;
- 对精度无要求、代码最简:忙等循环方案;
- 毫秒级以上延时:不要在ISR里做,改用状态机+定时器回调,把延时逻辑移到主循环。
五、注意事项
- 不要在ISR中调用任何依赖
HAL_GetTick()的函数,包括HAL_Delay、带超时的HAL_xxx轮询接口; - 若必须调整SysTick优先级,可提高其抢占优先级,但这会引入新的实时性风险,不推荐作为通用解法;
- 忙等延时期间关闭全局中断(
__disable_irq())会放大副作用,应避免; - 使用DWT前确认芯片内核型号,Cortex-M0/M0+不支持DWT CYCCNT;
- 所有延时参数应结合示波器或逻辑分析仪实测校准,不要照搬理论值。
暂无评论,来抢沙发~