# 引言 在 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 优先级,或改用其他时基源,可以有效避免这一问题。在嵌入式开发中,中断优先级的设置需谨慎,遵循“中断服务函数尽量短小”的原则,才能保证系统稳定可靠。希望本文能帮助你在遇到类似问题时快速定位并解决。