一、问题引入:中断里的HAL_Delay为何会“死等”

在STM32开发中,HAL_Delay() 是常用的毫秒级延时函数。但许多开发者发现,在中断服务函数(ISR)中调用 HAL_Delay() 会导致程序卡死。例如:

void EXTI0_IRQHandler(void) {
    HAL_Delay(10);  // 程序卡死在这里!
    HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
}

现象是程序不再响应任何中断,甚至看门狗复位。这并非HAL库的bug,而是其设计机制与中断上下文冲突的必然结果。

二、原理剖析:HAL_Delay的依赖链

2.1 HAL_Delay的实现

查看HAL库源码(以STM32F4为例):

__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++
}

2.2 中断上下文中的死锁逻辑

  • SysTick中断优先级:默认情况下,SysTick中断优先级较低(通常为最低)。
  • 中断嵌套规则:当CPU正在执行某个中断服务函数(如EXTI0_IRQHandler)时,只有优先级高于当前中断的中断才能抢占。
  • 死等发生:若在EXTI0中断中调用 HAL_Delay(),此时CPU停留在EXTI0的ISR中,SysTick中断无法抢占(因为其优先级不高于EXTI0),导致 uwTick 不再递增。HAL_Delay() 中的 while 循环永远等不到 HAL_GetTick() 变化,形成死等。

结论:HAL_Delay() 依赖SysTick中断更新时基,而中断上下文屏蔽了同级或低优先级中断,因此不可在ISR中使用。

三、解决方案:基于DWT的微秒级延时

3.1 DWT简介

DWT(Data Watchpoint and Trace)是ARM Cortex-M内核的调试组件,其中 CYCCNT 寄存器是一个32位自由运行的周期计数器,每个内核时钟周期递增一次。通过读取 CYCCNT 的差值,可实现高精度延时,且不依赖任何中断。

3.2 配置步骤

  1. 使能DWT:设置 CoreDebug->DEMCR 的 TRCENA 位。
  2. 清零CYCCNT:写 DWT->CYCCNT = 0。
  3. 使能CYCCNT:设置 DWT->CTRL 的 CYCCNTENA 位。
  4. 计算延时周期数:延时微秒数 × (系统时钟频率 / 1,000,000)。

3.3 完整代码示例

#include "stm32f4xx_hal.h"

// 初始化DWT周期计数器
void DWT_Init(void) {
    CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; // 使能跟踪
    DWT->CYCCNT = 0;                                // 清零计数器
    DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;            // 使能周期计数
}

// 微秒级延时(阻塞式,不依赖中断)
void delay_us(uint32_t us) {
    uint32_t start = DWT->CYCCNT;
    uint32_t cycles = us * (SystemCoreClock / 1000000U);
    while ((DWT->CYCCNT - start) < cycles) {
        __NOP(); // 防止编译器优化
    }
}

// 毫秒级延时(基于delay_us)
void delay_ms(uint32_t ms) {
    while (ms--) {
        delay_us(1000);
    }
}

使用示例:

void EXTI0_IRQHandler(void) {
    delay_us(500);  // 安全延时500微秒
    HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
    HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0);
}

四、注意事项

  • DWT仅Cortex-M3/M4/M7等内核支持,M0/M0+无DWT,需用其他方案(如定时器)。
  • 系统时钟变化时需重新计算:若动态调整主频,SystemCoreClock 变量需同步更新,否则延时不准。
  • 计数器溢出:CYCCNT 为32位,在168MHz下约25.5秒溢出一次。单次延时建议不超过1秒,否则需处理溢出(使用无符号减法可自然处理单次溢出)。
  • 中断优先级配置:若必须使用 HAL_Delay(),可临时提升SysTick优先级至最高,但会破坏系统时基,不推荐。
  • 低功耗模式:进入睡眠/停止模式后DWT可能停止计数,唤醒后需重新初始化。
  • 编译器优化:循环中加 __NOP() 或使用 volatile 防止优化掉空循环。

五、总结

HAL_Delay() 在中断上下文中死等的根本原因是其依赖SysTick中断更新时基,而中断嵌套规则阻止了SysTick的抢占。基于DWT周期计数器的延时方案完全独立于中断,精度可达微秒级,是中断服务程序中延时的理想选择。掌握此技巧,可显著提升嵌入式系统的实时性与可靠性。