ARTICLE DETAIL

资讯详情

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

STM32N6 XSPI2 Direct-XIP后Abort卡死BUSY的根因与规避

STM32N6 XSPI2 Direct-XIP后Abort卡死BUSY的根因与规避 做嵌入式这些年最怕碰到的就是“配置看着没问题、逻辑也挑不出毛病、但实际就是卡死”的外设问题。最近在STM32N6上调试XSPI2连接外部NOR Flash的工程时我就遇到了这么个非常刁钻的怪问题只要XSPI2的会话在此之前被实际用于Direct-XIP指令获取之后再调用HAL_XSPI_Abort()去中止操作状态寄存器里的BUSY位就永远为1怎么折腾都清不掉。这篇东西我会完整记录这个问题的复现路径、根因分析和最终落地的解决办法希望能给同样在STM32N6上踩坑的兄弟省点时间。1. 问题现场一个“看似不该发生”的BUSY卡死1.1 触发场景和具体现象先交代工程背景。主控是STM32N6外接了一片支持八线DTR模式的NOR Flash挂在XSPI2上。系统上电后部分启动代码是放在外部Flash里的通过内存映射模式直接执行。也就是说这片外部Flash不光是数据存储还被当作代码存储区CPU会直接从映射地址取指令跑起来。工程里有一个很常规的逻辑每次要对外部Flash做擦写操作之前先调用HAL_XSPI_Abort()确保之前可能挂在总线上的内存映射读操作被中止然后再用间接模式Indirect Mode发擦除或编程命令。这个流程在F4、H7等其他系列上一直工作正常换到STM32N6上第一次完整跑起来就直接翻车。翻车现象非常明确程序执行到HAL_XSPI_Abort(hxspi2)这一步之后没有按预期返回。用调试器暂停发现程序卡在HAL库内部的等待循环里。打开XSPI2外设寄存器SR寄存器的BUSY位始终是1无论等多久都不变化。手动往CR寄存器的ABORT位写1反复触发依然无济于事。整个外设就像是死死钉在了忙碌状态连一次状态切换的迹象都没有。更麻烦的是这个卡死不是偶发。我反复测试了几十次触发条件非常稳定而且带一个非常值得玩味的前提只有当XSPI2的会话在此之前被实际用于Direct-XIP指令获取时Abort()之后BUSY才卡死。如果之前只是普通的内存映射数据读取也就是CPU从Flash映射地址读数据块而不是取指令执行那么接下来调用Abort()一切正常BUSY正常清除函数正常返回。1.2 稳定复现这个问题的两个必要条件根据反复测试要稳定复现这个BUSY不清除的问题需要同时满足两个条件。第一个条件XSPI2必须处于memory-mapped模式并且外部Flash被映射到CPU地址空间里。这其实好理解Direct-XIP本身就是memory-mapped模式的一部分。如果只是用间接模式做读写根本不会走到Abort()这条路径也不存在这个现象。第二个条件也是容易被很多人忽略的就是在这之前的某个时刻CPU确实从XSPI2映射区域取过指令也就是真实发生过Direct-XIP指令获取。注意这里不是“配置了XIP模式”就算而是必须真正有代码从外部Flash取指执行。哪怕这个取指动作已经结束比如代码已经跳回内部Flash执行了只要这个会话曾经发生过Abort()就会出问题。我把这两种情况做了个对比如下表场景之前的会话状态调用HAL_XSPI_Abort()后的BUSYA普通内存映射数据读取data read正常清除HAL返回OKBDirect-XIP指令获取instruction fetchBUSY保持1HAL可能超时或卡死这两个场景的运行环境完全一致唯一的变量就是是否发生过指令获取。这个问题很快成为整个排查工作的关键突破口。2. 根因分析XSPI2的Direct-XIP会话为何能卡死Abort2.1 XSPI2在STM32N6里的角色和工作方式STM32N6是ST新一代高性能MCU主频最高可以到800MHz内部带NPU加速单元面向AIoT、视觉识别这类场景。它的存储接口也做了升级XSPI2负责外接高速NOR Flash或者PSRAM带宽和协议支持比早年的QSPI强不少。XSPI内部支持三种主要工作模式间接模式、内存映射模式、自动轮询模式。其中内存映射模式又支持两种典型用法一种是把外部Flash映射成普通数据区域CPU用load指令读数据另一种是把外部Flash映射成代码区域CPU可以直接从里面取指执行也就是Direct-XIP。在STM32N6的参考手册里XSPI2对外提供一组寄存器来控制传输。间接模式是通过CCRCommand Configuration Register配置命令序列再通过ARAddress Register给定地址数据走DRData Register或FIFO。内存映射模式则不同控制器会把CPU发起的映射地址访问请求自动转换成外部Flash的读命令序列并且直接把数据返回给CPU总线。无论哪种模式所有正在进行的操作都会反映在SR寄存器的BUSY位中。BUSY为1表示控制器正在处理一个尚未完成的操作。间接模式下这个操作有明确边界比如一次读命令发完、数据收完BUSY就自动清除。内存映射模式下边界就变得很模糊因为CPU可能随时发起新的读请求控制器的状态很难说什么时候是真正空闲的。2.2 Memory-mapped和Direct-XIP到底差在哪为什么取指令和读数据会产生完全不同的行为这里要仔细看一看CPU访问外部内存映射区域时控制器内部到底发生了什么。数据读取时CPU发出一个load指令访问映射地址区间。模型上XSPI控制器收到这个读请求转换成符合外部Flash时序的一次读操作拿到数据后通过总线返回给CPU。这个时候控制器的任务是有边界的请求进来、响应出去、事务完成BUSY就清除。即使CPU连续做很多次数据读取每个读请求之间也是相对独立的事务。指令获取时情况完全变了。CPU的指令预取器是贪婪的它会一次性发起很大范围的分组读取而且会尽量提前把后面的指令拿到流水线里。对XSPI控制器来说这意味着读请求可能是连续、突发、没有明显停顿的。更麻烦的是一旦CPU开始从外部Flash连续取指令指令流就在持续消耗控制器资源控制器内部可能始终忙于准备后续的指令数据BUSY位长时间保持为1。如果在这个持续取指的过程中某个代码路径调用了Abort()问题就来了。Abort()的意图很明确——终止当前操作让控制器回到IDLE。但控制器面对的是一个还在源源不断发出取指请求的CPU它不知道该在哪个点停下来。如果CPU的下一条指令还要从外部Flash取那么控制器一旦中止当前事务CPU就没法继续执行整个系统就进退两难。在实际测试中我观察到即使代码已经跳回内部Flash执行很久了只要发生过Direct-XIP取指XSPI控制器内部似乎仍然保留着某种“指令获取会话”状态不会因为取指暂停而自动退出。这时候调用Abort()控制器的原意可能是去中止一个已经不在活跃的取指会话结果整个状态机卡在某个中间态BUSY位就永远停在1了。2.3 Abort()清除BUSY的流程和失败点再往底层说一说。HAL_XSPI_Abort()做的事情拆解起来其实不复杂。它主要做了几件事检查当前有没有操作在跑往CR寄存器写ABORT位然后等待SR寄存器里的BUSY位和FIFO空标志等达到预期状态。HAL库实现里等待BUSY清除是一个循环。如果配置了超时超时时间到了还清不掉函数会返回HAL_TIMEOUT如果配置成HAL_MAX_DELAY也就是无限等待那程序就会卡死在这个循环里。我这次遇到的程序最初配置的就是无限等待所以现象是直接卡死。后面我改成有限超时返回的是HAL_TIMEOUT但BUSY位实测依然是1问题没有消失只是从卡死变成了超时报错。这就说明问题不在HAL库的等待逻辑而在于硬件层面的Abort机制本身就没有把BUSY清掉。也就是说控制器确实收到了ABORT命令但它没有成功完成中止流程。结合前面分析的Direct-XIP会话特性根因就非常清楚了当XSPI控制器正处于或者残留着Direct-XIP指令获取会话状态时ABORT命令无法让控制器从这个状态中正常退出。控制器没有完成中止响应BUSY自然不会被清除。换一个更直白的说法ABORT对间接模式的数据操作是有效的但对Direct-XIP的指令获取会话它的处理优先级不够或者状态机设计上就没有完整覆盖这条路径。2.4 为什么普通数据读取不会触发那为什么普通的内存映射数据读取不会触发核心在于数据读取是有明确边界的。CPU发一个load控制器响应一次事务完成状态回到IDLE。即使CPU连续做很多次数据读取每个读请求之间也是独立的事务Abort()可以在两个事务之间找到一个相对安全的分界点成功中止后续操作然后清除BUSY。指令获取则不同它偏向于持续流式输入。就算某条指令当前不在取指状态控制器的指令预取缓冲区可能仍然保留了大量预取的指令数据等着CPU来消费。如果控制器执行Abort()它就得处理这些已经在缓冲区里的数据而这个清理动作在某些芯片版本上存在缺陷导致状态机无法收敛。这也解释了为什么必须在“会话被实际用于Direct-XIP指令获取”之后问题才会出现。因为只有实际发生了取指才会在控制器内部留下这个流式会话状态。如果只是把区域映射好、配置好XIP模式但CPU从来没有真正从外部Flash取过指那控制器内部也不会产生对应的会话残留。3. 解决与规避方案三种能落地的处理办法根因清楚了接下来就是怎么处理。我最终的方案是组合拳软件上保证调用Abort()之前指令流不在XIP区域同时在Abort()之后做超时轮询和强制复位兜底。3.1 方案一调用Abort()前先让指令流撤出XIP区域核心思路很简单既然Direct-XIP指令获取会话是导致Abort()无法生效的诱因那就保证在调用Abort()的时候CPU不再从外部Flash取指令并且最好让控制器已经退出指令获取会话。最直接的做法是把负责调用Abort()的这个函数放在内部SRAM或者内部Flash里并且用链接脚本属性指定。这样进入这个函数后CPU的取指源就完全在内部存储上不会再向XSPI控制器发出取指请求。__attribute__((section(.ram_code))) void safe_xspi2_abort(XSPI_HandleTypeDef *hxspi) { /* 执行到这里时CPU已经运行在内部SRAM中 */ while (READ_BIT(hxspi-Instance-SR, XSPI_SR_FIFOEMPTY) RESET) { /* 等待XSPI2内部FIFO排空加有限超时防止死等 */ } /* 再执行标准Abort */ HAL_XSPI_Abort(hxspi); }要注意的是仅仅把代码放到内部SRAM还不够。如果链接脚本里还有其他来自外部Flash的代码段当执行到safe_xspi2_abort()之前的跳转时CPU的取指可能刚好停留在外部Flash区域。所以更稳妥的做法是在调用Abort()之前强制刷新CPU的指令流水线和预取缓冲。ARM Cortex-M提供了一些同步指令比如DSB和ISB可以强制流水线中已有的指令操作完成并且清空预取队列。__DSB(); __ISB(); safe_xspi2_abort(hxspi2);加上这两个屏障指令之后再去调用Abort()效果要好很多。实测下来大部分情况下只要确保Abort()调用前指令流已经移到内部存储再执行DSB和ISBBUSY是可以正常清除的。这个方法虽然不是100%保证但成功率很高。3.2 方案二超时轮询加外设强制复位兜底如果软件层面已经尽量规避了但BUSY还是清了很久或者在某些极端情况下依然卡住那就需要一个兜底机制。兜底机制很简单给Abort()调用加超时超时后若BUSY仍为1直接对XSPI2外设做整体复位。STM32N6的RCC对每个外设都有独立的强制复位和释放复位控制位。对XSPI2来说可以通过RCC寄存器强制把整个外设复位然后重新初始化。void robust_xspi2_abort(XSPI_HandleTypeDef *hxspi) { uint32_t timeout 50000; uint32_t sr; /* 先尝试标准Abort */ HAL_XSPI_Abort(hxspi); /* 轮询BUSY带有限超时 */ do { sr hxspi-Instance-SR; if (READ_BIT(sr, XSPI_SR_BUSY) RESET) { break; } } while (--timeout 0); /* 超时兜底强制复位外设 */ if (READ_BIT(hxspi-Instance-SR, XSPI_SR_BUSY) ! RESET) { __HAL_RCC_XSPI2_FORCE_RESET(); __HAL_RCC_XSPI2_RELEASE_RESET(); /* 需要重新初始化XSPI2 */ MX_XSPI2_Init(); } }这段代码里__HAL_RCC_XSPI2_FORCE_RESET和__HAL_RCC_XSPI2_RELEASE_RESET是STM32N6的HAL库提供的宏。强制复位的特点是干脆利落无论状态机卡在哪里外设都会被重置成上电状态。代价是需要重新初始化XSPI2控制器包括重新配置引脚、时序参数、映射地址等。如果应用不允许外设中途被重新初始化那这个方法就不适合用。这里有一个小细节必须注意强制复位会直接复位外设但如果当前CPU仍然从XSPI2映射区域取指令复位之后CPU再去访问映射区域时外设还没初始化好就会触发总线错误。所以强制复位之前一定要确保代码不在XIP区域运行。这也是为什么我把函数放在内部SRAM里的原因。3.3 方案三通过初始化配置规避异常路径我在排查过程里还试过一条路——从源头规避。既然Direct-XIP的取指会话是问题触发条件能不能在系统设计上减少或取消从外部Flash取指的执行方式答案是可以但要付出一定代价。如果外部Flash只用来存数据、存字库、存模型参数不存放可执行代码那么CPU永远不会从XSPI2映射区域取指令也就永远不会触发这个Bug。这样整个系统里对XSPI2的操作路径就只剩下数据读取和间接模式写操作Abort()可以正常工作。不过这个方案并不是所有项目都适用。如果项目确实需要从外部Flash启动、直接执行代码那只能考虑把一部分关键代码搬到内部SRAM或者把启动代码全量加载到内部RAM后再运行。STM32N6的内部SRAM容量还是比较大的对大多数应用来说代码量如果控制在内部RAM能容纳的范围内这个方案是可行的。我实际测过一种折中方案上电启动阶段和引导代码放在内部Flash客户应用的主体代码仍然放在外部Flash执行但每次进入擦写流程时先通过一个跳转函数把执行流切到内部SRAM中的临时代码再执行Abort()。这样既保证了系统可以在外部Flash执行代码又避免了Abort()被XIP会话卡住。这个折中方案的本质是把“调用Abort()的瞬间”与“Direct-XIP活跃会话”在时间上完全错开。只要保证Abort()调用时CPU不在XIP区、XSPI控制器也没有活跃的取指会话问题就从根本上被规避了。4. 调试工具、安全区设置与排查心得4.1 调试连接时的异常提示不可忽视在排查这个问题的过程中还遇到一个和调试工具相关的有趣现象。第一次程序卡死在Abort()里之后我直接按了调试器的复位想重新加载程序。结果调试器连接目标板时工具报了一个类似“not a genuine ST device, abort connection”的异常提示。这个提示一开始让我误以为芯片连接出了问题排查了半天发现根本不是。后来理清楚了这个提示其实是调试器和目标芯片握手阶段的一个异常警告在芯片异常复位、调试会话状态不干净的时候会出现。换句话说因为之前的程序卡死导致芯片处于异常状态调试器连上来时握手时序不干净工具就报了这样一个提示。解决办法是冷复位也就是断开调试器重新连接或者给目标板完全断电再上电让芯片恢复到干净状态。这个现象给排查带来的启发是调试工具的一些底层提示有时候不是工具本身坏了而是目标设备上一步的异常状态残留导致的。遇到这类提示先检查目标芯片是否处于异常死循环、外设是否处于未初始化状态往往比盲目怀疑工具更高效。4.2 安全区和应用非安全区对XSPI2访问的影响STM32N6支持TrustZone安全扩展内部存储和外设都可以配置为安全或非安全属性。XSPI2作为一个对外部存储访问的关键外设同样需要关注安全属性和访问权限的匹配。在排查这个
返回列表