一、HAL_Delay的工作原理与隐患

HAL_Delay是STM32 HAL库中最常用的延时函数,其实现依赖于SysTick定时器中断。默认情况下,HAL_Init()会配置SysTick为1ms中断一次,并在中断服务函数中递减全局变量uwTick。HAL_Delay通过记录起始tick并循环等待tick差值达到目标值来实现延时。

__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_Delay的推进完全依赖SysTick中断。如果SysTick中断因为优先级问题被其他中断抢占或屏蔽,uwTick不会更新,HAL_Delay就会一直卡在while循环中。

二、SysTick优先级陷阱详解

2.1 中断优先级机制回顾

Cortex-M内核使用NVIC管理中断优先级,数值越小优先级越高。SysTick作为内核异常,其优先级通过SCB->SHP[1]寄存器配置。HAL库默认在HAL_InitTick()中设置SysTick优先级为TICK_INT_PRIORITY(通常为0,即最高优先级),但很多开发者会在后续代码中修改,或者使用FreeRTOS等RTOS时被重设。

2.2 陷阱场景

  • 场景A:SysTick优先级被设为较低(如15),而某个高优先级中断(如UART接收中断)长时间执行或频繁触发,导致SysTick中断被延迟响应,HAL_Delay实际延时变长。
  • 场景B:在中断服务函数中调用HAL_Delay。若该中断优先级高于SysTick,则SysTick无法抢占,uwTick停止更新,HAL_Delay永久阻塞,形成死锁。
  • 场景C:使用__disable_irq()关闭全局中断期间调用HAL_Delay,同样导致死锁。

2.3 优先级配置步骤

若必须使用HAL_Delay,应确保SysTick优先级高于所有可能长时间执行的中断。在HAL_Init()之后、外设初始化之前,可手动设置:

HAL_NVIC_SetPriority(SysTick_IRQn, 0, 0); // 最高优先级

或在CubeMX中修改TICK_INT_PRIORITY宏定义。但即便如此,在中断中调用HAL_Delay仍是危险行为,应绝对避免。

三、替代延时方案与实测

3.1 方案一:DWT周期计数器(推荐)

Cortex-M3/M4/M7内核自带DWT(Data Watchpoint and Trace)单元,其CYCCNT寄存器记录内核时钟周期数,不依赖任何中断,精度高且无阻塞风险。

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) { }
}

实测:在STM32F407@168MHz下,DWT_Delay_us(1000)实测误差小于0.1%,且不受中断影响。注意:DWT_Delay_us本身是阻塞的,但不会死锁。

3.2 方案二:硬件定时器延时

使用一个基本定时器(如TIM6)产生微秒级计数,查询计数器实现延时。

void TIM_Delay_Init(void)
{
    __HAL_RCC_TIM6_CLK_ENABLE();
    TIM6->PSC = SystemCoreClock / 1000000 - 1; // 1MHz计数
    TIM6->ARR = 0xFFFF;
    TIM6->CR1 |= TIM_CR1_CEN;
}

void TIM_Delay_us(uint32_t us)
{
    uint32_t start = TIM6->CNT;
    while ((TIM6->CNT - start) < us) { }
}

实测:精度取决于定时器时钟,误差约0.5%,适合对精度要求不高的场景。

3.3 方案三:查询SysTick当前值(无需中断)

直接读取SysTick的VAL寄存器,利用其递减特性实现短延时,不依赖中断。

void SysTick_Delay_us(uint32_t us)
{
    uint32_t ticks = us * (SystemCoreClock / 1000000U);
    uint32_t start = SysTick->VAL;
    uint32_t reload = SysTick->LOAD;
    while (1) {
        uint32_t now = SysTick->VAL;
        uint32_t elapsed = (start > now) ? (start - now) : (reload - now + start);
        if (elapsed >= ticks) break;
    }
}

实测:精度较高,但需注意SysTick可能被RTOS占用,且延时不能超过一个重装载周期(通常1ms)。

四、注意事项与总结

  • 绝对禁止在中断中调用HAL_Delay,尤其是高优先级中断。
  • 若使用FreeRTOS,HAL_Delay通常被重定义为osDelay,此时SysTick由RTOS管理,优先级配置需遵循RTOS规则。
  • DWT方案需确保内核调试组件时钟开启,部分低功耗模式下DWT可能停止。
  • 硬件定时器方案会占用一个定时器资源,需合理规划。
  • 查询SysTick方案在SysTick被重装载时可能产生微小误差,适合短延时。

总结:HAL_Delay的SysTick优先级陷阱本质是中断依赖问题。在中断上下文或对时序要求严格的场景,应改用DWT或硬件定时器延时。实测表明DWT方案精度最高、最安全,推荐作为HAL_Delay的替代。