STM32CubeMX 生成代码中 HAL_Delay 失效排查:当 SysTick 中断优先级被改动之后
👁 1 阅读 · 2026-08-27 · 嵌入式
在 STM32 开发中,HAL_Delay 是常用的毫秒延时函数,其实现依赖 SysTick 中断。然而,当开发者为了满足实时性需求而调整中断优先级时,可能会意外导致 HAL_Delay 失效,表现为程序卡死或延时异常。本文深入分析 HAL_Delay 的工作原理,揭示 SysTick 中断优先级与 HAL_Delay 失效之间的因果关系,并给出完整的排查步骤、代码示例及注意事项,帮助开发者快速定位并解决问题。
# 引言
在 STM32 嵌入式开发中,`HAL_Delay` 是 HAL 库提供的标准毫秒延时函数,广泛用于时序控制、按键消抖等场景。然而,很多开发者在使用 RTOS 或自定义中断优先级时,会修改 SysTick 的中断优先级,这往往导致 `HAL_Delay` 意外失效,程序卡死在延时处。本文将从原理出发,剖析这一问题的根源,并提供系统的排查与解决方案。
# HAL_Delay 的工作原理
`HAL_Delay` 的实现依赖于 SysTick 定时器及其中断。在 STM32CubeMX 生成的代码中,`HAL_Init()` 会调用 `HAL_InitTick()`,该函数配置 SysTick 产生 1ms 周期的中断,并在中断服务函数 `SysTick_Handler()` 中调用 `HAL_IncTick()` 递增全局变量 `uwTick`。
`HAL_Delay` 的典型实现如下(HAL 库版本不同略有差异):
```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_Delay` 是一个忙等待循环,它不断读取 `uwTick` 的值,直到达到目标延时。而 `uwTick` 的更新完全依赖 SysTick 中断。因此,如果 SysTick 中断无法正常触发或被阻塞,`HAL_Delay` 将永远无法满足退出条件,导致程序卡死。
# 问题场景:SysTick 中断优先级被改动
在裸机开发中,SysTick 中断优先级默认设置为最低(数值最大),以确保其他中断可以抢占它。但在某些情况下,开发者可能会为了提升系统实时性,将 SysTick 中断优先级调高(数值减小),或者在使用 RTOS 时,RTOS 会接管 SysTick 并重新设置其优先级。
例如,在 CubeMX 中,如果启用了 FreeRTOS,SysTick 会被用作 RTOS 的时基,而 HAL 的时基则改用其他定时器(如 TIM6)。此时,`HAL_Delay` 不再依赖 SysTick,而是依赖 TIM6,因此问题不会出现。但在裸机环境中,若手动修改 SysTick 优先级,就可能引发问题。
# 失效原因分析
`HAL_Delay` 失效的根本原因在于:**SysTick 中断被更高优先级的中断阻塞,导致 `uwTick` 无法及时更新**。具体来说,当 SysTick 中断优先级被设置为高于某个外设中断时,如果该外设中断频繁触发且处理时间较长,SysTick 中断就会一直被挂起,无法执行 `HAL_IncTick()`,从而 `uwTick` 停止增长。
此外,还有一个更隐蔽的情况:如果 SysTick 中断优先级被设置为 **可屏蔽中断(如 PendSV 或 SVC)** 的优先级之上,并且在中断服务函数中调用了 `HAL_Delay`,则会导致死锁。例如,在某个高优先级中断中调用 `HAL_Delay`,而该中断的优先级高于 SysTick,那么 SysTick 中断无法打断该中断,`uwTick` 不更新,`HAL_Delay` 永远等待。
# 排查步骤
当遇到 `HAL_Delay` 失效时,可以按照以下步骤进行排查:
1. **确认 SysTick 配置**:在 `HAL_Init()` 后,检查 `SysTick` 的优先级设置。在 `HAL_InitTick()` 中,默认设置为最低优先级(`NVIC_EncodePriority` 计算)。如果被修改过,请记录当前值。
2. **检查中断优先级分组**:确认 `NVIC_PriorityGroupConfig` 的设置,确保 SysTick 优先级数值在有效范围内。
3. **检查是否有中断长时间占用 CPU**:在调试器中暂停程序,查看当前 PC 指针位置。如果停留在 `HAL_Delay` 的 while 循环中,说明 `uwTick` 未更新。此时,查看 `SysTick->CTRL` 和 `SysTick->LOAD` 寄存器,确认 SysTick 是否使能。
4. **检查 SysTick 中断是否被屏蔽**:在 `HAL_Delay` 卡住时,查看 `NVIC->ISPR` 寄存器,看 SysTick 中断是否处于挂起状态。如果挂起但未响应,说明被更高优先级中断阻塞。
5. **检查中断服务函数**:确认 `SysTick_Handler()` 是否被正确实现,并且没有被其他函数覆盖。
# 解决方案
根据不同的应用场景,有以下几种解决方案:
## 方案一:保持 SysTick 优先级为最低
在裸机开发中,最简单的方法是保持 SysTick 中断优先级为最低(数值最大)。这样可以确保任何外设中断都能抢占 SysTick,避免阻塞。但要注意,如果某个中断处理时间过长,仍然会间接影响 `HAL_Delay` 的精度,但不会导致完全失效。
## 方案二:避免在中断中调用 HAL_Delay
中断服务函数应尽量短小,避免调用 `HAL_Delay` 等阻塞函数。如果确实需要延时,可以使用状态机或定时器替代。
## 方案三:使用其他时基源
如果必须修改 SysTick 优先级(例如为了 RTOS),可以将 HAL 的时基切换到其他定时器。在 CubeMX 中,可以配置 TIM6 或 TIM7 作为 HAL 时基,这样 `HAL_Delay` 将依赖该定时器,与 SysTick 无关。
配置方法:在 CubeMX 的 `SYS` 选项卡中,将 `Timebase Source` 从 `SysTick` 改为 `TIM6`。生成代码后,`HAL_InitTick()` 会初始化 TIM6,而 SysTick 留给 RTOS 使用。
## 方案四:使用 DWT 或循环计数实现精确延时
如果不想依赖中断,可以使用 DWT(Data Watchpoint and Trace)单元或直接读取 `SysTick->VAL` 寄存器实现忙等待延时。例如,使用 DWT 的 CYCCNT 寄存器:
```c
void DWT_Delay_us(uint32_t us)
{
if (!(DWT->CTRL & DWT_CTRL_CYCCNTENA_Msk))
{
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CYCCNT = 0;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
}
uint32_t start = DWT->CYCCNT;
uint32_t ticks = us * (SystemCoreClock / 1000000);
while ((DWT->CYCCNT - start) < ticks);
}
```
这种方法不依赖中断,但会占用 CPU,且需要根据主频计算。
# 完整代码示例
以下是一个完整的示例,演示如何将 HAL 时基切换到 TIM6,并验证 `HAL_Delay` 正常工作:
```c
// main.c (CubeMX 生成后修改)
#include "main.h"
TIM_HandleTypeDef htim6;
void HAL_InitTick(uint32_t TickPriority)
{
// 重定向到 TIM6
htim6.Instance = TIM6;
htim6.Init.Prescaler = (SystemCoreClock / 1000000) - 1; // 1MHz 计数
htim6.Init.Period = 999; // 1ms 中断
if (HAL_TIM_Base_Init(&htim6) != HAL_OK)
{
Error_Handler();
}
HAL_NVIC_SetPriority(TIM6_DAC_IRQn, TickPriority, 0);
HAL_NVIC_EnableIRQ(TIM6_DAC_IRQn);
}
void HAL_IncTick(void)
{
uwTick++;
}
void TIM6_DAC_IRQHandler(void)
{
HAL_TIM_IRQHandler(&htim6);
}
int main(void)
{
HAL_Init();
// 注意:HAL_Init() 会调用 HAL_InitTick(),但这里我们重写了它
// 因此 SysTick 不再用于 HAL 时基
// 可以自由修改 SysTick 优先级,不影响 HAL_Delay
while (1)
{
HAL_Delay(1000);
// 每秒翻转 LED
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
}
}
```
注意:此示例需要手动实现 `HAL_InitTick` 和 `HAL_IncTick`,并确保 TIM6 中断服务函数正确。在 CubeMX 中,更推荐直接配置 Timebase Source 为 TIM6,这样代码自动生成。
# 注意事项
- **不要随意修改 SysTick 优先级**:除非明确知道后果,否则保持默认最低优先级。
- **中断中禁止调用 HAL_Delay**:即使 SysTick 优先级最高,也可能因嵌套中断导致问题。
- **RTOS 环境下使用专用延时**:在 FreeRTOS 中,应使用 `vTaskDelay` 代替 `HAL_Delay`,因为 RTOS 的调度器依赖 SysTick。
- **调试技巧**:使用调试器查看 `uwTick` 的值,如果长时间不变,则说明 SysTick 中断未执行。
- **优先级分组**:确保所有中断使用相同的优先级分组,否则优先级比较可能出错。
# 总结
`HAL_Delay` 失效通常是由于 SysTick 中断被阻塞或优先级设置不当所致。通过理解其工作原理,合理配置 SysTick 优先级,或改用其他时基源,可以有效避免这一问题。在嵌入式开发中,中断优先级的设置需谨慎,遵循“中断服务函数尽量短小”的原则,才能保证系统稳定可靠。希望本文能帮助你在遇到类似问题时快速定位并解决。