LMU之前那篇我写过同类型问题的排查思路,这次遇到的是 STM32N657L0 在低功耗模式下的 NOR flash XIP 访问问题,折腾了两天才定位到根因,把整个过程整理出来,希望能帮到同样被 LRUN、XIP 折磨的兄弟。
先说结论:这颗芯片的 NOR flash 在 XIP 模式下不是“电”的问题,而是“时序”和“顺序”的问题。LRUN 模式切换时,时钟源、电压档位和 flash 等待状态三者必须按严格顺序协同更新,任何一环脱节,跑着跑着就 HardFault,而且这问题在调试器下往往复现不出来,隐蔽性极高。
这篇文章适合正在用 STM32N6 系列做低功耗产品的嵌入式工程师,尤其是涉及内部 flash 直接执行代码、需要跑 LRUN 模式的场景。我会从原理讲到排查步骤,最后给一张症状对照表,你可以直接对照抄作业。
1. 问题现象:LRUN 模式下 XIP 执行的崩溃现场
1.1 项目背景和芯片选型
我负责的是一款工业数据采集终端,主控选的是 STM32N657L0。这颗料最大的优势是 Cortex-M7 内核跑到 800MHz,内部带大容量 NOR flash,代码可以直接映射到地址空间执行,也就是 XIP(Execute In Place),不需要像 Cortex-M0 系列那样非得把代码搬运到 SRAM 才能跑。
选它的原因很直接:一是单芯片方案可以省掉外部 NOR flash 的成本和布线;二是 800MHz 主频做 FFT、特征提取这类运算有余量;三是片内 flash 的 XIP 性能比 QSPI 外部 flash 的 XIP 要稳得多,毕竟是芯片内部总线,延时可预测。对于需要低功耗待机、定时唤醒采集的工业场景,LRUN 模式必不可少,这颗料也支持在低功耗运行模式下继续从 flash 取指。
1.2 现象描述与复现条件
问题出现在我们做完低功耗策略测试的第三天。白天跑正常 RUN 模式,连续 72 小时一点问题没有。晚上进入 LRUN 模式后,整机电流确实降下来了,但运行时间从几分钟到十几分钟不等,系统就会随机死机。通过看门狗抓到的复位原因来看,不是引脚复位也不是电源复位,而是内核锁死触发的 HardFault。
最让人头疼的是它的复现规律:接上 ST-LINK 用 IDE 在线调试时,怎么跑都不死;一旦拔掉调试器、脱离调试环境,问题必然出现。这种“调试器下正常、脱机必现”的毛病,我在之前几个低功耗项目里也遇到过,基本都是时序问题,不是逻辑问题。后来我拉长观察窗口,同时在关键位置翻转 GPIO,用逻辑分析仪记录死机前的状态,发现 HardFault 之前总会先出现一段时间异常的长周期取指停滞,方向指向了代码取指路径上的 flash 控制器。
2. 为什么 LRUN 和 XIP 会闹矛盾:原理拆解
2.1 从 NOR flash 为什么能 XIP 说起:和 NAND flash 的区别
要理解这个问题,得先明白 NOR flash 凭什么能做 XIP。NOR flash 的存储单元结构决定了它有完全随机的字节级读取能力,也就是说 CPU 发一个地址、给一个读时序,NOR flash 能在确定时间内把对应地址的指令取出来,不需要先读一整块到缓冲区再慢慢挑。这就让芯片可以把 flash 映射到统一地址空间,代码直接在 flash 地址上跑。
NAND flash 就不一样了。它虽然容量大、成本低,但只能按页读取,CPU 想取一条指令,还得先把整个页搬进 SRAM 缓冲,再从中找目标指令。这中间的时间是不确定的、不可预测的,所以 NAND 不可能直接 XIP,必须搭配外部 RAM 做 shadow 映射。这也是为什么业内常说 NOR flash 适合存代码、NAND flash 适合存数据。搞懂这个区别,你就能理解芯片原厂为什么把内部 flash 做成 NOR 结构,也就能理解为什么 XIP 对 flash 的访问时序极其敏感。
2.2 LRUN 模式到底动了哪些“定时炸弹”
LRUN(Low Power Run)模式是 STM32 低功耗策略里的关键一环。它跟停机模式不一样,CPU 内核没有停止,时钟还在跑,代码还在执行,只是系统时钟从 800MHz 的 PLL 输出降到几 MHz 级别的 MSI(多速内部振荡器)。电压档位也会从最高性能档降到低功耗档,片内外设时钟大多被门控,只有必要的定时器和唤醒源在工作。
听起来很简单:跑着跑着把主频降下来就行了。但问题就藏在“降频”这两个字里。CPU 时钟降了,flash 控制器的访问时序参数如果不跟着改,读 flash 的建立时间、保持时间就不匹配,轻则偶尔读错指令,重则取指直接超时、内核总线 stall,最终 HardFault。这就像你和一个搭档配合搬东西,原来约定好每秒搬 3 次,结果你突然改成每秒搬 1 次,搭档却还按原来的节奏伸手,不摔跤才怪。
2.3 等待状态、电压档位、时钟切换三者的耦合
这里要引入一个关键参数:flash 等待状态(Latency / Wait States)。NOR flash 内部存储阵列读取需要一段固定的物理时间,而 CPU 的时钟周期是随工作频率变化的。主频越高,一个时钟周期越短,flash 单次读可能赶不上 CPU 一个周期内完成取指,所以必须在两者之间插入若干等待周期,让 CPU 多等几个周期,等 flash 把数据稳定送出。
等待状态怎么定?芯片参考手册里会有一张表,横轴是主频范围,纵轴是电压档位,交叉点就是你应该配置的等待状态值。STM32N657L0 在 800MHz 高频下,等待状态通常要配到十几甚至更多;而在 LRUN 模式几 MHz 主频下,等待状态甚至可以是 0。问题就出在切换过程中:如果先降频、再改等待状态,中间会有一段窗口期,CPU 以低频率工作却仍按高频率的等待状态去读 flash,读取时间会比需要的更长,一般不至于出错;但如果顺序反过来,先调低了等待状态、主频还没降下来,CPU 就会在 flash 还没准备好的情况下强行取指,必崩。
电压档位同样是变量。主频 800MHz 时芯片肯定工作在最高电压档(比如 VOS1),降到几 MHz 的 LRUN 模式时,为了省电会切到低电压档。电压降低后,flash 存储单元的读取裕量变小,同样需要更长的时间来稳定读出数据。如果你在电压档位降低之后,还沿用高压下的等待状态配置,等于让 flash 在更严苛的电压条件下以更快的节奏工作,大概率翻车。
3. 核心细节与实操排查:一步一步定位问题
3.1 先说结论:最容易翻车的三个环节
我这次定位到的问题,最终可以收敛到三件事:一是进入 LRUN 时,MSI 时钟源切换后没有重新配置 flash 等待状态;二是电压档位切换后没有等待电压稳定标志就被下面的代码继续执行,flash 访问发生在电压跌落的中途;三是进入低功耗前没有把 Cortex-M7 的 D-Cache 数据写回,导致唤醒后读完脏数据直接当指令执行。
这三个问题单独拎出来都不难理解,但放在 LRUN 切换序列里,它们互相嵌套,任何一个环节顺序错了都会引起随机性极强的故障。下面我按排查顺序展开,你可以对照自己的代码逐步检查。
3.2 排查步骤:从 HardFault 到根因的五个关键动作
第一步,抓 HardFault 现场。我在 HardFault_Handler 里把栈上的返回地址 LR、PC、PSR 全部转存到一块不被刷新的 SRAM 区域,然后通过一个调试串口在唤醒后把数据打出来。跑了几轮之后,发现 PC 指向的地址范围居然不在代码段内,说明取指已经在 flash 映射区域之外飞了。再结合异常的 CPSR 值,我判断问题不是普通逻辑错误,而是取指路径上的机械性故障。
第二步,核对实际时钟配置。我在进入 LRUN 前后,把 RCC->CFGR、RCC->MSISR 等寄存器的值抓出来对比。正常做法是,进入 LRUN 前应该先把系统时钟切到 MSI,并把 SYSCLK 分频到目标低频率。但我发现自己的代码在切换时钟源时,只是简单调用了库函数,并没有确认 MSI 已经稳定输出就继续往下跑。MSI 从启动到稳定需要一段时间,如果在这个窗口期访问 flash,时序毫无保证。
第三步,对照等待状态配置表。我把当前工作电压档位、当前系统主频、当前 FLASH->ACR 寄存器的 LATENCY 值放在一起,逐个查参考手册的等待状态表,发现一个铁证:进入 LRUN 后,我的代码把 LATENCY 从高主频值只调低了一档,但实际主频已经降到几 MHz,按表格应该直接配 0 等待状态。配置和物理需求差了整整好几拍,这种长时间的过等待倒不会立刻崩,但会导致 CPU 访问 flash 的时序毛刺叠加,累积到一定程度就触发取指异常。
第四步,核查电压档位切换时序。STM32 的电压档位切换在 PWR 模块里有标志位指示。我在切入低电压档后,继续执行下面的代码之前,没有等待电压切换完成标志置位。读寄存器、配置外设,这些操作本身就要从 flash 取指,如果我第一脚踩进去的时候电压还在下探,那次取指就已经埋下了隐患。这一条用逻辑分析仪配合内核事件输出才能看出来,普通单步调试根本捕捉不到。
第五步,把可疑函数搬到 SRAM 里跑对比。为了验证取指路径问题,我把 LRUN 入口函数、LowPower 处理函数全部重新定位到 SRAM 执行,flash 访问在低功耗切换窗口期被彻底避开。改完这一版之后,脱机跑了 12 个小时,一次 HardFault 都没出现。这就反向证明了取指路径上的 flash 时序问题就是罪魁祸首。到此问题定位完成,剩下的就是梳理正确的进入序列。
3.3 进入 LRUN 的推荐配置序列
问题定位之后,我把进入 LRUN 的流程重新梳理了一遍,顺序固定为:先切时钟源并等稳定,再降主频分频,然后切换电压档位并等待稳定,最后重新配置 flash 等待状态,并且关闭预取缓冲或调整其策略。下面是精简后的配置片段,基于寄存器操作展开,方便你理解每一步在做什么:
void enter_lrun_mode(void) { // 1. 先切到 MSI,并等 MSI ready RCC->CR |= RCC_CR_MSION; while (!(RCC->CR & RCC_CR_MSIRDY)); // 2. 配置 MSI 输出频率为目标低频(例如 4MHz) RCC->CR &= ~RCC_CR_MSIRANGE_Msk; RCC->CR |= RCC_CR_MSIRANGE_4MHz; // 重新触发 MSI 频率切换,等待 ready while (!(RCC->CR & RCC_CR_MSIRDY)); // 3. 切换系统时钟源到 MSI RCC->CFGR &= ~RCC_CFGR_SW_Msk; RCC->CFGR |= RCC_CFGR_SW_MSI; while ((RCC->CFGR & RCC_CFGR_SWS_Msk) != RCC_CFGR_SWS_MSI); // 4. 降主频分频:HCLK 也需要同步调整 RCC->CFGR = (RCC->CFGR & ~RCC_CFGR_HPRE_Msk) | RCC_CFGR_HPRE_DIV1; // 5. 切电压档位,等待电压切换完成标志 PWR->CR1 = (PWR->CR1 & ~PWR_CR1_VOS_Msk) | PWR_CR1_VOS_LOW; while (!(PWR->SR1 & PWR_SR1_VOSF)); // 6. 关键一步:根据当前实际主频和电压档位重新配置 flash 等待状态 // LRUN 低频下通常可配 0x0,具体以参考手册等待状态表为准 FLASH->ACR &= ~FLASH_ACR_LATENCY_Msk; FLASH->ACR |= FLASH_ACR_LATENCY_0WS; // 7. 最后才进入 LRUN 模式 PWR->CR1 |= PWR_CR1_LPRUN; }注意第六步的配置顺序,必须放在电压切换完成之后。因为电压档位变了,flash 的读取物理特性就变了,等待状态必须基于最终的电压状态来算。如果按旧电压下的读取速度来配,新的低电压下同样会存在时序风险。这一步也是我最初代码里缺失的关键环节。
3.4 缓存、中断、调试器等容易干扰判断的因素
Cortex-M7 带有 I-Cache 和 D-Cache,这对 XIP 性能至关重要。I-Cache 能缓存 flash 上的指令,但它在硬件上对代码修改是透明的,一般不需要软件干预。D-Cache 就不一样了,如果 LRUN 切换过程中有 DMA 写 flash 或外设写内存的操作,必须保证数据一致性,否则唤醒后 D-Cache 里可能是陈旧数据。
调试器干扰这个坑我必须单独提一句。很多工程师遇到“调试器下正常、脱机必现”的问题,会怀疑是不是硬件接触不良,其实绝大多数时候是调试器改变了时序环境。调试器在线时,内核时钟经常被冻结,或者调试器访问总线时会触发额外的总线周期,这些操作会拉长 flash 访问的实际周期,意外地掩盖了时序缺陷。所以排查这类问题一定要以脱机运行为准,逻辑分析仪、GPIO 电平标记才是可信的观测手段。
4. 常见问题速查与避坑经验
4.1 症状、根因与对策对照表
这一节我把同类问题的高频症状整理了表格,你可以直接按图索骥:
| 症状 | 最可能的根因 | 快速检查方法 | 解决方案 |
|---|---|---|---|
| LRUN 下随机 HardFault,PC 飞到代码段以外 | 时钟切换后 flash 等待状态未更新 | 打印 FLASH->ACR 与参考手册对照 | 重新按最终主频配置 LATENCY |
| 脱机必现、调试器下正常 | 总线访问时序被调试器意外补偿 | 脱机+GPIO标记验证 | 以脱机运行为验证标准 |
| 从 LRUN 唤醒后死机 | 电压切换未完成就访问 flash | 检查 PWR 电压稳定标志 | 等待 VOSF 后再配 flash |
| D-Cache 开启后运行一段时间异常 | LRUN 前脏数据未写回 | 检查是否有 DMA 写入内存 | 进入 LRUN 前执行 D-Cache Clean |
| 中断触发后瞬间 HardFault | 中断入口取指发生在 flash 不稳定期 | 把中断回调放到 SRAM 对比 | 延长电压稳定等待或推迟开中断 |
| 低主频下反而出错,高主频正常 | 等待状态配得过高导致时序毛刺 | 实际频率与 LATENCY 按表格核对 | 降至对应 0WS 或最小等待状态 |
4.2 几个高价值排查技巧和工具
分享几个这次排查中真正提升效率的技巧。第一,在进入 LRUN 的关键节点翻转 GPIO,用逻辑分析仪观测整个切换序列的时长。如果某一步骤的间隔异常长或短,能快速定位是哪一步没有按预期执行。第二,用 DWT->CYCCNT 测量时钟源切换、电压切换等步骤消耗的实际周期数,把结果通过调试串口打印出来,能直接发现库函数内部哪些地方存在死等。第三,把所有中断处理函数临时重定位到 SRAM 执行,如果问题消失,基本可以确定是 flash 取指路径的问题,这个手段隔离性极好。
调这种低功耗 XIP 问题,我的习惯是准备一个小工具函数,能把当前 RCC 时钟源、HCLK 频率、VOS 档位、FLASH->ACR 这些关键参数一次性打包成结构体,在串口里用易读格式输出。遇到问题先打一针“状态快照”,能省掉一半的猜谜时间。
4.3 针对 STM32N657L0 的避坑经验总结
最后分享几条针对 STM32N657L0 这颗芯片的经验。一是它内部 flash 容量大、XIP 执行性能好,但等待状态配置对电压档位的敏感度比老款 M4 芯片更高,别拿老经验套新芯片。二是片上的 ART 加速器或预取缓冲如果使能,切换时钟源后有机会读到预取的旧指令,必要时建议在切频前后刷新或关闭预取。三是硬件上如果还有外部 NOR flash 通过 FMC 接口 XIP,它的时序参数表和片内 flash 是两套体系,不要用同一套配置去套。
还有一点值得强调:LRUN 模式不是“跑着跑着降个频”这么简单,它和 flash 控制器、电源管理、缓存策略、中断窗口是绑在一起的。任何一个环节顺序错了,结果就是这种随机性极强、排查成本极高的故障。我这次花了两天才定位到根因,本质上就是一开始没把“顺序”当作核心约束来审查。
我在实际排查中还发现一个很容易被忽略的点:进入 LRUN 之前,如果开启了全局中断,而某个中断恰好在电压切换标志位等待期间触发,中断入口的取指会直接踩在 flash 最不稳定的窗口上。建议进入低功耗切换序列的代码段,在切换完成之前把中断关掉,或者确保所有中断服务函数都放在 SRAM 里执行,这是成本最低的保险手段。
这个内容后续如果再遇到类似问题,我建议你先去看 ST 针对该系列整理的低功耗应用笔记,里面关于时钟切换顺序和 flash 等待状态的表格,比任何网上的例程都准确。拿到新芯片第一件事,就是把 clock tree、FLASH->ACR、PWR 相关寄存器全部打印出来,确认每一个配置和实际物理条件匹配,能省掉后面大量定位时间。