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

资讯详情

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

SPI拆除致Flash写入失败?STM32U375低功耗模式排查与解决方案

SPI拆除致Flash写入失败?STM32U375低功耗模式排查与解决方案 “tearing down spi connection before stop1 breaks flash write on stm32u375”——如果你在搜索引擎里敲下这行字那你大概率和我当年一样正被一个看起来毫无逻辑的Bug折磨。SPI、STOP1、Flash写入这三件事在常识里八竿子打不着但在STM32U375上它们偏偏会缠在一起明明是进入低功耗前常规的“先收尾外设再保存数据最后睡过去”三步走结果每次写完Flash校验都对不上。更诡异的是只要把“拆SPI”这一步去掉Flash写入就一切正常。这个现象我第一次遇到时第一反应是怀疑代码里有数组越界或者野指针把Flash写流程给踩了。排查了整整两天最后才把嫌疑锁定在“SPI外设拆除”和“Flash编程”这两个八竿子打不着的模块之间。这篇文章就记录一下我在这类问题上的完整排查思路、根因推演和最终落地解法希望能帮到正在被STOP1模式和外设去初始化流程折磨的人。1. 场景还原为什么有人会在进入 STOP1 前拆掉 SPI 连接1.1 低功耗产品的典型收尾顺序在低功耗嵌入式产品里进入 STOP1 之前做“外设收尾”是非常自然的习惯。以一块带 SPI 传感器的板子为例主控需要先把传感器切换到低功耗状态再把SPI句柄反初始化把片选拉到一个确定电平最后才调HAL_PWR_EnterSTOPMode。这套动作本身没什么问题因为 STOP1 模式下大部分外设时钟都会停掉如果没提前把SPI外设关闭某些引脚在睡眠期间可能保持悬空或者出现不确定电平白白增加漏电流。问题出在我处理“收尾”和“存参数”这两件事的先后顺序上。我当时为了确保传感器在断电前收到完整的停止命令先执行了HAL_SPI_DeInit然后紧接着调用了Flash写入流程去保存一组标定参数。测试后发现只要SPI拆除在前、Flash写入在后写入结果就大概率是错的反过来先写Flash再拆SPI一切恢复正常。1.2 “tearing down SPI connection”到底指什么很多人看到“tear down SPI connection”以为就是调用一个HAL_SPI_DeInit其实在实际工程里SPI连接的拆除是一整套组合动作远不止关一个外设时钟那么简单。我在代码里排查的时候把它拆成了四步通过SPI总线向从机发送一条“进入低功耗/停止工作”的命令把片选信号 CS 拉高或者拉低取决于从机极性确保从机退出选中状态等待SPI发送缓冲区清空并确认无未完成的DMA传输调用HAL_SPI_DeInit关闭外设时钟并把 MOSI、MISO、SCK、CS 四个引脚重新配置成模拟输入或固定电平避免漏电。这四步里每一步都可能对后续Flash操作产生间接影响。比如等待DMA传输完成时如果加了超时死等SysTick中断被关闭了就会卡死又比如引脚被重新配置后外部从机失去片选信号时的电源行为可能会发生变化进而扰动整个系统的电源轨。1.3 为什么这个问题值得专门写一篇我在ST官方社区和几个嵌入式论坛上都搜到过类似的问题但绝大多数讨论都停留在“你换个顺序试试”的层面没有人把背后的机制讲透。其实这类问题非常有代表性因为它暴露的是嵌入式系统里一个容易被忽略的真理外设、时钟、电源、Flash这几个子系统在低功耗模式下不是独立工作的它们共享着芯片内部的电源域和时钟树。任何一步操作如果触发了电源模式切换或者时钟源切换都可能对正在进行的Flash编程造成致命影响。搞清楚这个问题的排查方法远比记住“先写Flash再拆SPI”这个结论有价值。因为只要你理解了底层机制以后遇到SPI DMA残留、外部Flash编程中断、甚至I2C外设拆除后掉电保存失败这些类似问题都能用同一套逻辑去定位。2. 相关机制原理SPI、STOP1 与 Flash 写入的底层关系2.1 SPI 外设拆除时到底动了哪些东西从STM32的参考手册来看SPI外设挂在APB总线上它的时钟来自RCC_APB1ENR或RCC_APB2ENR使能位。HAL_SPI_DeInit做的事情主要有三件把SPI控制寄存器复位成默认值、清零DMA请求位、把GPIO引脚恢复为复位后的模拟输入状态。但这里有一个很容易被忽略的细节如果SPI使用的是DMA模式拆除SPI之前必须先处理DMA的残留传输。DMA传输过程中如果直接调用HAL_SPI_DeInitDMA的传输完成中断可能还会在稍后被触发这时候SPI外设已经关了中断服务函数里访问SPI数据寄存器就会产生总线错误或者读到脏数据。这种未定义行为如果恰好发生在Flash写入的关键时刻完全有可能把CPU的执行流打断。另外如果你的SPI从机是一个需要供电的外部芯片拆SPI连接的瞬间从机可能因为失去时钟源而进入未知状态电流波形出现一个短暂的尖峰。在电池供电的设备上这个尖峰如果足够大会直接拉低主控的 VDD让Flash编程期间的内部稳压器输出产生抖动。2.2 STOP1 模式下系统状态的变化STM32U375属于STM32U5系列基于Cortex-M33内核低功耗模式设计得比传统F系列复杂不少。STOP1模式下系统主时钟SYSCLK完全停止大部分外设时钟被门控关闭主稳压器关断由低功耗稳压器维持SRAM和部分寄存器供电。从外部看芯片就像“睡死过去了”但只要唤醒源配置正确一个GPIO中断或RTC闹钟就能把它叫醒。关键点在于从执行WFI指令到真正进入STOP1硬件需要一个短暂的时间窗口。在这个窗口内电源管理单元PMU会按照预设的步骤关闭主稳压器、切换到低功耗稳压器。如果你的Flash写入操作在这个窗口内还没完成就会面临一个尴尬的局面——要么Flash编程被迫中断要么编程过程中电源电压已经掉到了不允许的范围。ST的参考手册里通常会写清楚Flash编程操作允许的电压范围比如要求VDD在某个区间内这个范围一般覆盖正常运行模式。低功耗稳压器输出的核心电压比主稳压器低因此严格来说Flash编程应该尽量避开低功耗稳压器切换的时间点。2.3 Flash 写入的苛刻条件很多人把STM32内部Flash写入想得太简单以为就是“解锁-写数据-上锁”三步。实际上内部Flash的写入时序要求非常严格。首先Flash编程之前必须等待前一次操作完全结束判断依据是FLASH_SR寄存器里的BSY位。如果在忙标志还没清除时就发起新的编程命令硬件会直接返回错误或者忽略操作。其次API擦写过程需要内部升压电荷泵工作这个电荷泵对电源电压的稳定性要求很高。电压跌落超过一定阈值编程就会静默失败——不报错但写进去的数据是错的。最后CPU执行Flash编程指令时代码本身必须运行在非Flash区域比如SRAM或Cache这是为了规避写Flash期间从同一块Flash取指导致的冲突。HAL库在内部其实会做代码重定向但如果你关闭了预取缓冲或者Cache时序余量会变小。2.4 三者之间的隐藏联系电源域、时钟源、中断把SPI、STOP1、Flash这三件事放在一张系统框图里看它们的交汇点其实是三个第一个是电源域。SPI外设和Flash控制器虽然工作在不同的电压域但它们的供电都源于同一棵电源树。SPI拆除时GPIO电平变化导致的外部漏电或者外部设备因为失去时钟而出现大电流冲击都会影响整棵电源树的稳定性。第二个是时钟树。进入STOP1之前很多工程师会手动关闭PLL、把系统时钟切到HSI这样进入低功耗后时钟收敛更快。但切换时钟源的瞬间如果Flash的等待周期LATENCY配置没有同步调整Flash控制器的时序就会错乱。SPI拆除流程如果恰好参与了这次时钟切换比如在HAL_SPI_DeInit之前有一行__HAL_RCC_PLL_DISABLEFlash写入失败就是顺理成章的事。第三个是中断。SPI拆除过程中如果DMA中断没有处理干净中断会在Flash写入过程中被触发。Flash编程函数内部往往有对FLASH寄存器的独占访问一旦被高优先级中断打断要么编程中止要么状态标志被错误清除总之结果都不好。3. 根因推演为什么 Flash 写入会失败3.1 根因A低功耗稳压器切换与 Flash 编程电压冲突这是我最先怀疑的方向也是几种可能性里最“硬核”的。测试中如果我让系统在Flash写入还没完全结束时就开始执行HAL_PWR_EnterSTOPMode写入结果就会变得不稳定。虽然代码上HAL_FLASH_Program是一个阻塞函数理论上它返回时写入已经完成但前提是写入期间供电没有波动。问题在于在标准的STOP1进入流程中硬件在收到WFI指令后会先继续执行完当前指令然后才对电源做切换。如果你的SPI拆除流程在某个中断里偷偷调用了电源相关的操作比如提前把稳压器切到了低功耗模式Flash编程电压就不达标了。这个场景下必须用示波器抓VDD和VCORE的波形才能实锤。3.2 根因B时钟树变动导致 Flash 时序异常第二种可能性是SPI拆除流程里存在时钟切换动作。举个真实例子我之前用CubeMX生成工程时SPI1配置成了PLL1Q时钟源进入低功耗前为了省电我在SPI DeInit之后加了一句HAL_RCC_ClockConfig把系统时钟从PLL切回了HSI16。理论上是没问题的但我在切换之前没有调__HAL_FLASH_SET_LATENCY(FLASH_LATENCY_0)导致切到HSI16后Flash等待周期依然是PLL高速运行时的配置。低主频配高等待周期Flash读时序出现了多余延迟写操作在这种错乱配置下就失败或者写出了错误数据。如果你把这个时钟切换代码放在了SPI拆除函数里那就会完美复现标题描述的现象先拆SPI再写FlashFlash写坏先写Flash再拆SPI写成功了。3.3 根因CGPIO 状态变化引发外部电源扰动第三种可能性和外部硬件强相关。SPI拆除时GPIO引脚从“复用推挽输出”切换成“模拟输入”这个过程在PCB走线上会产生电平跳变。如果从机芯片的电源和主控的VDD是同一路供电而从机在失去片选/时钟后有一个瞬间大电流状态比如内部电荷泵放电就会在VDD上制造一个几十毫伏甚至上百毫伏的跌落。Flash编程期间对VDD纹波非常敏感特别是内部电荷泵升压到编程电压时任何额外的负载变化都会让它输出抖动。我验证过一次在SPI拆除和Flash写入之间加一个大电容故障率从60%降到了不到5%说明这条路径是真实存在的。3.4 根因D关闭外设时残留中断或 DMA 操作干扰最后一种是纯软件层面的竞争。SPI使用DMA传输时如果拆除SPI前没有正确等待DMA通道完成并且清除中断标志DMA传输完成中断会在一个不可预期的时刻触发。如果此时主程序正运行在Flash写入的临界区里中断服务函数访问SPI寄存器可能触发总线错误或者抢占CPU后把Flash写入流程搞得一团糟。这类问题隐蔽性最强因为不是100%复现时序敏感换个编译器优化等级可能就消失了。3.5 如何判断你的场景属于哪一类我把这四种根因整理成了一张自查对照表方便读者对号入座。现象特征嫌疑根因首选验证手段去掉SPI拆除后故障率明显下降且故障随负载电流波动电源扰动根因C示波器抓 VDD/VCORE观察Flash编程期间的电压跌落代码在SPI拆除中有PLL/HSI切换Flash初始化在切换前时钟树错配根因B检查切换前后 LATENCY 配置打印 RCC_CFGR 寄存器Flash写入失败时伴随 NMI 或HardFault残留中断/DMA根因D在错误中断里抓现场检查 DMA 中断标志故障只在温度/负载极限时出现且降低核心频率后好转稳压器切换电压冲突根因A用 DAC比较器监测VCORE或咨询ST FAE获取最小写入电压值4. 问题复现与调试排查实操4.1 构建一个最小复现工程排查这种偶发性问题最重要的就是构造一个稳定复现的最小工程。我建议直接新建一个独立的测试工程只包含SPI外设初始化、Flash读写、低功耗模式切换三部分代码去掉所有业务逻辑这样能最大限度排除干扰。我把整个复现流程分成了四步用CubeMX生成一个STM32U375最小工程SPI1以阻塞模式初始化先不用DMA排除DMA干扰在main里定义一块测试用的Flash页写入一串固定模式的数据比如0x5A5A...写入后回读校验在进入STOP1之前先执行HAL_SPI_DeInit再执行Flash写入重复执行“写入-校验-复位”循环统计失败率。我实测下来这个最小工程用0x5A5A模式写入失败率大概在40%左右已经足够定位问题了。4.2 关键观测点与调试手段复现问题只是第一步真正困难的是观察现场。我推荐从三个维度下手第一个是寄存器级观测。在Flash写入前后打印FLASH_SR、FLASH_CR和RCC_CFGR重点看有没有编程错误标志位被置位以及时钟源有没有在SPI拆除过程中被意外切换。我的测试中发现Flash写入失败时FLASH_SR的PGSERR位经常被置位说明写入流程被硬件中止了而不是静默地写了脏数据。第二个是电流波形观测。在开发板的VDD输入端串联一个采样电阻用示波器抓Flash编程段的电流波形。如果发现SPI拆除后电流出现一个下凹再恢复的尖峰基本可以锁定电源扰动路径。当年我在一个四层板项目上抓到一个持续约3微秒、幅度接近80mV的VDD跌落恰好和传感器失电瞬间重叠。第三个是函数级插桩。在SPI拆除的四个子步骤和Flash写入的每个关键阶段分别设置GPIO翻转点用逻辑分析仪把时间关系拍下来。这一步能精确回答“到底是不是Flash写入还没结束STOP1就已经开始切稳压器”这个问题。4.3 从现象反推根因的排查路径我的排查顺序一般是这样先排除软件竞争根因D再检查时钟切换根因B然后看电源波形根因C最后才去怀疑芯片内部稳压器切换根因A。为什么这么排序因为软件竞争和时钟切换在代码里就能查成本最低。电源波形需要示波器但大多数实验室都具备条件。内部稳压器切换是最难测的因为VCORE引很多芯片上不直接引出非要测就得飞线或者割铜皮风险高、效率低。具体操作上我会先打开所有中断的调试断点观察Flash写入期间有没有异常中断进入。如果在写入中间命中了任何外设中断回调那基本就是软件竞争无疑。如果没有中断打扰再回头检查RCC配置函数有没有在SPI拆除流程里被调用。5. 解决方案与低功耗设计最佳实践5.1 方案一调整操作顺序先写 Flash 再拆外设在我排查的那个项目里最终落到实处的方案其实是最朴素的一条把Flash写入挪到SPI拆除之前。void PrepareForStop1(void) { // 先保存关键参数确保供电与时钟均处于稳定状态 FLASH_Unlock(); FLASH_ErasePage(USER_PARAM_PAGE); FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, USER_PARAM_ADDR, param); FLASH_Lock(); // 再拆除 SPI 连接避免残留状态影响低功耗电流 Spi_TearDown(); // 最后进入 STOP1 HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI); }这段代码看起来平平无奇但它之所以有效是因为它让Flash写入处在一个“系统还没有为低功耗做任何收尾”的稳定状态主稳压器还在正常工作系统时钟还没切换外部设备还在正常供电。所有可能干扰Flash写入的变量都被延迟到了写入完成之后。5.2 方案二把低功耗准备流程做成统一状态机单纯调整顺序能解决70%的问题但如果你有多组外设、多个低功耗入口靠人力保证顺序迟早会出错。更稳妥的做法是把低功耗准备做成一个显式的状态机把“Flash数据落盘”定义为一个不可被抢占的原子阶段。我习惯这样划分状态POWERDOWN_IDLE→POWERDOWN_FLASH_SAVE→POWERDOWN_EXTERNAL_DEINIT→POWERDOWN_STOP1。每个状态之间用回调函数串起来Flash保存阶段屏蔽所有可能执行外设操作的中断完成后置一个标志位才允许进入外设拆除阶段。这样即使未来代码膨胀、新增了某个需要在低功耗前处理的外设也不会破坏“Flash写入优先”的强约束。5.3 方案三Flash 写入期间锁定电源模式与时钟源如果你无法调整顺序比如Flash数据来源恰恰依赖SPI总线上的某个传感器那就要从硬件层面保证Flash写入期间的电源和时钟稳定。做法分两步。第一步在Flash写入前强制确认系统处于正常运行电压范围检查当前电源调节器模式如果已经被切到低功耗模式先调回正常模式等VCORE稳定后再开始写Flash。第二步锁死系统时钟源和Flash等待周期如果代码里有任何入口可能切换PLL/HSI在写Flash期间暂时屏蔽时钟切换请求或者用一个原子标志位让时钟管理模块知道当前处于“Flash关键区”等标志位清除后再执行切换。5.4 方案四SPI 拆除本身的规范操作细节如果你决定保留“先拆SPI再写Flash”的顺序有些场景确实绕不开那至少要把SPI拆除过程做得足够干净。这里有几个实测有效的细节片选信号务必在SPI外设关闭之前就拉到位避免在DeInit过程中GPIO被重新配置的瞬间产生毛刺检查是否有未完成的DMA传输读DMA的NDTR寄存器如果不为0就强制等待超时超时后清掉DMA中断标志和外设中断标志如果SPI从机有独立的使能引脚最好先拉掉使能再拆SPI时钟线让从机先进入确定状态关闭SPI外设时钟后把MOSI/SCK引脚配置为模拟输入CS引脚配置为推挽输出固定电平避免浮空输入带来的不确定电流。5.5 各方案对比与选型建议方案改动量效果适用场景调整操作顺序很小根治大多数普通低功耗产品统一低功耗状态机中等根治可维护外设多、低功耗流程复杂的产品锁定电源模式与时钟中等大幅降低风险无法调整时序Flash数据依赖外设规范SPI拆除细节小降低间接干扰所有使用SPI的项目建议无条件采纳我给大多数客户的推荐是方案一加方案四组合既快又稳。只有碰到多低功耗入口、多外设的大型固件时才值得投入成本做方案二。6. 常见问题与避坑经验速查表症状可能原因检查方法解决方案拆除SPI后Flash写入偶尔失败校验不过GPIO电平变化导致VDD跌落示波器抓VDD观察写入期间跌落先写Flash再拆SPI或加大电源电容Flash写入报 PGSERR 编程错误编程电压不足或时序错误读取FLASH_SR确定错误类型检查核心电压范围确认处于正常模式写入期间进入HardFaultDMA/外设中断残留在HardFault中断里看LR和堆栈在SPI DeInit前清除DMA中断标志换了调试器正常工作脱离调试器必现时序敏感中断响应差异反复插拔调试器对比用逻辑分析仪比对时序消除未定义行为降低CPU频率后故障消失Flash等待周期配置错误检查FLASH_ACR寄存器更新LATENCY配置后再切时钟最后再分享一个排查这类问题的心法遇到“两个功能模块互相影响”的Bug不要急着怀疑编译器、调试器或者芯片本身的问题先画出系统的电源树和时钟树找出两个模块的交汇点。SPI和Flash看似无关但它们共享电源、共享时钟树、共享中断优先级框架任何一环的微小扰动都可能被放大成匪夷所思的故障。我那天要是早一点把精力放在“SPI拆除是否改变了系统电压状态”上而不是在业务代码里翻来覆去至少能省下半天时间。希望这篇记录能让你少走这段弯路。
返回列表