STM32CubeMX 生成代码中 HAL_Delay 失效的根因剖析与优先级配置规避策略

在 STM32 嵌入式开发中,HAL_Delay 是使用频率极高的延时函数,但许多开发者(尤其是从标准库转过来的)在修改 SysTick 中断优先级后,会遇到延时失效、系统卡死等诡异问题。本文将从 HAL 库底层实现出发,剖析根因,并给出基于 STM32CubeMX 的三种规避方案。

一、HAL_Delay 的工作原理

HAL_Delay 的实现依赖于 SysTick 定时器及其中断。其核心代码位于 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)
  {
  }
}

HAL_GetTick() 返回全局变量 uwTick,该变量在 SysTick 中断服务函数中递增:

void SysTick_Handler(void)
{
  HAL_IncTick();
}
__weak void HAL_IncTick(void)
{
  uwTick += uwTickFreq;
}

关键点HAL_Delay 是一个忙等待循环,它依赖 SysTick 中断周期性触发来更新 uwTick。如果 SysTick 中断被阻塞或无法触发,uwTick 停止增长,while 循环将永远无法退出,导致延时失效或系统卡死。

二、根因:SysTick 中断优先级被修改

在 STM32CubeMX 生成的代码中,默认配置如下(HAL_Init() 中):

HAL_InitTick(TICK_INT_PRIORITY);  // TICK_INT_PRIORITY 默认为 0(最高优先级)

但很多开发者为了其他外设(如 UART、DMA)的实时性,会修改 SysTick 中断优先级,例如:

HAL_NVIC_SetPriority(SysTick_IRQn, 3, 0);  // 降低 SysTick 优先级

问题场景

  • 如果系统中存在更高优先级的中断(如外部中断、定时器中断)且其 ISR 中调用了 HAL_Delay,那么高优先级中断会抢占 SysTick 中断,导致 uwTick 更新延迟。
  • 更严重的是,如果高优先级中断的 ISR 执行时间过长,或者嵌套中断导致 SysTick 中断被无限期阻塞,HAL_Delay 将永远无法完成。
  • 此外,如果修改了中断优先级分组(如从 NVIC_PRIORITYGROUP_4 改为 NVIC_PRIORITYGROUP_2),SysTick 的抢占优先级和子优先级组合可能意外改变,导致其被其他中断抢占。

根因总结HAL_Delay 的忙等待机制要求 SysTick 中断必须定期触发,任何导致 SysTick 中断延迟或阻塞的因素都会使其失效。修改 SysTick 优先级本身不是错误,但若未考虑中断嵌套和阻塞场景,就会引发问题。

三、规避方案与 STM32CubeMX 配置

方案一:保持 SysTick 中断优先级为最高(推荐)

在 STM32CubeMX 中,默认生成的 HAL_InitTick 使用最高优先级(0),这是最安全的配置。如果必须降低优先级,请确保:

  • 所有可能调用 HAL_Delay 的中断优先级都低于 SysTick。
  • 高优先级中断的 ISR 中不要调用 HAL_Delay

配置步骤

  1. 打开 STM32CubeMX,在 System Core > NVIC 中,找到 SysTick_IRQn,保持其抢占优先级为 0(最高)。
  2. 其他外设中断优先级设置为 1 或更低。
  3. 生成代码后,不要手动修改 HAL_InitTick 中的优先级参数。

方案二:使用定时器实现延时(彻底解耦)

如果系统需要灵活的中断优先级,建议使用通用定时器(如 TIM2)实现独立延时,不依赖 SysTick。

配置步骤

  1. 在 STM32CubeMX 中启用一个定时器(如 TIM2),设置时钟源为内部时钟,预分频器和自动重载值以产生 1ms 中断。
  2. NVIC 中使能 TIM2 中断,并设置合适的优先级(可高于或低于 SysTick)。
  3. 生成代码后,在 tim.c 中添加延时函数:
// 全局变量
volatile uint32_t timer_tick = 0;

// 在 TIM2 中断回调中递增
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM2) {
        timer_tick++;
    }
}

// 自定义延时函数
void TIM2_Delay(uint32_t ms)
{
    uint32_t start = timer_tick;
    while ((timer_tick - start) < ms);
}

调用示例

HAL_TIM_Base_Start_IT(&htim2);  // 启动定时器中断
TIM2_Delay(1000);  // 延时 1 秒

优点:完全独立于 SysTick,优先级配置自由;缺点:占用一个定时器资源。

方案三:调整中断优先级分组,确保 SysTick 不被抢占

如果必须使用 SysTick 且需要较低优先级,可以通过调整 NVIC 优先级分组来保证 SysTick 的抢占优先级仍然最高。

配置步骤

  1. 在 STM32CubeMX 的 System Core > NVIC 中,设置优先级分组为 NVIC_PRIORITYGROUP_2(2 位抢占优先级 + 2 位子优先级)。
  2. 将 SysTick 的抢占优先级设为 0,子优先级设为 0(最高)。
  3. 其他中断的抢占优先级设为 1-3,子优先级任意。

代码验证

// 在 main.c 中手动设置(CubeMX 生成后)
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_2);
HAL_NVIC_SetPriority(SysTick_IRQn, 0, 0);

注意:修改优先级分组会影响所有中断,需仔细评估。

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

以下是一个完整的基于 TIM2 延时的示例(STM32F103C8,CubeMX 生成):

// main.c
#include "main.h"
#include "tim.h"

volatile uint32_t timer_tick = 0;

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM2) {
        timer_tick++;
    }
}

void TIM2_Delay(uint32_t ms)
{
    uint32_t start = timer_tick;
    while ((timer_tick - start) < ms);
}

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_TIM2_Init();

    HAL_TIM_Base_Start_IT(&htim2);

    while (1)
    {
        HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
        TIM2_Delay(500);  // 500ms 延时
    }
}

注意HAL_TIM_PeriodElapsedCallback 是弱函数,需要在用户代码中重定义,且确保 htim2 实例正确。

五、注意事项

  • 不要在高优先级中断中调用 HAL_Delay:即使 SysTick 优先级最高,高优先级中断也会阻塞它,导致死循环。
  • 检查中断优先级分组:修改分组后,所有中断的优先级含义会变化,需重新评估。
  • 使用调试器观察:如果遇到延时异常,可在 SysTick_Handler 中设置断点,检查是否被触发。
  • 考虑使用 HAL_Delay 的替代:如 HAL_GetTick() 配合非阻塞延时(状态机)。

六、总结

HAL_Delay 失效的根因在于 SysTick 中断被阻塞或延迟,导致 uwTick 无法更新。通过保持 SysTick 最高优先级、使用独立定时器或调整优先级分组,可以有效规避。推荐在复杂系统中使用定时器方案,以解耦延时与系统时钟。希望本文能帮助开发者避免这一常见陷阱,写出更健壮的嵌入式代码。