一、问题现象与背景
在STM32开发中,许多开发者习惯使用HAL库提供的HAL_Delay()函数实现毫秒级延时。但在中断服务函数(如EXTI、定时器中断)中调用HAL_Delay()时,程序往往会卡死,表现为系统无响应、看门狗复位或调试器显示程序停在HAL_Delay内部的while循环中。
本文将从HAL_Delay的实现原理出发,分析其在中断上下文中失效的根本原因,并提供三种可靠的替代延时方案。
二、HAL_Delay的实现机制
HAL_Delay的源码位于stm32f1xx_hal.c(以F1系列为例):
__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)
{
}
}
其核心逻辑是:
- 记录当前系统节拍
tickstart = HAL_GetTick(); - 在while循环中不断读取
HAL_GetTick(),直到差值达到延时值; -
HAL_GetTick()返回全局变量uwTick,该变量由SysTick中断服务函数SysTick_Handler()中的HAL_IncTick()递增。
关键点:HAL_Delay依赖SysTick中断来更新uwTick。
三、中断上下文中卡死的原因
3.1 中断优先级与抢占规则
Cortex-M内核的中断优先级数值越小,优先级越高。SysTick中断通常配置为最低优先级(数值最大),而外部中断或定时器中断可能配置为较高优先级。
当程序在某个高优先级中断服务函数中调用HAL_Delay()时:
- 当前中断正在执行,SysTick中断被挂起(因为优先级低于当前中断);
-
HAL_Delay进入while循环,等待uwTick变化; - 但SysTick中断无法抢占当前中断,
uwTick永远不会递增; - while循环条件永远为真,形成死循环,系统卡死。
3.2 即使优先级相同或更低
如果SysTick中断优先级与当前中断相同,根据Cortex-M规则,相同优先级的中断不能互相抢占,SysTick中断同样无法执行。只有SysTick优先级高于当前中断时,HAL_Delay才能正常工作,但这会破坏系统节拍的实时性,不推荐。
3.3 结论
在任何中断服务函数中调用HAL_Delay都是不安全的,除非确保SysTick中断优先级高于该中断,但这会引入其他问题。正确做法是使用不依赖中断的延时方案。
四、替代延时方案
4.1 方案一:基于DWT的周期精确延时
Cortex-M3/M4/M7内核包含数据观察点与跟踪单元(DWT),其中的CYCCNT寄存器记录CPU周期数,可用于精确延时。
配置步骤:
- 使能DWT和CYCCNT;
- 读取系统主频计算每微秒周期数;
- 实现忙等待延时。
代码示例:
#include "core_cm4.h" // 根据内核选择
void DWT_Delay_Init(void)
{
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
DWT->CYCCNT = 0;
}
void DWT_Delay_us(uint32_t us)
{
uint32_t start = DWT->CYCCNT;
uint32_t cycles = us * (SystemCoreClock / 1000000U);
while ((DWT->CYCCNT - start) < cycles)
{
__NOP();
}
}
优点: 不依赖任何中断,可在中断上下文中安全使用;精度高,可达CPU周期级。 注意: 需确保DWT已初始化;不同内核(M0/M0+无DWT)不支持。
4.2 方案二:基于硬件定时器的微秒延时
利用一个基本定时器(如TIM6/TIM7)产生微秒级计数,通过轮询计数器实现延时。
配置步骤(以TIM6为例):
- 使能TIM6时钟;
- 配置预分频器,使计数器频率为1MHz(1us计数);
- 启动定时器;
- 实现延时函数。
代码示例:
void TIM6_Delay_Init(void)
{
__HAL_RCC_TIM6_CLK_ENABLE();
TIM6->PSC = (SystemCoreClock / 1000000U) - 1; // 1MHz计数
TIM6->ARR = 0xFFFF;
TIM6->CR1 |= TIM_CR1_CEN;
}
void TIM6_Delay_us(uint16_t us)
{
uint16_t start = TIM6->CNT;
while ((uint16_t)(TIM6->CNT - start) < us)
{
__NOP();
}
}
优点: 不依赖中断,精度较高;适用于所有STM32系列。 注意: 定时器计数器会溢出,延时时间应小于计数器周期(此处约65ms);若需更长延时,可结合软件循环。
4.3 方案三:基于轮询计数器的简单延时
对于精度要求不高的场景,可使用简单的for循环空操作实现粗略延时。
void Loop_Delay(volatile uint32_t count)
{
while (count--)
{
__NOP();
}
}
优点: 实现简单,无任何依赖。 缺点: 精度差,受编译器优化和主频影响大;需通过实验校准。
五、注意事项
- 禁止在中断中调用HAL_Delay:这是最常见的设计错误,应使用上述替代方案。
- 中断服务函数应尽量简短:避免长时间忙等待,必要时使用状态机或标志位在主循环中处理。
-
DWT延时需初始化:在main函数开头调用
DWT_Delay_Init()。 - 定时器延时注意溢出:使用无符号减法可正确处理溢出,但需确保延时小于计数器周期。
- 优先级配置:若必须使用HAL_Delay,确保SysTick优先级最高,但会牺牲其他中断的实时性,不推荐。
-
调试技巧:若程序卡死,检查调用栈是否停在
HAL_Delay,并确认中断优先级配置。
六、总结
HAL_Delay在中断上下文中卡死的根本原因是其依赖SysTick中断更新节拍,而中断优先级规则导致SysTick无法抢占当前中断。通过DWT、硬件定时器或简单轮询实现不依赖中断的延时,可彻底避免该问题。开发者应根据精度需求和硬件资源选择合适的方案,并养成良好的中断编程习惯。
暂无评论,来抢沙发~