STM32U375低功耗模式下SPI Flash写入偶发损坏的根因与修复 1. 事故现场加了低功耗之后Flash数据开始随机损坏1.1 项目背景一块数据记录板卡要过功耗关我在做一款基于STM32U375的数据记录板采集端挂了一颗SPI接口的外部Flash用来存传感器原始数据和运行日志。STM32U375是新一代超低功耗MCUCortex-M33内核带TrustZone低功耗模式也分得很细既有STOP0这种浅睡眠也有STOP1、STOP2这种深度睡眠还有STANDBY和SHUTDOWN。当时项目的功耗指标卡得很死待机电流要压到几十微安以下所以低功耗方案自然选择了STOP1。开发的前期阶段一切正常SPI读写Flash、DMA搬运数据、日志循环覆盖存储这些功能在正常模式下跑得都很稳。问题出在我们做低功耗改造之后模块进入STOP1模式再唤醒继续做Flash写入就会偶发写失败。这个偶发是最烦人的它不是每次必现而是可能连续跑几十次都正常突然某一次写入的数据就是错的。最典型的表现是读取Flash时发现某个扇区里的数据是新旧混合的——前一半是本次写入的新数据后一半还是上次的旧数据或者整片出现0xFF擦除态看起来像是写了个寂寞。1.2 诡异的现象写失败不是必现而是概率出现排查初期我先怀疑是SPI速率太高导致信号质量问题。我的SPI时钟配置在20MHz走线不是很长理论上没问题但还是先把速率降到了5MHz试了一把问题依旧。又把SPI从硬件片选改成软件片选把CS的时序放宽也不行。真正开始让我警觉的是问题的复现条件非常明确只要进入STOP1前执行了拆除SPI连接的动作唤醒后紧接着写Flash就大概率出问题如果只进STOP1但进之前不碰SPI和GPIO配置写Flash就完全正常。这个现象让我意识到问题大概率出在拆除SPI连接这个动作本身而不是Flash器件或者SPI外设的硬件故障。提示如果你也遇到低功耗模式唤醒后外设行为异常的问题先把问题拆成两半——一半是低功耗模式本身导致的一半是进入低功耗前的清理动作导致的。很多时候责任在后半段。这个排查过程走了一些弯路但最终定位到的根因非常有代表性SPI总线的对端外部Flash在我们进入STOP1时并不是处于一个可随时断电的状态而我们在拆除连接时恰好踩中了Flash时序的禁区。接下来我会把STOP1模式下SPI、GPIO和外部Flash各自的状态讲清楚然后给出完整的排查链路和修复代码。2. 先搞清楚STOP1模式下SPI外设和GPIO还活到什么程度2.1 STOP1在U375的功耗栈里处于什么位置STM32U375的功耗模式从浅到深大致是Sleep、STOP0、STOP1、STOP2、STANDBY、SHUTDOWN。STOP0是最浅的停止模式关闭CPU时钟但保留大部分外设时钟能力唤醒快代价是功耗降得有限。STOP1比STOP0深一级内核和大部分外设的时钟都会停止SRAM可以保留快速唤醒仍然支持适合需要保留RAM数据同时功耗要求较高的场景。STOP2进一步关闭更多的SRAM区域具体取决于配置STANDBY基本等于所有RAM全部掉电唤醒从头开始执行代码。项目要保留日志缓存和通信上下文所以选了STOP1这是U375上保数据、压低功耗、快速唤醒三者平衡得最好的档位。在STOP1模式下进入前保持的GPIO输出状态默认是维持的这一点非常关键。很多工程师在这里有一个认知误区以为进了低功耗模式所有引脚都变成高阻或者关断了。实际上U375在STOP1下GPIO的电平是冻结的退出前配置成什么状态进入后就是什么状态不会自动释放。这意味着如果你进入前让某个引脚输出低电平那Flash的片选可能一直处于被拉低的状态如果你想让它变成高阻省电必须主动配置。2.2 进入STOP1时SPI外设、引脚和外部Flash各自的状态用一个表格把三方的状态列出来会清楚很多对象STOP1进入后状态需要特别注意的点STM32U375的SPI外设时钟停止寄存器状态保持发送/接收FIFO中的数据保持在原地BUSY标志可能仍为1GPIO引脚输出状态冻结保持进入前电平如果之前配置为推挽输出引脚上仍会输出对应电平外部SPI Flash无时钟、无片选控制进入听天由命状态如果当时正在执行Page Program或Sector Erase会因供电或时钟异常而写入失败如果外部Flash的VCC是由独立的负载开关控制的情况会更复杂。有的设计里MCU先进入STOP1然后负载开关延时关断VCC此时MCU的GPIO还维持着输出高电平就会通过Flash的IO引脚向VCC路径倒灌电流。即使你的板子不是这种设计只要MCU引脚仍然输出高电平而Flash的VCC在低功耗模式中被关断同样会有电流从GPIO灌进Flash的电源网络。2.3 我们常说的tear down SPI connection到底拆了什么拆除SPI连接在代码里通常不是一个单一动作而是一串操作。最常见的实现是这样的调用HAL_SPI_DeInit()或者直接操作寄存器关闭SPI外设把SCK、MOSI、MISO、CS这几个引脚从复用功能切换回GPIO模式为了省电把这些引脚配置成ANALOG模拟输入或者高阻输入个别设计里还会把CS引脚拉高或者保持某个静态电平。这一套动作做下来功耗确实能降不少因为引脚不再有静态电流SCK和MOSI也不会悬空抖动。但问题恰恰出在这里你拆除的只是一条数字连接但Flash内部可能还有一张正在进行中的写操作没有被处理完。以一颗典型的SPI NOR Flash比如W25Q系列、MX25L系列、GD25Q系列为例当主机发完Page Program命令、地址和数据后CS拉高的一瞬间Flash才开始进入内部编程阶段这个阶段需要的时间tPP通常在0.4ms到3ms之间。更长的Sector Erase操作则需要45ms到400ms不等。在这个内部操作过程中Flash芯片对VCC的稳定性和CS的电平极其敏感。如果此时MCU进入STOP1VDD电压出现瞬时跌落或者CS线被扰动Flash内部正在进行的编程/擦除操作就会中断后果就是数据不完整、扇区内容错乱甚至是状态寄存器被锁死。这就是tear down SPI connection before STOP1 breaks flash write背后的核心机制。3. 三种常见的拆除姿势三种不同的破坏路径3.1 姿势一DeInit外设后把所有引脚切到ANALOG这是追求极致功耗的典型做法代码大约长这样HAL_SPI_DeInit(hspi1); GPIO_InitStruct.Pin GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7; // SCK, MISO, MOSI GPIO_InitStruct.Mode GPIO_MODE_ANALOG; HAL_GPIO_Init(GPIOA, GPIO_InitStruct);跑起来之后功耗确实好看待机能低到手册标称值附近。但这个姿势的杀伤力在于GPIO从推挽输出切到ANALOG中间会经历一个短暂的过渡态引脚电平从高电平变成高阻。对Flash来说SCK和MOSI从有效电平塌陷到不确定电平非常容易产生毛刺。实测中我用逻辑分析仪抓到过再配置成ANALOG的瞬间SCK线上出现了一个大约几十纳秒宽的杂散脉冲。Flash虽然不一定会立刻把这种毛刺当成有效时钟沿但配合CS同时释放的情况就有概率误触发写状态机。3.2 姿势二只关时钟引脚保持推挽输出有些工程师觉得ANALOG切换太麻烦索性只关SPI外设时钟引脚保持原有推挽输出状态__HAL_SPI_DISABLE(hspi1); __HAL_RCC_SPI1_CLK_DISABLE();问题在于如果此时SPI正处于传输过程中——比如DMA刚搬完最后一个字节但SPI的移位寄存器还没把数据完全发出去——你直接把时钟关了SCK就会停在半路。Flash那边的状态机还等着剩余的时钟沿把命令收完整结果时钟断了CS又是低电平Flash就锁在了一个半命令状态。下次唤醒后继续通信Flash可能还能恢复因为芯片内部有超时机制。但在进入STOP1后外部Flash挂在一条半拉子总线上VDD又经历跌落那就是雪上加霜。3.3 姿势三只把CS拉高其他信号不动这个姿势相对温柔很多有经验的老手会先拉高CS释放片选再管其他。CS拉高在SPI协议里代表本次事务结束对于大多数Flash芯片CS拉高意味着当前操作被终止进入待机状态。看起来是在体面地收尾。但这里有个陷阱CS拉高只能终止普通的读写命令不能终止正在进行的内部编程/擦除。数据手册里写得清清楚楚Page Program或Sector Erase一旦进入内部执行阶段外部CS再拉高也没用只能在Flash完成内部操作之后CS拉高才会被识别为确认收到。如果在编程过程中拉高CS有些Flash会中止编程有些会继续但结果不可控数据手册在tPP期间对CS电平的要求往往是dont care但实际上翻车的案例不少。3.4 在外部Flash眼里这些操作意味着什么把三种姿势放在一起看它们的问题本质都一样你在Flash还没准备好退场的时候强行把总线和电源环境弄乱了。外部Flash对干净退场的要求可以归纳成三条写命令发出后必须等到内部操作BUSY位清零完成CS拉高必须发生在SCK完全静止之后VCC的稳定性必须覆盖整个内部写操作周期不能在编程中途掉电。任何一条不满足写失败都只是早晚的问题。我们项目最终锁定在第一条和第三条的组合上进入STOP1前前一刻还在接收数据——DMA搬运的最后一个数据块还没等Flash写完——代码就跑去拆SPI了然后PWR模块切到STOP1VDD瞬间有几毫秒的跌落Flash内部的升压电路供电不足直接写坏。这些拆除姿势在正常模式下可能永远不会暴露问题因为所有操作都发生在同一电压域Flash的编程窗口被宽松地满足了。但一旦加入低功耗模式切换电源和时序的裕量瞬间被压缩问题就浮出水面了。4. 把元凶从波形里揪出来一次完整的排查链路4.1 先复现再谈修复排查这种偶发问题第一要务是复现。如果问题不能稳定复现后面所有的假设都无从验证。我当时把整个流程写成了压力测试写4KB数据到Flash等Flash写完成轮询BUSY位然后进STOP1唤醒后再写4KB循环1000次。结果跑了大约300多次左右读回来的数据和写入的数据不一致。有了这个稳定的复现里程我开始在代码里布点在入STOP1的入口处翻转一个测试引脚同时用逻辑分析仪同时抓SCK、MOSI、MISO、CS和测试引脚这五路信号。这样就能把进入STOP1的时刻和SPI总线的最后一次活动的先后关系精确对齐。4.2 逐条验证假设时钟毛刺、片选时序、电源跌落我一开始的怀疑对象是时钟毛刺。因为之前看波形时发现GPIO切ANALOG瞬间SCK会闪一下那时候怀疑是不是这个毛刺让Flash误判成了写命令。所以先把GPIO切换的代码删了只保留HAL_SPI_DeInit问题概率下降了一些但还是会有。说明这不是主因。第二个假设是片选时序。我在退出SPI传输后立刻拉高CS然后延时1ms再进STOP1问题还是偶发。于是我把延时拉到10ms、20ms概率继续下降但没有完全消失。当时我心里已经有个模糊的方向可能和时间太短有关也可能是Flash的写操作根本没完成。最终让我确认的是示波器。我在MCU的VDD上接了探头用测试引脚触发看到进入STOP1的瞬间VDD跌落了大约300mV从3.3V掉到3.0V左右持续时间大约几百微秒。而Flash的数据手册明确写着编程时VCC不能低于2.7V写操作才能保证可靠。3.0V虽然在绝对最低值之上但裕量已经不大了如果板上还有其他负载同时切换这个跌落幅度会更大就会直接击穿Flash的最低工作电压。4.3 真正的元凶组合写操作没等完 CS提前释放综合所有波形和电压数据根因终于浮出水面我的代码在写完最后一个字节后只等待SPI外设的TXE标志变空就认为数据传输结束了直接去拆SPI连接、拉高CS、进STOP1。但SPI外设的TXE变空只代表数据从MCU侧发出去了不代表Flash已经完成内部编程。数据从MCU的SPI移位寄存器发出去经过SCK时钟同步进入Flash的输入缓冲Flash开始执行Page Program这个执行时间tPP在毫秒级。如果此时MCU进了STOP1、VDD发生跌落、CS被提前拉高Flash的编程过程就处于一个供电不足输入状态被破坏的状态结果就是写入数据损坏。这里最核心的教训是SPI外设的发完和Flash的写完是完全不同的两回事。发完是MCU视角的完成写完是Flash内部非易失性存储单元真正的物理操作完成。很多工程师习惯把SPI发送完成当成整个写操作完成省掉了轮询Flash状态寄存器的BUSY位这步这在正常供电、正常时序下不容易出事但一旦遇到低功耗模式切换这个省略的代价就显现出来了。提示调试器连接时Flash写入从不出错拔掉调试器就偶发失败——多半就是供电裕量问题。调试器通过SWD接口给目标板供了一部分电掩盖了真实电源的跌落幅度。这种问题在量产现场最致命因为开发环境下怎么测都正常。5. 修复方案给SPI Flash一个体面退场的序列5.1 修复一写完后必须轮询BUSY位修复的第一步很简单每次发起写操作后必须等待Flash内部操作结束。标准做法是发0x05读状态寄存器命令轮询bit0BUSY位直到它变成0。static void spi_flash_wait_busy(void) { uint8_t cmd 0x05; // Read Status Register uint8_t status 0x01; spi_flash_cs_low(); spi_flash_tx_rx_byte(cmd, 1); do { status spi_flash_tx_rx_byte(cmd, 0); // 连续读状态寄存器 } while (status 0x01); spi_flash_cs_high(); }注意读取状态寄存器期间CS必须保持低电平Flash才能持续输出状态字节读完再把CS拉高。这套逻辑是Flash操作的基本功但在低功耗改造时常常被忽略。5.2 修复二CS释放与时钟关断的顺序等待BUSY位清零之后还要注意释放顺序。正确的顺序是等待SPI发送FIFO为空且BUSY标志为0确保移位寄存器不残留数据停止DMA传输如果用了SPI DMA的话确认DMA传输完成中断已经触发把CS拉高释放片选等几百纳秒到几微秒让CS释放的电气效应完全稳定最后再关SPI外设时钟切GPIO状态。代码实现大概是这样void spi_flash_deinit_for_stop1(void) { // 1. 确保SPI内部没有在传的数据 while (!LL_SPI_IsActiveFlag_TXE(SPI1)) { ; } while (LL_SPI_IsActiveFlag_BSY(SPI1)) { ; } // 2. 等Flash内部编程/擦除真正完成 spi_flash_wait_busy(); // 3. 释放片选 LL_GPIO_SetOutputPin(CS_GPIO_Port, CS_Pin); // 4. 给总线一个静默窗口避免毛刺 delay_us(5); // 5. 关SPI外设 LL_APB2_GRP1_DisableClock(LL_APB2_GRP1_PERIPH_SPI1); // 6. GPIO切高阻如果外部Flash的VCC由独立开关控制且会关断这步必须放在Flash VCC掉电之后 LL_GPIO_SetPinMode(SCK_GPIO_Port, SCK_Pin, LL_GPIO_MODE_ANALOG); LL_GPIO_SetPinMode(MOSI_GPIO_Port, MOSI_Pin, LL_GPIO_MODE_ANALOG); LL_GPIO_SetPinMode(MISO_GPIO_Port, MISO_Pin, LL_GPIO_MODE_ANALOG); LL_GPIO_SetPinMode(CS_GPIO_Port, CS_Pin, LL_GPIO_MODE_ANALOG); }步骤6单独强调一下。如果你的板子上外部Flash的VCC在低功耗模式下会被关掉比如通过负载开关控制那么GPIO引脚必须切到高阻否则MCU的GPIO会通过Flash的IO引脚倒灌电流到Flash的VCC网络导致Flash实际上没有完全断电功耗白降了还可能在再次上电时出现闩锁风险。5.3 修复三GPIO低功耗状态与外部供电的协同如果你的设计进一步要求外部Flash的VCC在STOP1下完全关断那还需要考虑MCU GPIO和Flash VCC两个电源域之间的时序关系。最稳的做法是先把CS拉高SPI总线进入空闲等Flash完成内部操作BUSY轮询MCU GPIO全部切高阻最后关Flash的VCC。千万不能反着来先关Flash VCCGPIO还输出着高电平电流倒灌几乎不可避免。我在实验室里用电流表实测过GPIO输出3.3V高电平Flash VCC被切断后倒灌电流能达到几毫安对于低功耗设计来说这是不可接受的而且长期运行有损坏IO的风险。如果你的板子上Flash VCC和MCU VDD是同一个电源域那情况简单一些GPIO切不切高阻影响不大反正VCC都在。但为了安全还是建议切高阻防止引脚上的静态电流损耗。5.4 最终实现一个可复用的低功耗进入函数把上面几步整合成一个完整的进入函数配合状态机使用可以避免拆除SPI连接这一步重复造轮子typedef enum { ENTRY_IDLE, ENTRY_FLASH_IDLE_WAIT, ENTRY_BUS_RELEASE, ENTRY_GPIO_HIZ, ENTRY_STOP1_READY } lowpower_entry_state_t; void lowpower_enter_stop1(void) { lowpower_entry_state_t state ENTRY_IDLE; // 如果Flash正在写入告诉上层这次不能进STOP1 if (spi_flash_is_busy()) { // 记录日志返回等下次空闲再进低功耗 return; } spi_flash_deinit_for_stop1(); // 确认SPI和GPIO都进入安全状态后再进STOP1 HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI); }这里有一个很重要的设计取舍当Flash正在写的时候强行进入STOP1是不可接受的哪怕功耗指标再紧张也要等Flash写完成。你可以选择把进入STOP1的时机延后或者让Flash写入逻辑和低功耗逻辑通过一个互斥锁协调。最怕的是为了省电牺牲数据可靠性这种省最终会变成事故现场。对于要用DMA搬数据的场景还要注意DMA传输完成中断和SPI总线空闲两个事件的顺序。之前我用SPI DMA写Flash时踩过一个坑DMA传输完成中断触发了但SPI移位寄存器里还残留着最后几个时钟周期的数据没发完此时如果直接进入低功耗流程就等于发了一半被掐断。所以DMA完成之后依然要查SPI的BUSY标志确认移位寄存器清空。6. 踩过这坑之后我对低功耗设计的几点反思6.1 关闭外设之前先考虑外设另一端的状态这次问题的根源表面上是时序和供电问题本质上却是一个思维习惯问题我们在设计低功耗时总是盯着MCU内部怎么关、怎么降却很少站到总线对端器件的视角想问题。Flash在写你不能因为想省电就不让它写完传感器在转换你不能直接断电而不给完成信号。低功耗设计不是单纯地关东西而是组织所有参与者有序退场。我现在做低功耗方案时会先列一份外设退场清单逐个问这个外设当前可能处于什么状态它的状态机允许我在哪个点打断它打断后重新上电是怎么恢复的如果这个器件有自己的内部操作周期Flash编程、ADC转换、传感器测量那必须等它结束或者给它一个明确的sleep命令。6.2 电压域之间的电流倒灌比你想的更隐蔽GPIO输出高电平而外部器件VCC被切断导致的倒灌电流是一个在原理图阶段很难发现、在实测阶段又很容易被忽略的问题。它不会像短路那样立刻烧板子而是表现为待机电流异常、器件复位异常、偶尔写入失败。排查起来特别费时间因为软件逻辑完全正确硬件连接也查不出问题只有拿电流表在低功耗模式下逐路排查才能定位。如果你设计的板子上有多个独立供电的外设强烈建议在原理图评审阶段就把MCU引脚电平和外设VCC掉电之间的时序关系列出来。必要时加电平隔离器件或者统一用漏极开路的引脚别让IO成为电源轨之间的意外导体。6.3 低功耗的回归测试要覆盖写入中的中断场景这次bug之所以在项目后期才暴露是因为前期测试都是正常模式跑功能低功耗模式只测功耗没有把两个场景组合起来测。低功耗和高负载虽然不是互斥的但恰恰是它们交叉的瞬间最容易出问题。现在我把低功耗模式切换的回归测试分成了三类空闲状态下进低功耗醒来后外设功能正常外设正在工作时进低功耗比如Flash正在写入、CAN正在发帧、ADC正在采样验证系统会不会强制打断低功耗唤醒后紧接着执行密集读写验证外设状态机没有残留错误状态。第三类测试尤其重要因为很多外设的异常状态不会立刻报错而是在下一次操作时才体现出来。比如Flash这次写失败了读出来才发现数据不对中间已经隔了很长一段时间数据损坏早已发生只是在读取时才被发现。提示在做低功耗改造时不要只盯着数据手册里的典型电流值那是在理想条件下测出来的。实际系统的待机电流要多算上引脚漏电、外部器件静态电流、电源转换芯片自身的静态损耗。引脚切高阻和切ANALOG的漏电差异可能就几个微安但对低功耗产品来说这几个微安可能就是过不过认证的关键。说回到这次的问题修复本身并不复杂核心就是一件事在拆除SPI连接之前确认Flash已经完成它该做的事。但这件小事背后牵扯出的是低功耗模式下电源稳定性、外设状态机、GPIO配置时机、数据可靠性之间的复杂关系。任何一个环节考虑不到位埋在系统里的雷就会在某个不经意的时刻炸开。如果你也在STM32U375或者类似超低功耗MCU上做SPI Flash的低功耗改造我建议你回去看一眼自己进入低功耗的代码路径有没有轮询Flash的BUSY位有没有等SPI处于完全空闲状态GPIO切高阻和外部Flash VCC关断之间的顺序对不对这三条都确认无误基本就能避开我这个坑。剩下的就是让测试程序多跑几轮压力把偶发问题扼杀在实验室里而不是等它在现场咬你一口。

