一、问题现象与背景
在STM32开发中,许多开发者习惯使用HAL_Delay()实现毫秒级延时。然而,当在中断服务函数(ISR)中调用HAL_Delay()时,系统可能出现死锁——程序卡死在延时循环中,看门狗复位或系统完全挂起。本文将从HAL库底层实现出发,分析死锁成因,并提供三种可靠的替代方案。
二、HAL_Delay实现原理与死锁成因
2.1 HAL_Delay源码分析
HAL库中HAL_Delay()的核心实现如下(以STM32F1为例):
__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()中递增:
void SysTick_Handler(void)
{
HAL_IncTick(); // uwTick++
HAL_SYSTICK_IRQHandler();
}
2.2 死锁根本原因:中断优先级冲突
Cortex-M内核中,中断优先级数值越小优先级越高。若某个外设中断(如USART、TIM)的抢占优先级高于或等于SysTick中断,则在该ISR执行期间,SysTick中断无法抢占。此时:
- ISR中调用
HAL_Delay(),进入while循环等待uwTick变化; - 但SysTick中断被当前ISR屏蔽(或同优先级无法抢占),
uwTick永不递增; -
HAL_GetTick()始终返回相同值,循环条件永远成立,形成死锁。
关键点:即使SysTick优先级高于当前ISR,若在ISR中调用HAL_Delay(),SysTick仍可抢占并递增uwTick,延时可以完成。但HAL库默认将SysTick优先级设为最低(TICK_INT_PRIORITY = 0x0F),而用户外设中断常配置为更高优先级,导致SysTick无法抢占,从而死锁。
2.3 其他风险
- 在中断中长时间阻塞会破坏实时性,影响其他中断响应;
- 若在
SysTick_Handler自身中调用HAL_Delay(),则直接死锁(uwTick无法递增)。
三、替代方案与完整代码
3.1 方案一:基于DWT的微秒级忙延时(推荐用于短延时)
Cortex-M3/M4/M7内核自带DWT(Data Watchpoint and Trace)周期计数器,可实现高精度非阻塞延时,不依赖中断。
配置步骤:
- 使能DWT:置位
CoreDebug->DEMCR的TRCENA位; - 清零
DWT->CYCCNT; - 使能周期计数器:置位
DWT->CTRL的CYCCNTENA位; - 根据系统主频计算延时周期数。
完整代码:
#include "stm32f4xx.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();
}
}
使用示例(在ISR中):
void TIM2_IRQHandler(void)
{
HAL_TIM_IRQHandler(&htim2);
DWT_Delay_us(10); // 安全延时10us
}
注意事项:
- DWT仅Cortex-M3及以上内核支持;
- 延时期间CPU忙等,不适合长延时(>1ms);
- 需确保调试器未占用DWT(通常无影响)。
3.2 方案二:硬件定时器单次延时(适合毫秒级)
利用一个基本定时器(如TIM6)实现阻塞延时,不依赖SysTick。
配置步骤(CubeMX):
- 启用TIM6,预分频器设为
SystemCoreClock/1000000 - 1(1MHz计数); - 自动重装载值设为
delay_ms * 1000 - 1; - 生成代码后,在ISR中启动定时器并等待更新标志。
完整代码:
void TIM6_Delay_ms(uint32_t ms)
{
__HAL_TIM_SET_AUTORELOAD(&htim6, ms * 1000 - 1);
__HAL_TIM_SET_COUNTER(&htim6, 0);
HAL_TIM_Base_Start(&htim6);
while (__HAL_TIM_GET_FLAG(&htim6, TIM_FLAG_UPDATE) == RESET);
__HAL_TIM_CLEAR_FLAG(&htim6, TIM_FLAG_UPDATE);
HAL_TIM_Base_Stop(&htim6);
}
注意事项:
- 需独占一个定时器,避免与其他功能冲突;
- 若在更高优先级ISR中调用,需确保定时器中断不被屏蔽(此处为轮询标志,不依赖中断,安全);
- 延时精度取决于定时器时钟。
3.3 方案三:非阻塞状态机(最佳实践)
在中断中避免任何阻塞延时,改用状态机+时间戳。
示例:按键消抖
typedef enum { KEY_IDLE, KEY_DEBOUNCE, KEY_PRESSED } KeyState;
KeyState keyState = KEY_IDLE;
uint32_t keyTick = 0;
void EXTI0_IRQHandler(void)
{
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0);
keyTick = HAL_GetTick();
keyState = KEY_DEBOUNCE;
}
void Key_Process(void) // 主循环中调用
{
if (keyState == KEY_DEBOUNCE && (HAL_GetTick() - keyTick) >= 20)
{
if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET)
keyState = KEY_PRESSED;
else
keyState = KEY_IDLE;
}
}
注意事项:
- 彻底消除中断阻塞,系统实时性最佳;
- 需合理设计状态机与超时逻辑;
- 适用于所有需要“延时后执行”的场景。
四、总结与建议
-
禁止在ISR中调用
HAL_Delay(),除非确认SysTick优先级高于当前ISR且延时极短; - 短延时(<100us)优先使用DWT;
- 毫秒级延时且无其他定时器可用时,可用硬件定时器轮询;
- 最佳实践是重构为非阻塞状态机,从根本上避免中断上下文阻塞;
- 若必须使用
HAL_Delay(),请将SysTick优先级设为最高(数值最小),但会牺牲其他中断的实时性,不推荐。
通过理解HAL_Delay的底层机制并采用上述替代方案,可有效避免死锁,构建稳定可靠的嵌入式系统。
暂无评论,来抢沙发~