STM32CubeMX 生成代码中 HAL_Delay 失效的根因剖析与优先级配置规避策略
👁 2 阅读 · 2026-08-27 · 嵌入式
在 STM32 开发中,HAL_Delay 是常用的延时函数,但不少开发者修改 SysTick 中断优先级后,发现延时异常甚至系统卡死。本文深入剖析 HAL_Delay 依赖 SysTick 中断的机制,揭示优先级修改导致失效的根因,并结合 STM32CubeMX 给出三种规避方案:保持默认优先级、使用定时器延时、或调整中断优先级分组。通过原理讲解、配置步骤和完整代码示例,帮助开发者彻底解决这一隐患,提升系统稳定性。
# STM32CubeMX 生成代码中 HAL_Delay 失效的根因剖析与优先级配置规避策略
在 STM32 嵌入式开发中,`HAL_Delay` 是使用频率极高的延时函数,但许多开发者(尤其是从标准库转过来的)在修改 SysTick 中断优先级后,会遇到延时失效、系统卡死等诡异问题。本文将从 HAL 库底层实现出发,剖析根因,并给出基于 STM32CubeMX 的三种规避方案。
## 一、HAL_Delay 的工作原理
HAL_Delay 的实现依赖于 SysTick 定时器及其中断。其核心代码位于 `stm32f1xx_hal.c`(以 F1 为例):
```c
__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 中断服务函数中递增:
```c
void SysTick_Handler(void)
{
HAL_IncTick();
}
```
```c
__weak void HAL_IncTick(void)
{
uwTick += uwTickFreq;
}
```
**关键点**:`HAL_Delay` 是一个忙等待循环,它依赖 SysTick 中断周期性触发来更新 `uwTick`。如果 SysTick 中断被阻塞或无法触发,`uwTick` 停止增长,`while` 循环将永远无法退出,导致延时失效或系统卡死。
## 二、根因:SysTick 中断优先级被修改
在 STM32CubeMX 生成的代码中,默认配置如下(`HAL_Init()` 中):
```c
HAL_InitTick(TICK_INT_PRIORITY); // TICK_INT_PRIORITY 默认为 0(最高优先级)
```
但很多开发者为了其他外设(如 UART、DMA)的实时性,会修改 SysTick 中断优先级,例如:
```c
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` 中添加延时函数:
```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);
}
```
**调用示例**:
```c
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,子优先级任意。
**代码验证**:
```c
// 在 main.c 中手动设置(CubeMX 生成后)
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_2);
HAL_NVIC_SetPriority(SysTick_IRQn, 0, 0);
```
**注意**:修改优先级分组会影响所有中断,需仔细评估。
## 四、完整代码示例(方案二)
以下是一个完整的基于 TIM2 延时的示例(STM32F103C8,CubeMX 生成):
```c
// 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 最高优先级、使用独立定时器或调整优先级分组,可以有效规避。推荐在复杂系统中使用定时器方案,以解耦延时与系统时钟。希望本文能帮助开发者避免这一常见陷阱,写出更健壮的嵌入式代码。