相关新闻

最新新闻

2026 Java后端面试新趋势:从背八股到讲场景,高频考点与故障排查实战

2026 Java后端面试新趋势:从背八股到讲场景,高频考点与故障排查实战

2026 年的 Java 后端面试,已经不只是背八股文了。我看了很多最近的技术讨论和岗位要求,一个很明显的变化是:Java 核心、Spring 全家桶这类基础仍然是底盘,但线上故障排查、项目落地细节、AI 大模型应用开发,正在变成新…

2026/8/30 18:08:54
Claude Code 从 0 到 1:安装、模型接入与高频报错排查

Claude Code 从 0 到 1:安装、模型接入与高频报错排查

第一次接触 Claude Code,很多人以为它只是把聊天窗口搬进了终端。真正装完、跑完一个任务之后,你会发现这个判断只对了一半:它确实能聊天,但聊天只是入口,真正值钱的是它能自己读文件、改代码、执行命令,把…

2026/8/30 18:08:54
Polar码译码实战:SCL与BP实现要点与function节点加速技巧

Polar码译码实战:SCL与BP实现要点与function节点加速技巧

简介:在信道编码与译码算法领域,Polar码凭借明确的极化结构和逼近香农限的性能,已成为5G及未来通信系统的关键方案。围绕Polar码的工程落地,SCL译码与BP译码是两条最主流的技术路径:前者基于串行抵消列表,通…

