引言
在 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 库版本不同略有差异):
__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 失效时,可以按照以下步骤进行排查:
-
确认 SysTick 配置:在
HAL_Init()后,检查SysTick的优先级设置。在HAL_InitTick()中,默认设置为最低优先级(NVIC_EncodePriority计算)。如果被修改过,请记录当前值。 -
检查中断优先级分组:确认
NVIC_PriorityGroupConfig的设置,确保 SysTick 优先级数值在有效范围内。 -
检查是否有中断长时间占用 CPU:在调试器中暂停程序,查看当前 PC 指针位置。如果停留在
HAL_Delay的 while 循环中,说明uwTick未更新。此时,查看SysTick->CTRL和SysTick->LOAD寄存器,确认 SysTick 是否使能。 -
检查 SysTick 中断是否被屏蔽:在
HAL_Delay卡住时,查看NVIC->ISPR寄存器,看 SysTick 中断是否处于挂起状态。如果挂起但未响应,说明被更高优先级中断阻塞。 -
检查中断服务函数:确认
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 寄存器:
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 正常工作:
// 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 优先级,或改用其他时基源,可以有效避免这一问题。在嵌入式开发中,中断优先级的设置需谨慎,遵循“中断服务函数尽量短小”的原则,才能保证系统稳定可靠。希望本文能帮助你在遇到类似问题时快速定位并解决。