尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

STM32 HAL_Delay卡死原因排查:uwTick为0与SysTick中断优先级解析

STM32 HAL_Delay卡死原因排查:uwTick为0与SysTick中断优先级解析 这问题我有发言权。早前帮一个项目调 STM32H7B3I-EVAL 和 STM32H573I-DK 之间的 UART 通信程序跑到HAL_Delay()就卡死打开调试器一看uwTick纹丝不动地躺在 0 上。一开始我也以为是延时函数本身的问题搞了半天才发现坑埋在更深的地方。如果你也遇到HAL_Delay()无限阻塞或者uwTick始终为 0这篇排查记录应该能帮你省下好几个小时。{% note warning %} 先说结论HAL_Delay()卡死的根因基本不是HAL_Delay()本身而是SysTick中断没有按预期工作或者在中断上下文里让SysTick被更高的优先等级压住了。这类问题在串口通信场景里尤其容易踩中因为HAL_UART_Transmit/HAL_UART_Receive的阻塞超时也依赖同一个uwTick。 {% endnote %}1. 现象描述程序像被点了暂停键我这里复现的硬件环境很简单STM32H7B3I-EVAL板载 STM32H7B3LI 芯片作为发送端STM32H573I-DK板载 STM32H573I 芯片作为接收端两边用 UART 直连波特率 1152008N1 格式。发送端的代码逻辑大概是发送前先调用HAL_Delay(10)做节奏控制接收端收到数据后调用HAL_Delay(5)处理一下业务。结果就是程序在第一个HAL_Delay()调用处就再也没有返回断点停在while循环里HAL_GetTick()永远返回 0uwTick也一直是 0。1.1 先别急着怀疑 UART 配置遇到这种问题第一反应往往是查串口配置波特率、引脚复用、时钟使能挨个看一遍发现都没问题。这时候更要冷静因为问题很可能不在 UART 本身而在“时间基准”。我当时在HAL_Delay的while循环里打了一个断点单步执行几次发现HAL_GetTick()返回值没有变化。这个现象说明uwTick没有被更新也就是SysTick_Handler要么没被调用要么HAL_IncTick()没有执行。1.2 先分清两种“卡死”卡死在HAL_UART_Transmit内部的等待标志位循环里此时uwTick可能正常递增只是发送没有完成。卡死在HAL_Delay的while循环里且uwTick不变这种情况几乎可以锁定是时间基准出了问题。这两种卡死虽然表现相似但排查方向完全不同。我的案例属于第二种所以我把重心放到了SysTick和中断优先级上。2. HAL_Delay 和 uwTick 到底是怎么协作的在 HAL 库里HAL_Delay并不是凭空计时它依赖一个毫秒计数器uwTick。这个计数器默认由SysTick中断负责累加而HAL_Delay只是不断读取这个计数器的值判断是否到时。__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的中断没有触发uwTick就会一直保持不变HAL_Delay就会永远在while里死转。2.1 SysTick 和 HAL 的关系HAL_Init()内部会调用HAL_InitTick()把SysTick配置成 1ms 中断一次并注册中断服务函数。正常情况下SysTick_Handler会被定义为void SysTick_Handler(void) { HAL_IncTick(); }每次中断把uwTick加 1。看起来很简单但有一个关键点常被忽略SysTick的中断优先级由HAL_InitTick设置默认是TICK_INT_PRIORITY这个值在 STM32H7 系列上通常在 CubeMX 生成的代码里被定义为0x0F也就是系统里比较低的优先级。2.2 为什么 uwTick 为 0 会引发连锁问题uwTick不只是给HAL_Delay用很多 HAL 阻塞函数的超时判断也靠它。比如HAL_UART_Transmit的超时参数Timeout实际是HAL_GetTick()计算的。一旦uwTick停住这些超时也全部失效程序就可能永久阻塞在等待标志位的循环里。更麻烦的是这种问题有时候不会马上暴露。如果你在初始化阶段调用HAL_Delay恰好SysTick还没配置好可能一开机就卡死如果系统跑了几分钟后才卡那就更倾向于优先级抢占或者中断服务函数被覆盖。2.3 优先级是绕不开的坎Cortex-M 内核的中断优先级是数值越小优先级越高。SysTick的优先级如果比其他中断低它就不能打断正在执行的其他中断。假设你在 UART 接收中断里调用HAL_Delay而SysTick优先级比 UART 中断低那么SysTick中断无法抢占当前 UART 中断uwTick就不会增加HAL_Delay就在中断里死等。这个场景非常经典也是 UART 通信里最常见的 HAL_Delay 卡死原因之一。很多新人在 UART 回调函数里加延时一加就出问题。3. 逐层排查为什么 uwTick 始终是 0当时为了让发送端节奏稳定我在发送循环里用了HAL_Delay代码结构大概是这样的while (1) { uint8_t data 0xAA; HAL_UART_Transmit(huart1, data, 1, 1000); HAL_Delay(10); }发现卡死之后我先做了几件常规事情按顺序排除这里分享给同样遇到问题的朋友。3.1 第一步检查 SysTick_Handler 是否存在打开工程确认启动文件或者中断向量表里有没有SysTick_Handler。如果你用的是 CubeMX 生成的标准工程这个函数一般会在stm32h7xx_it.c里。但要小心如果你手动添加过同名的函数或者把SysTick_Handler写进了别的文件两个符号可能冲突导致实际生效的是空实现。确认SysTick_Handler里的HAL_IncTick()有没有被注释掉。我曾经接手过一个工程上一任工程师为了调试方便在SysTick_Handler里加了喂狗操作结果把HAL_IncTick()给挡住了代码编译没问题运行到延时函数就卡死。3.2 第二步确认 HAL_Init 和 SystemClock_Config 执行顺序HAL_Init()必须在任何 HAL 外设初始化之前执行否则HAL_InitTick没有运行uwTick自然永远为 0。虽然 CubeMX 生成的main()里默认是先调HAL_Init()再调SystemClock_Config()但如果你手动修改过代码顺序可能会导致时钟配置覆盖了SysTick设置。我遇到过一次比较隐蔽的情况SystemClock_Config()里重新配置了SysTick的时钟源导致SysTick不再以 1ms 为周期中断uwTick增长异常缓慢看起来就接近卡死。如果SysTick_CTRL的 CLKSOURCE 位被设置成外部参考时钟而外部时钟并未正确提供也会让中断频率异常。3.3 第三步用调试器看寄存器现场用 ST-LINK 连接调试器暂停程序查看当前 PC 指针停在哪个函数。如果停在HAL_Delay的while循环再查看SysTick-CTRL的值确认 ENABLE 位是否为 1。SysTick-LOAD和SysTick-VAL确认定时器是否在走。uwTick的地址和值。SysTick_Handler是否被编译链接进固件。也可以在调试器里手动给uwTick赋值比如改成 10000再看HAL_Delay是否立即返回。如果立刻返回说明卡死原因就是uwTick不更新。这个操作很粗暴但能快速确认方向。3.4 第四步在 UART 通信代码中打点如果你怀疑是 UART 中断把SysTick压住了可以在HAL_UART_RxCpltCallback或HAL_UART_TxCpltCallback里加一个 GPIO 翻转用来观察 UART 中断的实际频率和持续时间。若中断里长时间运行代码SysTick可能长时间得不到执行。还可以在HAL_UART_Transmit调用之前和之后分别抓取uwTick的值看看发送过程中uwTick变化是否正常。如果发送期间uwTick卡住说明问题出在串口中断或 DMA 中断的优先级配置上。4. 修复方案从根上解决而不是绕开在我这次案例里最终确认的原因是接收端在 UART 接收中断回调函数里调用了HAL_Delay(5)而SysTick优先级配置得比 UART 中断低导致中断嵌套时SysTick无法抢占uwTick停止更新继而卡死。发送端其实也受到波及因为两个板子通信时接收端不处理数据发送端发送超时整个业务链路被迫停滞。4.1 直接修复调整优先级和初始化顺序如果确实需要在中断里调用HAL_Delay最简单的方法是把SysTick优先级提高至少比所有会调用延时或阻塞函数的其他中断优先级高。在 CubeMX 里可以在NVIC配置中把SysTick的抢占优先级设置成比 UART 中断更小的数值。如果没有特殊需求我更推荐的做法是不要在中断里调用HAL_Delay。中断服务函数应该尽量短把耗时操作放到主循环或者任务中。你完全可以用一个标志位在回调里置位主循环检测到标志位后再做延时处理。4.2 给 HAL_Delay 加一个保护壳如果你无法避免在中断里使用延时可以考虑自己实现一个基于 DWT 或普通定时器的延时函数不依赖SysTick。比如用TIM做微秒级延时或者用DWT-CYCCNT做高精度延时。但这里有个前提不同外设的优先级和中断行为不一样盲改可能会导致其他问题。最好的办法还是重构代码逻辑让中断函数里的时间开销可控。4.3 UART 阻塞发送超时的处理HAL_UART_Transmit的最后一个参数是超时时间内部实现也依赖uwTick。如果uwTick不更新传多大的超时都没用。所以先确保uwTick正常工作再讨论超时参数。另外阻塞发送期间如果又发生接收中断需要合理配置 NVIC 抢占关系避免 UART 中断互锁。我建议在做双板通信时发送和接收尽量使用分开的串口外设和分开的中断服务函数这样可以降低相互干扰的复杂度。如果只有一个串口就要仔细想清楚发送、接收、错误中断的优先级。4.4 双板通信的最终代码骨架发送端STM32H7B3I-EVAL简化代码int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_USART1_UART_Init(); uint8_t txData[4] {0x01, 0x02, 0x03, 0x04}; while (1) { HAL_UART_Transmit(huart1, txData, sizeof(txData), 100); HAL_Delay(100); } }接收端STM32H573I-DK简化代码uint8_t rxData[4]; volatile uint8_t rxComplete 0; int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_USART1_UART_Init(); HAL_UART_Receive_IT(huart1, rxData, sizeof(rxData)); while (1) { if (rxComplete) { rxComplete 0; HAL_Delay(10); HAL_UART_Receive_IT(huart1, rxData, sizeof(rxData)); } } } void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { rxComplete 1; } }这样HAL_Delay全部放在主循环中断里只做标志位置位可以最大化避免优先级死锁。5. 常见问题与排查经验速查下面这个表是我在实际调试中总结出来的常见原因你可以对照排查症状可能原因快速验证方法uwTick一直为 0SysTick_Handler未执行或HAL_IncTick()被屏蔽在SysTick_Handler打断点看是否进入uwTick有值但不增长SysTick时钟源配置异常检查SysTick-CTRL和CLKSOURCE某个中断发生后卡在HAL_DelaySysTick优先级低于当前中断查看 NVIC 优先级临时调高SysTick优先级HAL_UART_Transmit报超时uwTick停止或波特率/引脚异常单独测试串口回环去掉HAL_Delay两个板子通信时一方卡死接收中断处理过长主循环无法及时喂HAL_Delay用 GPIO 翻转测量中断耗时初始化阶段卡死HAL_Init没有最先调用查看main()初始化顺序开启调试优化后卡死编译器优化了相关变量未加 volatile把uwTick相关变量声明为 volatile5.1 我在现场踩过的具体坑第一个坑我一度以为是 STM32H573I-DK 的电源问题因为只在接收板卡上出现卡死。后来把接收板卡单独跑一个 LED 闪烁程序发现一切正常才把注意力转回通信代码。第二个坑我试图用HAL_UART_Transmit的返回值判断卡死原因但发送端卡住时返回值一直是HAL_BUSY。这是因为发送队列里已经有未完成的数据但uwTick停住导致状态机无法跳转。HAL_BUSY这个返回值会误导人让你误以为是串口故障。第三个坑修改SysTick优先级时我把优先级组NVIC_PRIORITYGROUP_4改成了NVIC_PRIORITYGROUP_2结果所有中断的抢占关系都变了系统时序完全乱掉。改优先级时务必先确认优先级分组不要轻易动全局配置。5.2 如何避免下一次踩坑在双板 UART 通信项目里我建议从一开始就建立一套自检机制一是开启一个调试串口专门打印关键变量的值比如uwTick、发送状态、接收状态。不需要 OLED 屏幕一个串口加一个 USB 转 TTL 就很够了。二是用 LED 指示主循环是否在运行。主循环每跑一圈翻转一次 LED如果 LED 不闪说明程序卡在某处能缩小范围。三是在启用中断之前先把基础外设跑通。比如先只跑HAL_Delay的点灯程序确认时间基准正常再叠加 UART 发送最后再叠加接收中断。每加一层功能就测试一次能最大程度避免一次性引入多个问题。四是用volatile修饰在中断和主循环之间共享的变量。uwTick在 HAL 库里已经是全局变量内部有自己的处理但如果自己设计标志位一定要记得加volatile否则优化器可能会把变量缓存到寄存器里导致主循环看不到中断里的修改。6. 从 UART 双板联调到系统级稳定性这次问题的表面是HAL_Delay卡死深挖下去其实暴露的是整个系统的中断优先级设计和时序设计问题。单独一个板卡上SysTick优先级低可能没什么感觉因为不会频繁发生长时间中断但一旦两板通信UART 中断、DMA 中断、可能的定时器中断全叠加在一起优先级设计不合理就会变成定时炸弹。如果你还想让系统更稳定可以考虑把短延时功能从SysTick迁移到其他硬件定时器。比如用 TIM6 做基本定时器配合HAL_TIM_Base_Start_IT实现属于自己的uwTick这样可以完全避免和 UART 中断产生优先级冲突。不过这样做的代价是你需要自己维护延时接口工作量会大一些。我个人的习惯是除非项目里对时序有极高要求否则不会主动更换时间基准。保持SysTick作为 HAL 库的默认时间基准是最省心的选择但与此同时我会严格遵守“中断里不调用耗时函数”的原则把所有业务逻辑都搬到主循环或 RTOS 任务里。说到底HAL_Delay卡死不是玄学uwTick不递增一定有原因。按从简单到复杂的顺序排查最快找到根因。这次双板联调让我印象最深的一点是问题往往不出在报错的那一行代码而是出在它背后的中断时序和优先级设计上。希望这篇记录能帮到被同样问题折磨的你。
返回列表