一、问题现象与背景
在STM32开发中,HAL_Delay() 几乎是最常用的函数之一。但如果你在中断服务函数(ISR)里调用它,往往会遇到以下现象:
- 程序卡死在
HAL_Delay()内部,看门狗复位; - 延时时间远大于预期;
- 系统响应变慢,其他中断被阻塞。
很多开发者第一反应是“HAL_Delay不准”,但真正的原因在于中断优先级与SysTick的交互机制。
二、HAL_Delay的实现原理
HAL库中 HAL_Delay 的典型实现(以STM32F1/F4为例)如下:
__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_Handler()中递增; -
HAL_Delay本质是忙等待轮询,依赖SysTick中断持续更新uwTick。
三、失效根因分析
3.1 中断优先级冲突
Cortex-M内核的NVIC支持中断嵌套,但同优先级中断不能互相抢占。SysTick默认优先级通常由HAL初始化设置为最低(数值最大)。假设你在某个外设中断(如USART、TIM)中调用 HAL_Delay:
- 当前中断正在执行,SysTick中断无法抢占它(优先级更低或相同);
-
uwTick停止递增; -
HAL_Delay的while循环永远等不到tick变化; - 系统卡死。
即使SysTick优先级高于当前中断,若在ISR中长时间忙等,也会阻塞其他低优先级中断,破坏实时性。
3.2 根本结论
HAL_Delay依赖中断更新时基,而中断上下文本身会屏蔽或延迟时基更新,因此HAL_Delay在ISR中不可靠,应禁止使用。
四、三种安全替代实现
4.1 方案一:DWT周期计数器(推荐,精度高、无中断依赖)
Cortex-M3/M4/M7内核自带DWT(Data Watchpoint and Trace)单元,其中的CYCCNT寄存器记录CPU周期数,可用来做精确忙等延时,完全不依赖中断。
#include "core_cm4.h" // 根据内核型号选择
void DWT_Delay_Init(void)
{
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CYCCNT = 0;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
}
void DWT_Delay_us(uint32_t us)
{
uint32_t start = DWT->CYCCNT;
uint32_t cycles = us * (SystemCoreClock / 1000000U);
while ((DWT->CYCCNT - start) < cycles)
{
__NOP();
}
}
要点:
- 初始化只需一次,可在
main开头调用; - 延时精度取决于
SystemCoreClock,需确保时钟配置正确; - 适用于微秒级短延时,ISR中安全;
- 注意
CYCCNT为32位,长时间延时需考虑溢出(约数十秒@168MHz)。
4.2 方案二:硬件定时器轮询(适合毫秒级、可移植)
使用一个基本定时器(如TIM6/TIM7)自由运行,读取计数器差值实现延时,不依赖中断。
void TIM_Delay_Init(void)
{
__HAL_RCC_TIM6_CLK_ENABLE();
TIM6->PSC = (SystemCoreClock / 1000000U) - 1; // 1MHz计数
TIM6->ARR = 0xFFFF;
TIM6->CR1 |= TIM_CR1_CEN;
}
void TIM_Delay_us(uint16_t us)
{
uint16_t start = TIM6->CNT;
while ((uint16_t)(TIM6->CNT - start) < us)
{
__NOP();
}
}
要点:
- 定时器自由运行,不开启中断;
- 16位计数器最大延时约65ms(1MHz),更长延时需扩展计数;
- 适合毫秒级以内延时,ISR中安全;
- 占用一个定时器资源。
4.3 方案三:状态机非阻塞延时(最佳实践)
在ISR中根本不应阻塞。正确做法是记录时间戳,在主循环或状态机中判断超时。
volatile uint32_t g_last_tick = 0;
volatile uint8_t g_timeout_flag = 0;
void EXTI_IRQHandler(void)
{
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0);
g_last_tick = HAL_GetTick(); // 仅记录,不等待
g_timeout_flag = 0;
}
void App_Task(void)
{
if (g_last_tick != 0 &&
(HAL_GetTick() - g_last_tick) > 100U)
{
g_timeout_flag = 1;
g_last_tick = 0;
// 执行超时后的业务逻辑
}
}
要点:
- ISR只做最小工作,记录时间戳;
- 主循环轮询判断超时,不阻塞任何中断;
- 实时性最好,是嵌入式开发的推荐范式;
- 需注意
HAL_GetTick()在中断关闭时可能不更新,但主循环中调用安全。
五、三种方案对比
- DWT:微秒级,精度最高,无外设占用,适合短延时;
- 硬件定时器:微秒到毫秒,可移植,占用一个定时器;
- 状态机:零阻塞,实时性最佳,适合业务逻辑延时。
六、注意事项
- 严禁在ISR中调用
HAL_Delay,包括回调函数(如HAL_GPIO_EXTI_Callback); - 若必须短延时,优先选DWT方案;
- 使用DWT前确认内核支持(M3及以上),M0/M0+无DWT;
- 定时器方案注意计数器位宽与溢出;
- 状态机方案中共享变量需加
volatile,必要时做临界区保护; - 检查SysTick优先级配置,避免与其他中断冲突。
七、总结
HAL_Delay 失效的本质是中断上下文屏蔽了时基更新。理解SysTick与NVIC的协作机制后,应根据场景选择DWT、硬件定时器或状态机方案,从根源上避免ISR阻塞,提升系统实时性与稳定性。