ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

STM32调试卡死HAL_Delay?SysTick暂停是元凶

STM32调试卡死HAL_Delay?SysTick暂停是元凶 1. 说下现象一进debug就卡死在HAL_Delay最近在调一块STM32H745XI的双核板子M7核做主控Keil MDK ST-Link调试。程序编译、下载、全速运行都正常但只要一进Debug模式程序跑进HAL_Init()之后就再也不动了。看堆栈它停在HAL_Delay()里面卡在一个while循环中单步也跳不出去连“暂停”再“运行”都没用只能复位或者重启调试器。最诡异的是只要退出调试模式、直接全速跑启动流程就完全正常。这个现象在H7系列、甚至其他Cortex-M内核的HAL工程里都非常典型。很多人在STM32H745XI、STM32H743、STM32F407上都会遇到只不过H7的双核架构会让问题看起来更迷惑。今天我把这个坑从现象、源码、排查到解决完整拆一遍希望能帮到卡在这个问题上的人。先说结论方向这不是芯片坏了也不是HAL库有bug绝大多数情况是Cortex-M内核的SysTick在调试器暂停期间停止计数导致的外加一点调试器和时钟配置的“助攻”。后面我会一步步把这句话拆开讲清楚。2. 先把HAL_Init和HAL_Delay的代码翻出来看2.1 HAL_Init到底做了什么要搞清楚为什么卡死先看它调用了什么。HAL_Init()的源码不长核心逻辑大约是这样HAL_StatusTypeDef HAL_Init(void) { /* 使能Flash预取、指令缓存、数据缓存 */ #if (INSTRUCTION_CACHE_ENABLE ! 0U) __HAL_FLASH_INSTRUCTION_CACHE_ENABLE(); #endif #if (DATA_CACHE_ENABLE ! 0U) __HAL_FLASH_DATA_CACHE_ENABLE(); #endif #if (PREFETCH_ENABLE ! 0U) __HAL_FLASH_PREFETCH_BUFFER_ENABLE(); #endif /* 设置NVIC中断优先级分组 */ HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4); /* 配置HAL库的时基默认使用SysTick1ms中断一次 */ HAL_InitTick(TICK_INT_PRIORITY); /* 底层硬件初始化用户可重写 */ HAL_MspInit(); return HAL_OK; }严格来说HAL_Init本身并没有直接调用HAL_Delay。HAL_InitTick()里做的是配置SysTick的重装载值、清零当前计数值、使能SysTick中断然后设置中断优先级。真正调用HAL_Delay的地方通常是SystemClock_Config()、外设初始化函数、或者你自己的HAL_MspInit()里。所以标题里说“HAL_Init() hangs in HAL_Delay()”更准确的描述是HAL_Init()调用链之后的某个初始化流程里第一次调用HAL_Delay()时系统卡死了。卡死的位置本身无所谓关键是HAL_Delay为什么进去就出不来。2.2 HAL_Delay为什么是一个“死等”循环HAL_Delay不是靠空循环消耗CPU实现的它是基于一个全局变量uwTick的查询循环__weak void HAL_Delay(uint32_t Delay) { __IO uint32_t tickstart uwTick; uint32_t wait Delay; if (wait uwTickFreq) { wait (uint32_t)1; } while ((HAL_GetTick() - tickstart) wait) { } }uwTick在SysTick_Handler中断服务函数里通过HAL_IncTick()每次加1void SysTick_Handler(void) { HAL_IncTick(); }所以HAL_Delay能正常工作的前提是SysTick被正确配置并使能SysTick中断能按时触发SysTick_Handler被正确挂到中断向量表SysTick_Handler里调用了HAL_IncTick让uwTick增长。这四个前提任何一环出问题HAL_Delay就会进入死等状态。而在调试场景下还有一个非常隐蔽的“第五个前提”SysTick中断必须在你暂停调试时还能照常触发。但Cortex-M的SysTick恰恰做不到这一点。2.3 SysTick_Handler和HAL_IncTick是配套的先别急着往下排查硬件检查一下你的工程里SysTick_Handler长什么样。CubeMX生成的工程中stm32h7xx_it.c里会有一个void SysTick_Handler(void) { HAL_IncTick(); }这个函数必须存在并且必须调用HAL_IncTick()。如果你是自己手写启动文件、或者从别的工程移植过来的很容易漏掉这一步。漏掉之后的现象就是全速跑的时候HAL_Delay一样会卡死因为uwTick永远是0。不过在本文的场景里全速跑是正常的所以这个原因只能算“备选”不是元凶。3. 调试时卡在HAL_Delay的六个高频原因3.1 最核心Cortex-M的SysTick在暂停时冻结这是最容易被忽视、也是真正的主角。Cortex-M3/M4/M7内核有一个设计行为当调试器把CPU暂停Halt时使用处理器时钟CLKSOURCE1的SysTick计数器会跟着内核一起停止计数。HAL库配置SysTick时就是用处理器时钟代码里会设置SysTick-CTRL | SysTick_CTRL_CLKSOURCE_Msk | SysTick_CTRL_TICKINT_Msk | SysTick_CTRL_ENABLE_Msk;这意味着什么你在调试器里点了暂停或者打了断点或者单步执行CPU停下来等你的指令SysTick也跟着停摆。此时来自SysTick的中断永远不会来uwTick也永远不会增加。然后你在HAL_Delay的while循环里继续单步每执行一次while判断uwTick都不变条件永远不满足自然永远跳不出来。不是H745特例所有Cortex-M的HAL工程都有这个问题。你全速运行时CPU没停SysTick正常跑HAL_Delay几毫秒就过去了。一旦进入调试暂停状态它就成了一个永远等不到的中断。说句玩笑话这是“嵌入式调试界的新手村大礼包”几乎人人都会踩一次。3.2 第一次HAL_Delay往往在系统时钟切换之前回到H745的启动流程。CubeMX生成的main函数开头是这样的int main(void) { HAL_Init(); SystemClock_Config(); /* ... */ }HAL_Init()在SystemClock_Config()之前运行。此时系统时钟还是上电默认的MSI 64MHzHAL_InitTick()就是用这个64MHz算SysTick重装载值reload_value SystemCoreClock / (1000U / uwTickFreq);64MHz时重装载值就是64000。这个配置本身没问题。但问题在于调试时如果你在HAL_Init()入口处设了断点然后单步执行紧接着的某个初始化函数调用了HAL_Delay此时CPU是暂停的SysTick从第一次配置开始就没走过。还有一个变体如果SystemClock_Config()内部或之后的某段代码调用了HAL_Delay而PLL切换完成后、HAL_RCC_ClockConfig()又调用HAL_InitTick()重新配置SysTick在重新配置之前SysTick的reload值还是老的值系统时钟却已经变了tick周期就会偏甚至出现类似卡死的现象。不过这个一般只是时间不准不一定会完全卡死。真正让它卡死的还是调试暂停。3.3 SystemCoreClock或Trace时钟频率设置不对SystemCoreClock这个全局变量决定了SysTick重装载值。如果它不对比如还是默认的64MHz但实际系统时钟已经被你切到480MHz而HAL_RCC_ClockConfig()又没有正确更新这种情况极少但手写时钟配置时很容易出SysTick触发频率就会变成预期的好几倍或几分之一。全速跑还能勉强工作调试暂停时因为计数停止照样卡死。另外一个相关的点在Keil MDK的调试界面里如果启用了TraceSWV功能需要手动设置内核时钟频率。如果你设置了错误的频率调试器对时间的跟踪会错乱甚至影响SysTick的观察结果让你误判。3.4 SysTick_Handler没调HAL_IncTick这个前面提到过单独列出来是提醒你别忽略。有些精简过的工程模板中断向量表里没有定义SysTick_Handler或者定义了但里面是空的。这种情况不管调不调试HAL_Delay都会卡死。如果你是全速正常、只有debug卡死那这个原因基本可以排除但如果你Debug和Release都卡先查这个。3.5 在复位向量或main入口单步被误伤很多人习惯在调试启动时勾选“Run to main()”让程序自动跑到main函数入口的断点。然后从main的第一行开始单步。这个习惯本身没问题问题在于单步到HAL_Init()附近时只要遇到HAL_DelayCPU就停在暂停状态SysTick又不会跑直接就卡住。还有一个场景你从复位向量Reset_Handler开始单步可能在SystemInit()、__low_level_init()等阶段就停下来过此时外设时钟还没完全起来。等你一路单步到main再单步到HAL_DelayCPU仍然是暂停的结果一样卡住。3.6 H745双核额外要注意的调试目标选择STM32H745XI是双核芯片Cortex-M7 Cortex-M4。两个核各自有独立的SysTick但共享大部分时钟资源。调试的时候Keil或ST-Link默认连接的通常是M7核。如果你把应用程序下载到M4核上跑然后在M7核上打断点或者暂停两个内核的状态不是完全同步的。常见的情况是M7核暂停时如果它正在通过RCC操作某些共享时钟或者M4核正在等一个由M7释放的信号比如HSEM硬件信号量双核之间互相等待调试器一暂停看起来就像全系统卡死。还有一个RDCResource Domain Controller的问题H745里可以把部分外设或时钟域分配给特定内核访问。如果M7试图访问被RDC划给M4的资源总线访问会挂起现象也可能表现为HAL_Delay卡死。所以调试双核项目前先确认你当前调试的目标核是不是你要调试的那个。CubeMX生成的工程M7和M4是分开的两个工程下载和调试也要分开处理。4. 怎么快速定位窗口、变量、寄存器三板斧4.1 Keil里如何查看正在变化的变量很多新手遇到这个问题并不知道该看什么。我说两个最直接的入口。第一Watch窗口。在Keil的Debug模式下菜单View - Watch Window - Watch 1然后在Watch 1里输入变量名。HAL库的uwTick是全局变量可以直接添加。另外把HAL_Delay的局部变量Delay、tickstart、wait也加进去。当程序卡在while里时你会看到uwTick一直不变tickstart和uwTick相等wait还是你传入的延时值。这个画面就是“SysTick根本没有更新”的铁证。第二命令窗口。在View - Command Window里输入? uwTick回车会直接打印当前值。也可以输入? SysTick-CTRL查看SysTick控制寄存器状态。命令窗口在调试时比Watch窗口还快适合快速确认某个值。4.2 从SysTick寄存器判断是“配置错”还是“被暂停”打开Peripherals - Core Peripherals - SysTick窗口可以看到完整寄存器信息寄存器关键位正常值异常情况SYST_CSRENABLE(bit0)10说明SysTick根本没使能SYST_CSRTICKINT(bit1)10说明SysTick中断被关闭SYST_CSRCLKSOURCE(bit2)10说明使用的是外部参考时钟SYST_RVRRELOAD与时钟频率对应数值异常说明时钟配置有问题SYST_CVRCURRENT不确定暂停时永远不变正常如果你的CTRL寄存器ENABLE和TICKINT都是1说明SysTick配置没问题。此时再看CVR如果暂停状态下CVR完全不动那就是典型的“调试暂停导致SysTick停止”。如果ENABLE是0说明你的工程可能自己关闭了SysTick或者tick source根本就不是SysTick。另外一个可以看的寄存器是ICSR中断控制和状态寄存器。在Peripherals - Core Peripherals - NVIC窗口里找到PENDSTSET这一位。如果SysTick中断已经挂起PENDSTSET应该是1。如果它一直是0说明SysTick中断源没有被触发问题可能在时钟源或SysTick使能上。4.3 使用DWT CYCCNT做时间基准旁证如果你对SysTick寄存器不太放心还可以用DWT-CYCCNT来交叉验证。DWT的周期计数器基于内核时钟每次内核跑一个周期就加1。在调试暂停时CYCCNT同样停止。这个特性可以帮你判断“到底是被调试暂停卡住还是时钟配置问题”。初步初始化DWTCoreDebug-DEMCR | CoreDebug_DEMCR_TRCENA_Msk; DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk; DWT-CYCCNT 0U;在HAL_Delay卡住时查看DWT-CYCCNT。如果它在你暂停期间保持不变说明内核时钟域整体都停了更印证了SysTick不会增长。如果你全速跑一小段CYCCNT在涨而uwTick不涨那就要回头查SysTick_Handler或中断向量表了。5. 我实测有效的四种解决办法5.1 最省事全速运行到关键点再手动暂停如果你只是在初始化阶段想调试外设寄存器不需要单步走HAL_Delay那就别在HAL_Delay处单步。做法是在HAL_Init()之前设置断点然后在HAL_Delay调用之后的一行设置另一个断点右键选择“Run to Cursor”让程序全速跳过延时段。全速运行时SysTick是正常工作的HAL_Delay能正常返回然后再次停在下一个断点。这个办法零成本适合只是想看初始化结果、不关心中间时序的场景。缺点也很明显一旦你想单步跟踪某一段初始化逻辑还是会踩坑。5.2 治本把HAL时基从SysTick换成TIM6这是我在项目里最终采用的方案一劳永逸。在STM32CubeMX中SYS - Timebase Source那里把默认的SysTick改成TIM6或者TIM7。重新生成工程后HAL库的时基就变成硬件定时器中断不再依赖SysTick。工作原理是TIM6挂在外设时钟上它的计数不受调试器暂停CPU的影响只要没有手动在DBGMCU里冻结TIM6暂停调试时它照样计数中断照样触发uwTick继续增长HAL_Delay也就不会卡死。这个方案还有一个额外好处SysTick可以腾出来给RTOS用。如果你后面要移植FreeRTOSCubeMX在启用FreeRTOS时其实也会自动把HAL时基切到TIM道理是一样的。需要注意改完Timebase Source后SysTick_Handler里就不需要再调用HAL_IncTick了。而定时器中断里会多出一个类似void TIM6_DAC_IRQHandler(void) { HAL_TIM_IRQHandler(htim6); }并且HAL_TIM_PeriodElapsedCallback里会调用HAL_IncTick()。生成代码会自动处理好不用手动改。5.3 在线修改uwTick让死循环跳出这是个调试技巧适合你暂时不想改工程、只是想快速验证后续代码的情况。当程序卡在HAL_Delay的while循环里时直接在Watch窗口找到uwTick把它修改为tickstart wait 1。修改后while条件立即不成立程序继续向下执行。具体操作是在Watch窗口右键uwTick - 选“Edit Value”输入tickstart wait的值加1。或者用命令窗口输入uwTick tickstart wait 1。注意tickstart和wait是HAL_Delay的局部变量命令窗口里不一定能直接访问到可以先查一下它们的当前值再手动计算。这个办法只能“救急”不能根治而且修改后延时行为会错乱。但如果系统已经启动到后期你只想快速看一眼后面模块的输出它比重新配置工程快得多。5.4 检查时钟配置与SysTick_Handler如果上面几个办法都试了还是卡那就得认真排一下SysTick相关配置了。第一确认stm32h7xx_it.c里SysTick_Handler调用了HAL_IncTick。有些精简工程模板会漏掉或者被你的其他中断重名覆盖。第二确认SystemCoreClock变量的值。可以在SystemClock_Config()执行完之后打印或者Watch一下SystemCoreClock看看是不是你配置的目标频率。如果不对检查有没有调用HAL_RCC_ClockConfig以及HAL_RCC_ClockConfig里是否确实更新了SystemCoreClock。第三检查调试器的Trace设置。如果你开了SWV把内核时钟设置成和实际一致。在Keil里是Options for Target - Debug - Settings - Trace需要填CPU时钟频率这里填错会导致调试器对SysTick的观测混乱。6. 一次完整排查的实操记录写一个真实排查过程方便大家对号入座。开发环境STM32H745XI 双核板M7工程STM32CubeMX 6.8生成Keil MDK 5.39ST-Link V3现象程序全速运行启动正常进入Debug后在main入口暂停单步到HAL_Init附近时卡在HAL_Delay。观察堆栈调用栈显示HAL_Delay - SystemClock_Config内部的某个等待或者更常见的是HAL_MspInit里我加的HAL_Delay(50)。处理过程第一步先看PC停在HAL_Delay的哪一行。反汇编窗口发现停在while循环比较处。说明HAL_Delay已经进入轮询状态。第二步打开Watch窗口添加uwTick和Delay。发现uwTick一直等于0Delay等于50tickstart等于0。也就是说从进入HAL_Delay那一刻起uwTick一次都没动过。第三步查看Peripherals - Core Peripherals - SysTick窗口。CTRL寄存器ENABLE1、TICKINT1、CLKSOURCE1SysTick配置了SYST_RVR64000说明SystemCoreClock是64MHz重载正确SYST_CVR停在某个值不动。CSR和RVR都对CVR不动再结合uwTick不变基本能确认SysTick没有更新。第四步因为全速运行正常排除了向量表和HAL_IncTick缺失的问题。那么剩下的解释就是内核处于暂停状态SysTick停止计数。这与Cortex-M的调试行为完全一致。第五步我没有改代码先用Run to Cursor跳过HAL_Delay验证后续初始化正常。然后在CubeMX里把Timebase Source从SysTick改成TIM6重新生成、编译、下载。再次进入Debug模式单步执行HAL_Init相关流程顺利通过HAL_Delay不再卡死。问题解决。整个过程大概半小时大部分时间花在看寄存器和确认SysTick行为上。如果你遇到类似问题最快的路径是先看uwTick变不变再确认SysTick配置然后直接用TIM切换方案。7. 速查表和一些“邪门”技巧我把常见情况整理成一张速查表方便后续排查现象可能原因验证方法解决办法全速正常debug暂停时卡HAL_Delay调试器暂停导致SysTick停止uwTick不变、SYST_CVR不动改TIM时基/全速跳过全速也卡HAL_DelaySysTick_Handler缺失或没调HAL_IncTick在SysTick_Handler打断点补全中断函数HAL_Delay时间不准SystemCoreClock与实际时钟不符Watch SystemCoreClock调用SystemCoreClockUpdateSysTick没使能Tick配置被其他代码覆盖查看SYST_CSR.ENABLE恢复SysTick配置双核调试混乱调试目标选错或RDC配置不当查看当前核状态切换调试目标核最后再分享几个实操中比较“邪门”但很好用的技巧。第一个如果你用SysTick做时基又改不了工程可以在调试会话开始前把“Run to main()”选项关掉让程序直接从复位向量全速运行到main。但是不要在main入口停让它继续全速跑到你的目标断点避免中途暂停导致SysTick冻结。第二个如果你需要在初始化阶段单步调试又不想切TIM可以在进入HAL_Delay之前临时把SysTick中断优先级调低然后用一个外部信号源比如另一个核的GPIO翻转给SysTick外设提供时钟这个太麻烦不推荐。实操中我用下来最顺手的还是切TIM没有之一。第三个利用调试器的“硬件断点脚本”功能。有些调试器支持在断点命中时自动执行一段脚本把uwTick修到一个略大于tickstart的值。Keil的Command窗口支持在断点处设置uwTick 1000这种操作这样即使卡在HAL_Delay也会被“无痛”推出去。不过这种操作对真实时序有干扰只适合快速验证逻辑。我在实际项目中最终把HAL时基固定在TIM6SysTick留给FreeRTOS。调试时再没遇到过HAL_Delay卡死的问题初始化阶段的单步调试也恢复正常。如果你用的是其他工程结构至少记住一点调试暂停时CPU停了SysTick也停了所有依赖SysTick的HAL延时都会变成死等。理解了这一点问题就已经解决一半。
返回列表