ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32H7双核调试卡死HAL_Delay?从SysTick到RCC的根因排查

STM32H7双核调试卡死HAL_Delay?从SysTick到RCC的根因排查 “又卡住了而且卡在 HAL_Delay() 里。”这句话对用 STM32H7 的朋友来说应该不陌生。尤其是调试 H745XI 这种双核芯片时明明代码逻辑看起来没问题全速运行却怎么也跑不过 HAL_Init()走到 HAL_Delay(1) 就再也出不来。排除电源和硬件连接之后剩下的坑基本都藏在“调试器怎么复位芯片”和“双核共享 RCC”这两件事里。我早期第一次在 H745XI 上遇到这个现象时第一反应是 HAL 库有问题甚至怀疑芯片坏了。后来花了一整天把启动流程、SysTick 配置、时钟树全都翻了一遍才明白这其实是“SysTick 作为 HAL 时基”与“调试器干预”之间的一场典型冲突。这篇博文就按当时的排障路径来写先拆解 HAL_Init() 和 HAL_Delay() 的底层执行逻辑再给出从现象到根因的四步排查方法最后针对调试器复位策略、SysTick 配置缺失、双核运行冲突三类典型场景给出能直接抄的解决办法。正在用 H7 系列双核开发或者遇到类似“调试一进去就死等”问题的朋友可以参考一下。1. 现象复现每一次“卡死”都不是偶然1.1 我的现场H745XI 双核工程调试卡在 HAL_Delay今年年中我接手一个用 STM32H745XI 做双核采集的项目M7 负责主流程和数据处理M4 只做简单的 GPIO 触发读取。工具链是 Keil MDK 5.38 ST-LinkHAL 库由 STM32CubeMX 生成工程是默认的 M7 单核模板M4 部分当时还没有正式固件。第一次在板子上跑调试我就遇到了标题里说的这个现象编译、下载都正常一旦进入 Debug 模式程序要么停在 main 开头的断点上要么全速运行后整个系统像被冻住。暂停程序查看 Call Stack程序正卡在 HAL_Init() 内部的 HAL_Delay(1) 那行后面跟了一串调用关系。更迷惑的是我以为是自己初始化顺序不对于是把 HAL_Init() 挪到 main 最后、去掉所有外设初始化问题依旧。这个“什么问题都排除了还复现”的特点是 SysTick 这类内核定时器问题最典型的信号。因为 SysTick 的配置和中断响应不依赖具体外设很容易让排查方向跑偏。1.2 卡住的位置与最小复现条件先把最小复现条件固定下来不接外部晶振用内部 HSI 启动不初始化 GPIO、DMA、串口只保留 SystemInit()、HAL_Init()、SystemClock_Config() 三件套。在 Keil 里打开调试下载完自动复位运行然后全速跑。就这样的一个空工程依然在 HAL_Delay(1) 里死住那就说明和业务代码无关了。我还用一个笨办法确认它不是“慢”而是“死”在 HAL_Delay 的 while 循环里加一个局部计数变量放在 Watch 窗口观察。如果这个计数变量一直不变说明循环确实没退出来而不是单纯跑得慢。结果计数完全不动SysTick 中断根本没有在累积 tickHAL_GetTick() 的值始终是同一个数。这里要特别提醒一句如果你是单步走进 HAL_Delay 看到 while 出不来先别急着下结论说“卡死”。因为单步时内核暂停SysTick 计数器也暂停要全速运行几秒再看。我就见过同事在单步模式下纠结了半天最后全速跑一下就直接跳过去了。2. 源头拆解HAL_Init 和 HAL_Delay 的执行逻辑2.1 HAL_Init 里到底干了什么很多朋友对 HAL_Init() 的理解就是“初始化 HAL 库”然后就不管了。但在 H7 系列上它内部做的事会对 SysTick 有直接依赖。打开 stm32h7xx_hal.c 的 HAL_Init() 源码可以看到大致流程HAL_StatusTypeDef HAL_Init(void) { /* 1. 使能 Flash 指令缓存 / 数据缓存 */ /* 2. 设置 NVIC 优先级分组 */ HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4); /* 3. 用 SysTick 作为 HAL 时基并启动 tick */ if (HAL_InitTick(TICK_INT_PRIO) ! HAL_OK) { return HAL_ERROR; } /* 4. 调用用户自己的 MSP 初始化 */ HAL_MspInit(); return HAL_OK; }真正和 HAL_Delay 挂钩的是第 3 步。HAL_InitTick() 会做几件事根据当前系统时钟频率算出 SysTick 的重装值把 tick 周期配成 1ms把 SysTick 中断优先级设置好打开 SysTick 计数和中断使能。它的执行顺序是“先开中断再等中断”。不同版本的 HAL 库获取频率的方式略有差异有的直接读 SystemCoreClock有的通过 HAL_RCC_GetSysClockFreq() 动态计算。但本质都一样SysTick 的 LOAD 值必须等于“系统时钟频率 / 1000”。如果 HAL_Init 执行时系统时钟和 SystemCoreClock 不一致后面就会埋雷。2.2 HAL_Delay 的死等机制与 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而 uwTick 只能在 SysTick_Handler 中断里通过 HAL_IncTick() 增加。也就是说如果 SysTick 中断没发生HAL_Delay 就是一个永远都跳不出去的 while 循环连一点超时保护都没有。这也是为什么我会反复强调“看 SysTick 是否在跑”比“改 HAL_Delay 代码”更重要。SysTick 是内核私有定时器它的状态不像串口、GPIO 那样一眼就能看到必须从寄存器层面确认。一旦确认 SysTick 没有产生中断问题就回到了三个方向SysTick 没配置、中断没使能、或者是中断向量表根本没进来。2.3 为什么 debug 状态下更容易触发我觉得有三个因素叠加才造出这种“只在调试时出现”的诡异现象。第一个是单步与暂停。SysTick 使用的是处理器时钟源CPU 一旦被调试器暂停SysTick 计数器也会跟着停。如果你习惯单步调试走进 HAL_Delay 的 while 里每一步执行后 CPU 暂停SysTick 不能计数自然看不到 tick 在涨。这个问题在 Cortex-M 全系列都会遇到只是 H7 的 HAL 时基设计特别容易踩中。第二个是调试器的复位方式。Keil 里面 Debug 设置中的 Reset 选项可以选 SYSRESETREQ、VECTRESET、自动检测等。有些复位方式不会把调试逻辑里的一些冻结位清干净也不会重新把时钟源恢复到上电默认状态。如果你的代码在 HAL_Init 之前还没做过复杂时钟切换复位后 SysTick 配置时算出来的“当前时钟频率”可能和调试器实际提供给 CPU 的时钟不一致导致重装值算错。第三个是 H7 双核的共享外设。H745XI 是 M7 M4 双核两个核共享 RCC、Flash、GPIO、电源管理。调试器按“系统复位”时两个核一起复位M4 如果此时跑的是随机代码或者旧固件它把共享的时钟控制器改了M7 这边的 SysTick 就会莫名其妙“罢工”。3. 排查实操四步定位卡死根因3.1 第一步先确认不是“假死”——单步不算全速才算遇到卡在 HAL_Delay 的情况我建议先做三个快速确认先看 Call Stack。在 Keil 的 Call Stack 窗口里如果能看到当前 PC 停在 HAL_Delay 的 while 里面并且调用链是 main - HAL_Init - HAL_InitTick - HAL_Delay那就说明 HAL 库的初始化流程已经走到了等待 tick 的环节。再看 HAL_GetTick()。在 Watch 窗口添加表达式HAL_GetTick()然后全速运行。如果这个值一直不变说明 SysTick 中断没有在递增它如果这个值在变化只是程序很慢那就不算“死”可能是 SysTick 频率被配错导致延时不正常。最后用一个手动计数器辅助判断。在 while 循环里塞一个volatile uint32_t loop_cnt 0; loop_cnt;看这个变量是否持续增长。它能区分“代码没执行”和“代码在执行但条件不满足”这两种情况。这个土办法在排查死等类问题时非常有效。3.2 第二步看 SysTick 寄存器与中断状态确定是 SysTick 没跑之后打开 Keil 的 Peripherals - Core Peripherals - SysTick 窗口或者直接在 Watch 窗口添加以下表达式SysTick-CTRL SysTick-LOAD SysTick-VAL这几个值能说明大部分问题。正常情况应该是表达式正常值异常值含义SysTick-CTRL 0x011ENABLE0SysTick 根本没开SysTick-CTRL 0x021TICKINT0中断没使能SysTick-CTRL 0x041CLKSOURCE0可能用了外部时钟要注意来源SysTick-LOAD约 SystemCoreClock/10000 或异常大说明频率计算有问题SysTick-VAL不断变化固定不变说明时钟没进来这里最容易踩的坑是 LOAD 值看起来正常但 TICKINT 位是 0。有些用户自己的初始化代码或者 RTOS 启动代码会重新配置 SysTick把中断给关了。如果 SysTick_Handler 里没有调用 HAL_IncTick()那么即使 SysTick 在计数、中断也在发生uwTick 也永远不会增加HAL_Delay 照样死循环。还要确认全局中断是否打开。在 Debug 窗口里看 xPSR 的 I 位或者调用__get_PRIMASK()观察返回值。如果 PRIMASK 被置 1所有可屏蔽中断都无法响应SysTick 中断自然无效。这种情况在用户某个外设库函数里用了__disable_irq()但忘记恢复时特别常见。3.3 第三步查 Clock 与 RCC 是否还在预期状态如果 SysTick 寄存器看起来正常但 HAL_GetTick() 就是不涨下一步要查时钟树。在 Keil 的 Watch 窗口里添加SystemCoreClock RCC-CR RCC-CFGR重点看 RCC-CR 里的 HSIRDY、PLLRDY 这些就绪位以及 SystemCoreClock 变量的值。H7 系列上电默认是内部 HSI频率通常为 64MHzSystemCoreClock 应该和它匹配。如果在 HAL_Init 执行之前用户代码已经做了时钟切换但 SystemCoreClock 没有同步更新那么 SysTick 的 LOAD 值就会按错误频率计算。比如实际系统时钟已经切到 400MHz而 SystemCoreClock 还停留在 64MHz那么 SysTick 重装值是按 64MHz 算的实际中断频率会变成 400MHz / 64000大约 6.25kHz 而不是 1kHz。这种情况下 HAL_Delay 不会卡死但延时时间会快 6 倍多而且十分隐蔽。另一种更恶劣的情况是 PLL 还没锁定就切了过去系统时钟直接丢失SysTick 计数器得不到时钟输入那就是真正的死等。调试器在低功耗调试模式下也可能影响 SysTick 的时钟路径。如果 DBGMCU 的低功耗冻结位被置位外部定时器和部分总线时钟在低功耗模式下会被冻住虽然 SysTick 是内核定时器一般跟随 CPU 暂停但在某些低功耗调试配置下仍然可能被影响。建议在调试阶段不要打开低功耗相关冻结功能。3.4 第四步排查双核共用的 RCC 资源到了这一步如果 M7 自己的时钟和 SysTick 都没问题那我要强烈怀疑另一个核心在捣乱。H745XI 是双核M7 和 M4 共享大部分总线时钟和复位控制。调试器执行系统复位时两个核都会重新从启动地址运行。如果 M4 的工程还没做好或者 Flash 里 M4 区域本来就是随机数据M4 复位后会执行一段不可控代码这段代码可能把共享的 PLL、分频器、Flash 等待周期全部改乱。怎么判断是不是这个问题简单粗暴的办法在 M7 调试会话里手动把 M4 内核 halt 住。如果你的调试器支持多核调试可以在 Keil 里创建两个 target分别连接 M7 和 M4然后将 M4 的调试会话暂停或者通过 ST-Link 的 SWD 接口用 STM32CubeProgrammer 先连接 M4选择 halt 核心。然后再回到 M7 工程重新全速运行看 HAL_Delay 是否能过。如果 halt M4 之后问题消失那基本就实锤了M7 卡死是因为 M4 在启动阶段动了共享 RCC。这个原因在单核 MCU 上永远遇不到但 H745XI 这种双核芯片上非常常见而且越早遇到越好因为后面双核联调的坑会更多。4. 根因与三种正解4.1 场景A调试器复位策略导致执行顺序异常我遇到的第一个真正理论上的根因就是 Keil 的复位策略。Keil 在 Debug 模式下有几个复位相关的选项Options - Debug - Settings - Debug 页里的 Reset 选项默认是 Auto Detect但很多人会为了“稳定连接”改成 SYSRESETREQ 或 VECTRESET。不同复位方式对调试逻辑的影响不一样。SYSRESETREQ 是 Cortex-M 内核里最常用的软复位方式它会复位大部分系统逻辑但不会重新初始化调试接口本身VECTRESET 只复位 CPU 核心不复位外设和总线。问题是有些 ST-Link 固件版本执行复位时会忽略 Flash 下载算法的复位序列导致调试器先把目标复位了再恢复运行此时 RCC、SysTick 等硬件状态并没有被完整地重新初始化。这个问题的解决思路很直接下载完不要依赖“Reset and Run”那个自动复位改成手动控制。在 Keil 中我建议这样设置Debug - Settings - Debug 页Reset 选择 Auto Detect让调试器自己判断最合适的复位方式。Utilities - Settings 里不要勾选“Reset and Run”或者先不勾选等程序下载完成后自己在调试工具栏手动点一下 Reset。Flash Download 完成后确认程序入口地址和当前调试会话的逻辑一致避免程序从错误的向量表位置跑起来。如果你用 STM32CubeIDE思路类似在 Debug Configuration - Startup 页里把“Reset and halt”和“Run to main”分开设置通常先保持“Reset and halt”进入调试后手动全速运行。这样就绕开了调试器自动复位时序和用户代码启动时序冲突的问题。4.2 场景BSysTick 分频与 SystemCoreClock 不一致第二种场景是我在一个老工程里遇到的。工程从 F429 移植到 H745 后没有用 CubeMX 重新生成 startup 文件而是沿用老启动文件SystemCoreClock 变量在启动后没有被正确初始化。H7 的 system_stm32h7xx.c 在 SystemInit() 里会调用 SystemCoreClockUpdate() 来刷新 SystemCoreClock。但如果你自己修改了 SystemInit()或者在 HAL_Init 之前手动切换了时钟源却没有更新 SystemCoreClockHAL_InitTick 就会按错误的频率去配置 SysTick。一个比较稳妥的排查
返回列表