2026/8/30 18:08:54
AI自动化测试路线:从环境搭建到项目框架与就业

AI自动化测试路线:从环境搭建到项目框架与就业

AI自动化测试是我最近两年比较推荐测试从业者认真投入的方向,但先泼一盆冷水:网上流传的“30个项目学完即可就业”通常是引流说法,真正决定你能不能找到工作的,不是你收藏了多少个视频、刷了多少个项目编号,而是你能不…

2026/8/30 18:08:54
Vibe Coding应用部署运维实战:从容器化到监控告警的全流程指南

Vibe Coding应用部署运维实战:从容器化到监控告警的全流程指南

先说一个很真实的场景:团队用 Vibe Coding 的方式快速搭了一个 App 后端,从需求描述到 MVP 只用了两天。开发同学很开心,产品同学也很开心,结果部署上线后的第一周,运维同学连续三个晚上都在翻日志、补环境变量、重启容…

2026/8/30 18:08:54
模型评测排名为何波动?配置影响与可靠评测流程指南

模型评测排名为何波动?配置影响与可靠评测流程指南

最近一段时间,团队里有一个很常见的画面:几个人围着一份内部模型对比表,争论为什么昨天还领先的模型,今天就排到了后面。模型权重没有换,训练数据没有动,推理框架也没有升级,唯一的区别是有人把…

2026/8/30 18:03:53