资讯动态

STM32N6 XSPI2 Abort()坑:Direct-XIP取指后BUSY位不清零的排查与解决

发布时间:2026/8/30 4:27:40 来源:尧图企业网站定制
XSPI2 的 Abort() 让我在 STM32N6 项目里卡了整整一天现象非常诡异Abort() 明明返回成功可状态寄存器里的 BUSY 位纹丝不动后续所有间接模式读写全部超时。而问题只在一种情况下稳定复现——这个 XSPI2 会话真的被 CPU 从映射地址取指执行过。我把排查过程、根因分析和最终能用的几种解法完整记录下来给正在做外部 Flash XIP 方案的同行一个参考。1. 背景交代为什么要在 XSPI2 上跑 Direct-XIP又为什么必须调 Abort()1.1 这个项目的真实场景算法库从外部 NOR Flash 直接执行STM32N6 是 ST 新一代带 NPU 的 MCU很多 AI 推理、音频处理工程都会遇到一个尴尬问题内置 Flash 不够装算法库或者不想在启动时把几百 KB 拷到 SRAM 里浪费时间和内存。最常规的做法就是把算法代码放到片外的 Octal NOR Flash通过 XSPI2 的 memory-mapped 模式直接 XIP 执行。N6 的 XSPI 模块支持最高 8 线 SPI跑跑只读代码完全够用。我当时的方案就是这样系统上电后Boot 代码先把外部 Flash 里的算法镜像校验一遍然后把 XSPI2 配置成 memory-mapped 模式直接把 PC 指针跳过去执行。刚开始听起来很完美实测跑起来也确实是这么回事。但做产品不是只让代码跑起来总会有需求要中途切回间接模式去读 Flash 里的参数、或者做小范围擦写。这时候就躲不开那个要命的函数——HAL_XSPI_Abort()。1.2 模式切换里 Abort() 的常规角色XSPI 外设大体上分成两个阵营间接模式indirect mode和内存映射模式memory-mapped mode。间接模式下每次读取要配置寄存器、发起命令、等 FIFO、读数据一切都由软件显式控制而 memory-mapped 模式下外部 Flash 被映射到 MCU 的地址空间CPU 直接按普通地址访问XSPI 相当于一个透明的桥接引擎。要从 memory-mapped 切回间接模式按常规流程就得先调用 Abort()。HAL 函数库里对 Abort 的解释是“中止当前事务并清除 BUSY 标志”设计意图是让你在两种工作模式之间安全切换。正常场景下比如你刚做完一次间接读或者刚向 Flash 发完一个状态查询命令调 Abort() 都不会有问题函数正常返回BUSY 也正常清除。1.3 session actively used for Direct-XIP instruction fetch 这句话到底意味着什么这个长标题里最关键的一个限定条件就是“the session was actively used for Direct-XIP instruction fetch”。翻译成人话就是XIP 这个会话不是被“配置过”而是被“真正取指执行过”。配置好 memory-mapped 模式但从未跳转过去执行是不会触发这个 bug 的。这个区别特别容易被忽略也正因为如此很多人在排查时会先怀疑自己的配置流程走了不少弯路。2. 复现过程一套非常确定的必现路径2.1 完整的复现步骤按照下面的步骤在 STM32N6 上可以百分之百复现这个问题初始化 XSPI2配置成 memory-mapped 模式挂接外部 Octal NOR Flash。将 PC 跳转到外部 Flash 映射地址执行一段至少几十条指令的函数例如一个简单的加法循环或者算法库入口。确认 XIP 指令确实被执行过哪怕只执行了几条。将 PC 跳回内部 SRAM 的代码区。调用 HAL_XSPI_Abort() 或者直接操作寄存器设置 ABORT 位。读取 XSPI2 状态寄存器的 BUSY 位——发现它一直为 1永远不清零。此时尝试发起任何间接模式读操作全部卡死在等待 BUSY 清零的循环里。这里要特别说明第 2 步里的“执行一段指令”是必要条件。如果只是配置好 memory-mapped 模式然后用指针从映射地址读几个数据CPU 取数据而不是取指令再调 Abort()一切正常。只有指令取指instruction fetch会触发这个行为。2.2 对照实验的结果为了把触发条件摸清楚我做了一组对照实验前置操作调 Abort() 后 BUSY 状态配置 memory-mapped但从未访问映射地址正常清零配置 memory-mapped用 CPU 从映射地址读数据正常清零配置 memory-mapped用 DMA 从映射地址读数据正常清零配置 memory-mapped并从映射地址取指执行BUSY 恒为 1卡死XIP 执行后等待 100ms 再调 Abort()BUSY 恒为 1卡死XIP 执行后关闭 I-Cache 再调 Abort()BUSY 恒为 1卡死这个表格说明问题不是时序竞争不是 Cache 残留也不是简单的未完成数据读取。它和“指令取指”这个动作有非常直接的关联。2.3 卡死之后具体是什么状态卡死时我读取了相关寄存器状态非常有意思ABORT 位是可以写入的说明外设的寄存器访问通道还活着但状态寄存器里 BUSY 一直为 1而且 FIFO 相关的标志位也处于一种非空闲状态。最令人困惑的地方是 HAL 函数返回的是 HAL_OK这意味着 HAL 库的代码路径里认为 Abort 已经执行成功了。它可能没有真正去检查 BUSY 是否归零或者在软件时序上刚好错过了清零窗口但从用户视角看你的外设就是一个半死状态。3. 排查链路从 HAL 源码到参考手册一层层往下挖3.1 第一步先看 HAL_XSPI_Abort() 底层到底做了什么排查这种问题第一步永远是掀开 HAL 库的盖子看它到底在干什么。HAL_XSPI_Abort() 在 stm32n6xx_hal_xspi.c 里的实现逻辑大致是这样先检查外设状态防止已经在 Abort 过程中然后向 CR 寄存器写 ABORT 位接着等待传输完成标志或者超时最后清理相关标志并返回。问题就在这个“等待传输完成标志”上。HAL 等的是它自己定义的“命令完成”条件而 BUSY 位反映的却是外设内部更底层的忙闲状态。在间接模式下两者是同步的在 memory-mapped 模式下这个同步关系就不成立了。所以 HAL 返回成功BUSY 却还挂着根本不是 HAL 库的 bug而是 HAL 的设计模型只覆盖了间接模式下的 Abort 语义。3.2 第二步翻参考手册确认 BUSY 位的真确定义把 STM32N6 参考手册里 XSPI 章节翻出来找到状态寄存器SR的 BUSY 位说明表述大意是“该位用于指示当前是否有 XSPI 传输正在进行”。这个“当前是否有传输正在进行”到底算不算 memory-mapped 模式下的指令取指透传手册并没有给出明确的图解或说明。在实践中我发现在 memory-mapped 模式下只要有 CPU 取指流持续命中外部映射地址XSPI 就会不断向 Flash 发起读命令这个状态对软件来说是不可见的你无法通过寄存器查询到具体有多少个 in-flight 事务。而当你跳离 XIP 区域再调 Abort()理论上已经没有新的取指请求了但外设内部有没有遗留未完成的读命令手册没有覆盖这种场景。3.3 第三步用最小化实验缩小范围怀疑对象逐个排除为了排除 CPU Cache、中断、DMA 这些变量的干扰我做了一个最小化工程关闭 I-Cache 和 D-Cache、关闭所有中断、不使用 DMA只保留 XSPI2 初始化和一段直接放在外部 Flash 里的测试函数。结果问题照旧。这说明问题不在缓存策略也不在中断优先级或者 DMA 配置上。我还试过把外部 Flash 换成不同厂商的片子确认问题与具体 Flash 型号无关。这时基本可以确定是 XSPI2 外设内部状态机在某个特定场景下出现了“无主事务”的残留。3.4 排查过程中的一个小插曲调试器也来添乱排查过程中还有一件值得记录的事。有一阵子连接突然变得非常不稳定IDE 偶尔报错弹出一段提示说调试器存在兼容性问题连接被中止。我一开始以为是自己把外设搞挂了连 MCU 都进入异常状态后来换了一个官方调试器连接稳定了问题照旧。这个插曲的教训是排查底层外设问题时一定要先保证调试工具链可靠否则很容易把调试链路的问题和业务代码的问题混在一起白白浪费时间。4. 根因分析Direct-XIP 取指事务和 Abort() 机制的根本矛盾4.1 两种模式下事务的“归属”完全不同间接模式下你调一个 HAL 函数发起读操作这个事务是软件“拥有”的发起时间、数据长度、结束条件都由软件控制。所以 Abort() 就像一个中断铃它可以让正在进行的操作停下来因为软件有权随时终止自己发起的事务。memory-mapped 模式下就完全不一样了。CPU 访问映射地址时XSPI 自动把总线请求转成 SPI 读事务整个过程是硬件自动完成的。软件没有显式发起某个“传输任务”事务的持续时间由总线上实际的取指需求决定。这种情况下Abort() 就像一个想要叫停一辆自己没叫的出租车的人——它没法真正终止一个不是由软件发起的传输。用公交车道来类比可能更贴切。间接模式是你自己开车可以随时靠边停车XIP 模式更像是公交车专用道你只是乘客车怎么开、什么时候停车由交通系统决定。Abort() 想指挥这个交通系统可惜它没有这个权限。4.2 为什么 Abort() 没把 BUSY 清掉外设内部状态机处在“无主”状态从外设状态机角度理解问题出在事务状态的归属关系上。当 CPU 从 XSPI2 映射地址取指执行时XSPI 内部会预先发起多个突发读请求因为指令流水线需要连续取指。当你跳回 SRAM 执行时这些已经发出的请求中可能还有一部分等待 Flash 返回数据。ABORT 位被置位后外设尝试终止当前事务但它发现自己根本没有“当前事务”的完整上下文——现有的只是总线侧遗留的未完成读请求。状态机无法完成 Abort 的收尾也无法主动终结这些遗留请求于是 BUSY 位就一直保持置位状态。这本质上是一个状态机没有覆盖到的边界情况。4.3 为什么“取指执行过”才触发而“数据读取”不触发这也是最让人好奇的点。我的推测是指令取指和普通数据读在总线行为上差异巨大。指令取指是连续不断的、多事务并发在飞的而普通内存映射读通常是一次性、点对点的每个事务很短前一个完成之后下一个才会发起。当内部有多个 uncompleted outstanding read 时Abort() 到达的时机就可能落在某个事务流中间导致状态机无法安全停止而单个数据读事务要么已经完成要么就是等待时间极短Abort() 几乎总是能找到一个干净的停止点。这也是为什么只有“指令取指流”会让问题变得必然发生。4.4 顺带排查过的安全区/非安全区因素STM32N6 支持 TrustZone 安全扩展XSPI2 外设和它连接的映射地址都涉及安全区与非安全区的访问权限。我一开始也怀疑过是不是安全属性配置影响了外设状态机后来在 GTZC 配置里反复检查确认安全/非安全属性与当前访问者状态完全匹配问题依然存在。这个因素虽然与本问题无关但在排查时必须先排除掉否则两个问题纠缠在一起会更难定位。5. 解决方案实测三种可落地的 Workaround以及我的最终选型5.1 方案 A先“软撤离”再用 Abort()思路是在 Abort 之前确保 CPU 真正确认不再访问 XIP 区域。操作顺序是跳转到 SRAM 代码区执行 I-cache 和 D-cache 的 clean/invalidate然后执行 DSB 和 ISB 指令确保流水线刷新等待总线事务排空再调用 Abort()。实测结果这些操作仍然不能保证 BUSY 清零。即便加了延时、反复清缓存问题依旧复现。原因很简单这些操作告诉你“CPU 这边不会再访问了”但 XSPI 内部遗留的事务依然存在Abort() 还是终结不了它。这个方案适合作为防御性代码的一部分不能作为真正解决方案。5.2 方案 B放弃 Abort()直接做外设级强制复位这是我最推荐的方案。思路是绕开 Abort()直接通过 RCC 对外设做强制复位和释放复位// 强制复位 XSPI2 __HAL_RCC_XSPI2_FORCE_RESET(); // 等待几个时钟周期让外设彻底复位 for (volatile uint32_t i 0; i 100; i); // 释放复位 __HAL_RCC_XSPI2_RELEASE_RESET(); // 重新初始化 XSPI2 全部寄存器 XSPI2_Reinit();这个方案实测稳定解决问题。外设被 RCC 复位后内部状态机完全回到默认状态BUSY 位必然清零之后重新配置寄存器即可继续正常工作。代价是要把 XSPI2 的全部配置重新写一遍包括引脚复用、时钟极性、Flash 参数等。我在项目里封装成了xspi2_force_reset_and_reinit()函数调用处暂时失去 Flash 访问能力但只要保证调用期间没有其他代码访问 XSPI2 即可。5.3 方案 C从架构上避免在 XIP 活跃期调用 Abort()这是釜底抽薪的方案。重新审视产品需求之后我调整了整体设计把需要访问外部 Flash 的操作全部安排在 XIP 会话开始之前、或者结束之后避免在同一个会话中间切换模式。比如启动时先通过间接模式把需要的参数读出来进入 XIP 模式后就不再切回间接模式如果确实需要写 Flash就通过另外的 XSPI 实例或者直接降级到非 XIP 模式运行。这个方案改动量最大但最可靠。因为你不去触碰那个状态机自然不会触发它的边界条件。我在后续版本中正是用了这种思路把 XSPI2 设计成“只做 XIP跑完就复位”彻底绕开了 Abort() 的坑。5.4 实测对比与我的选型建议方案可靠性代码改动量对系统影响推荐场景方案 A软撤离 Abort低实测还是卡小几乎无仅作防御方案 B外设强制复位高稳定可用中等有短暂不可用窗口中途切模式必须用方案 C架构避开最高较大影响设计流程新项目尽量用最终我的选择是方案 B 为主、方案 A 作为辅助同时记录了当前 XIP 会话是否处于活跃状态只有确认不活跃时才允许走 Abort() 路径一旦检测到活跃就直接走外设复位流程。对于新项目建议直接参考方案 C 的设计思路。6. 踩坑之后XSPI 排错的通用经验和注意事项6.1 遇到 XSPI 卡死时的排查顺序这次问题之后我总结出一套针对 XSPI 外设的通用排查顺序分享出来供参考先读状态寄存器确认 BUSY、TCF、TEF 等标志位的具体状态判断是“传输未完成”还是“传输错误”。检查当前外设处于间接模式还是 memory-mapped 模式两种模式下 Abort 行为完全不同。排除 Cache 和流水线的影响关闭 I-Cache、D-Cache 后重新测试。确认中断和 DMA 没有占用当前传输通道。以上都排除后直接试外设级复位对比是否恢复正常。如果复位后恢复基本可以锁定是状态机残留问题而不是引脚或 Flash 硬件问题。这个顺序核心思路是先确认外设内部状态再排除外部干扰最后拿复位当试金石。6.2 安全区/非安全区功能对 XSPI 的隐性影响STM32N6 的 TrustZone 功能把应用分成安全区和非安全区XSPI2 外设本身也有安全属性配置通过 GTZC 控制器设置。如果你的代码在非安全世界里执行而 XSPI2 被配置为安全外设那么访问就会触发总线错误反过来如果你在安全世界里配置了映射地址但实际取指是在非安全世界也可能出现权限错误。这类问题最坑人之处在于报错方式不统一。有时是访问违例直接 HardFault有时则是总线层静默堵塞像极了 BUSY 卡死。所以排查 XIP 问题时我强烈建议先确认两件事当前代码运行在哪个安全状态XSPI2 外设及其映射地址的安全属性是否与之匹配。这个前置确认能帮你排除掉一整类权限相关的问题。6.3 一个关于排查工具的非常实际的小提醒这次经历中调试器不稳定曾在关键时刻给了我错误方向。如果你遇到类似“连接被中止”或者兼容性提示不要急着怀疑自己的代码先检查调试器是否可靠。换一个官方调试器再试往往会发现业务代码本身的问题依然稳定复现——这反而是好消息说明你的排查环境是干净的。另外我强烈建议做一个最小化复现工程把配置精简到只有 XSPI2 初始化、一段 XIP 测试函数、一个 Abort 调用。这种工程的好处是复现速度极快修改任何变量都能立刻看到结果差异而且方便日后拿去给原厂 FAE 沟通时直接复现。回看这个问题的整个排查过程最大的心得就是外设库抽象层会让你误以为它覆盖了所有场景而实际上硬件状态机的边界条件往往藏在文档没有展开的角落里。遇到类似问题不要迷信 HAL 的返回值也不要跳过最底层的寄存器读取和外设复位试金石这些往往才是解决问题的最后一道保险。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价