一、问题现象:中断里调用HAL_Delay为何会卡死

许多开发者习惯在中断服务函数(ISR)中调用HAL_Delay()实现简单延时,但常发现程序“卡死”在延时循环中,无法退出。例如:

void EXTI0_IRQHandler(void)
{
    HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0);
    HAL_Delay(100); // 期望延时100ms
    HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
}

现象:系统无响应,调试器显示程序停留在HAL_Delay内部的while循环。

二、根本原因:SysTick中断优先级与阻塞延时的冲突

2.1 HAL_Delay的实现机制

HAL_Delay()基于SysTick中断实现,核心代码如下(简化):

__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();
}

2.2 中断优先级的关键作用

Cortex-M内核中,中断优先级数值越小优先级越高。默认情况下,SysTick中断优先级由HAL_InitTick()设置,通常为最低(数值最大)。若用户中断的优先级高于SysTick,则:

  • 在用户ISR中调用HAL_Delay()时,CPU处于中断上下文,当前优先级为ISR优先级。
  • SysTick中断因优先级较低,无法抢占当前ISR,uwTick停止更新。
  • HAL_Delay()的while循环依赖uwTick变化,因此永远等待,形成死锁。

即使SysTick优先级高于用户ISR,若在ISR中调用HAL_Delay(),SysTick仍可打断ISR并更新uwTick,延时可以完成,但会延长ISR执行时间,破坏实时性。更严重的是,若在更高优先级中断中调用,则必然卡死。

2.3 其他潜在风险

  • 在SysTick_Handler本身或优先级高于SysTick的ISR中调用HAL_Delay(),必然死锁。
  • 若用户修改了SysTick优先级低于某个ISR,而该ISR调用了HAL_Delay(),同样死锁。

三、替代延时方案

3.1 方案一:DWT周期计数器实现微秒级忙等待

Cortex-M3/M4/M7内核包含DWT(Data Watchpoint and Trace)单元,其CYCCNT寄存器记录内核周期数,可实现高精度延时,且不依赖中断。

配置步骤:

  1. 使能DWT和CYCCNT:
void DWT_Delay_Init(void)
{
    CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
    DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
    DWT->CYCCNT = 0;
}
  1. 实现微秒延时(假设系统时钟为SystemCoreClock):
void DWT_Delay_us(uint32_t us)
{
    uint32_t start = DWT->CYCCNT;
    uint32_t cycles = us * (SystemCoreClock / 1000000U);
    while ((DWT->CYCCNT - start) < cycles) { }
}
  1. 毫秒延时基于微秒实现:
void DWT_Delay_ms(uint32_t ms)
{
    while (ms--) {
        DWT_Delay_us(1000);
    }
}

优点: 不依赖中断,可在任意ISR中安全使用;精度高。
缺点: 忙等待消耗CPU,仅适合短延时。

3.2 方案二:硬件定时器非阻塞延时

使用一个基本定时器(如TIM6)产生固定周期中断,维护软件定时器列表,实现非阻塞延时。适合需要长时间延时且不阻塞CPU的场景。

配置步骤(以TIM6为例):

  1. 初始化定时器为1ms中断:
void TIM6_Init(void)
{
    __HAL_RCC_TIM6_CLK_ENABLE();
    TIM6->PSC = SystemCoreClock / 1000000U - 1; // 1MHz计数
    TIM6->ARR = 1000 - 1; // 1ms中断
    TIM6->DIER |= TIM_DIER_UIE;
    TIM6->CR1 |= TIM_CR1_CEN;
    HAL_NVIC_SetPriority(TIM6_IRQn, 1, 0);
    HAL_NVIC_EnableIRQ(TIM6_IRQn);
}
  1. 在中断中递增软件定时器计数:
volatile uint32_t soft_timer_tick = 0;
void TIM6_IRQHandler(void)
{
    if (TIM6->SR & TIM_SR_UIF) {
        TIM6->SR &= ~TIM_SR_UIF;
        soft_timer_tick++;
    }
}
  1. 实现非阻塞延时检查:
uint32_t get_tick(void) { return soft_timer_tick; }

void delay_nonblocking(uint32_t ms)
{
    uint32_t start = get_tick();
    while ((get_tick() - start) < ms) {
        // 可执行其他任务
    }
}

优点: 不占用SysTick,可灵活设置优先级;支持长延时。
缺点: 需要额外定时器资源;忙等待仍占用CPU(可改为状态机)。

3.3 方案三:中断安全的忙等待延时(基于循环计数)

对于极短延时且无高精度要求,可使用简单的循环计数,但需根据主频校准。

void delay_loop(uint32_t count)
{
    while (count--) {
        __NOP();
    }
}

注意: 循环次数与编译器优化、主频相关,需实测校准;不推荐用于精确延时。

四、完整代码示例(DWT方案)

#include "stm32f4xx_hal.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) { }
}

void DWT_Delay_ms(uint32_t ms)
{
    while (ms--) {
        DWT_Delay_us(1000);
    }
}

// 在中断中安全使用
void EXTI0_IRQHandler(void)
{
    HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0);
    DWT_Delay_ms(10); // 安全延时10ms
    HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
}

五、注意事项

  • 避免在ISR中调用HAL_Delay:除非确认SysTick优先级高于当前ISR,但仍会延长中断响应时间。
  • 检查SysTick优先级:在HAL_Init()后可通过HAL_NVIC_GetPriority(SysTick_IRQn)查看,默认通常为15(最低)。
  • DWT延时需初始化:确保在main()中调用DWT_Delay_Init(),且系统时钟已正确配置。
  • 非阻塞延时需配合状态机:在RTOS或复杂系统中,建议使用软件定时器或RTOS延时函数。
  • 循环延时受优化影响:使用__NOP()或volatile变量防止编译器优化掉循环。
  • 优先级配置原则:若必须使用HAL_Delay,确保调用它的ISR优先级低于SysTick,但这不是推荐做法。

通过理解HAL_Delay的底层机制并采用合适的替代方案,可以有效避免中断上下文中的卡死问题,提升嵌入式系统的可靠性与实时性。