
最近项目里在调STM32N6的板子芯片的800MHz主频和NPU大家关注得比较多但真到落地阶段绕不开的其实是它的外部存储系统。STM32N6没有沿用传统MCU上的FMC或者OctoSPI方案而是换成了一个叫XSPIM的存储管理器配合multiplexed memory configurations来做多设备复用和地址映射。再加上ST这次把安全区和普通应用区的隔离做进了外部存储的配置体系里整个系统的复杂度比以往任何一颗STM32都要高。这篇文章就从XSPIM的工作原理讲起重点拆解multiplexed模式下多设备共享一组I/O时的映射机制以及应用安全区Secure和应用非安全区Non-Secure在XSPIM上怎么划分、怎么配、怎么验证。后面还会给出一套可以照着抄的初始化流程以及我在调试过程中遇到的高频问题和排查思路。适合正在评估STM32N6方案、或者已经拿到样片正在折腾外部Flash/PSRAM的嵌入式工程师。1. XSPIM在STM32N6外部存储体系中的位置与设计逻辑1.1 为什么STM32N6不继续沿用传统的FMC和OctoSPI方案过去用STM32外扩NOR Flash或者SRAM大家习惯用FMC或者OctoSPI。FMC适合并口NOR/SRAM/PSRAM速度快但引脚占用多OctoSPI把8根数据线复用起来跑四线或八线协议能省不少引脚但本质上它还是一个外设CPU要通过AHB/APB总线去访问它而且对多个设备的映射能力很有限。到了STM32N6这一代情况变了。芯片内部有Cortex-M55、NPU、以及大量的DMA通路这些主设备都可能需要访问外部存储。NPU跑推理模型时要读权重M55要从外部Flash取指执行DMA要往PSRAM里搬数据。如果还按老思路做一个简单的SPI控制器总线瓶颈会非常明显多master场景下也缺乏优先级管理。XSPIM在STM32N6里不是一个普通的SPI外设而是一个带缓存、带地址翻译、带多设备映射能力的存储管理单元。外部Flash和PSRAM挂在它下面对CPU、DMA、NPU来说就是一段可以直接访问的memory map地址而不是一个需要不断操作寄存器才能读写的“外设”。这是方案选型上的根本变化。1.2 XSPIM的内部组成与数据流向从功能块上看XSPIM可以粗分为几个部分总线接口、缓存、地址翻译和映射逻辑、协议引擎、以及引脚控制逻辑。总线上来的读请求先经过缓存如果命中缓存就直接返回数据没命中再去访问外部器件。缓存的存在对XIP执行极其关键因为取指有很强的空间局部性没有缓存的话每次取指令都要走外部总线M55的800MHz运行频率完全跑不起来。协议引擎负责把总线请求翻译成具体的SPI/OPI/QSPI时序包括命令阶段、地址阶段、数据阶段的组合。XSPIM支持SDR和DDR两种模式DDR模式下IO线在时钟上升沿和下降沿各采样一次理论带宽翻倍。这个引擎还负责间接访问模式下的命令序列生成比如发JEDEC ID命令、页编程、扇区擦除这些都是靠协议引擎自动处理的CPU只需要往寄存器里填命令内容。地址翻译模块是XSPIM区别于传统SPI控制器的地方。它能把不同的外部器件分配到不同的地址区间每个区间有自己的基地址和大小。这样一片Flash和一片PSRAM可以同时挂在同一组I/O上而CPU访问时只需要正常读写对应的地址完全不用关心它到底访问的是哪个器件。1.3 multiplexed memory的本意一组引脚、多个设备、多套协议所谓multiplexed memory configurations核心就是让多个外部存储器件共享同一组XSPIM引脚通过片选信号和地址解码来区分访问目标。最典型的组合是“一块NOR Flash存代码 一块PSRAM跑数据和堆栈”也有做“双Bank镜像Flash”用于OTA备份的。这带来一个实际好处板级走线大幅简化。传统方案里Flash和SRAM各自占用一组数据线、地址线、控制线走线密度高不说串扰也难控制。XSPIM的multiplexed方案把所有器件的数据线、时钟线、控制线都并联到了同一组引脚上板子上只需要多拉几根片选线。对于顶多8层板、面积有限的产品设计来说这个优势非常直接。但代价是配置和调试复杂度上来了。两个器件共享同一组I/O意味着它们的数据线宽度、协议类型、工作时序必须能够兼容。比如Flash工作在八线OPI模式而PSRAM只支持四线SPI模式那么整组引脚的线宽就不能简单设置为8。这种情况在配置XSPIM时就需要用multiplexed配置来处理而不是所有外设都套同一套参数。2. multiplexed memory配置的原理与映射方法2.1 地址映射的基础基址、大小与对齐规则XSPIM会把外部存储映射进系统地址空间这个映射关系由每个CS通道的基址和窗口大小决定。配置基址时有一个硬性要求窗口起始地址必须按窗口大小对齐。举个例子如果给CS1配置的窗口大小是16MB那CS1的基址就必须是16MB的整数倍比如0x70000000、0x71000000这样不能是0x70000100这种随意地址。这个对齐要求背后是地址解码逻辑的简化。外部器件本身不要求地址对齐但XSPIM内部通常用高位地址来做CS选择低位地址直接透传给外部器件。如果两个CS窗口不对齐高位选择逻辑就会产生歧义可能出现两个器件都能响应同一地址区间的情况。我一般建议在系统层面先做一张地址规划表把所有需要映射的外部器件列出来明确各自的基址、大小、安全属性之后再动手写配置代码。地址规划表这件事看着不起眼但实际项目里因为地址重叠导致访问错乱的案例相当多。2.2 静态映射与动态映射的适用场景XSPIM的multiplexed配置通常可以工作在静态映射和动态映射两种模式下。静态映射的意思是每个CS窗口在初始化时一次性配置好后续不再改变。适合Flash和PSRAM同时挂载、且两个器件的工作模式相对固定的场景。系统上电后Flash窗口和PSRAM窗口各自就位CPU直接访问即可。动态映射则更适合“多个器件共用同一个窗口、按需切换”的场景。比如板上有两块相同接口的Flash系统运行期间只需要访问其中一块另一块作为离线备份。为了节省地址空间可以把两块Flash映射到同一个窗口访问前通过控制寄存器的bit来切换当前生效的CS。这种用法类似老式PC上的bank switch本质上是用地址空间换电路简化。选择哪种模式主要看应用需求。如果两个器件需要同时出现必须用静态映射如果器件之间是“二选一”或“多选一”的关系动态映射可以省下地址空间。我自己做产品时更倾向于静态映射因为动态映射的切换操作需要额外的软件同步时序上如果不加保护容易在切换过程中产生总线错误。2.3 多协议复用与I/O线宽配置multiplexed配置里还有一个容易被忽略的点就是多个器件的协议模式可能不一样。拿常见的FlashPSRAM组合来说Flash用8线OPI时带宽最高但PSRAM往往只支持4线SPI模式。如果整组总线按8线配置PSRAM就会挂掉如果按4线配置Flash的性能优势又发挥不出来。实际工程里解决这个问题通常有两种做法。第一种是选用支持相同线宽的器件组合都走8线DDR协议这样XSPIM的multiplexed配置最简单所有参数统一。第二种是允许不同器件使用不同线宽每个CS独立配置协议参数XSPIM需要能够感知当前访问的是哪个CS并自动切换对应的协议引擎配置。我建议在设计选型阶段就把这个问题解决掉不要等到画完板子才去操心。先确定两个器件分别工作在什么协议模式再确认XSPIM是否支持这种组合。如果XSPIM的协议引擎是全局共享的那么两个器件就必须用同一套线宽和时序参数设计上只能按最低性能的那个器件来妥协。3. 安全区与非安全区划分XSPIM如何配合TrustZone3.1 TrustZone对存储安全属性的基本要求STM32N6是带TrustZone的M55内核系统里的内存和外设都要区分安全属性。安全属性分为Secure安全和Non-Secure非安全安全代码可以访问非安全资源但非安全代码不能访问安全资源一旦越界访问就会触发总线错误或异常。这对XSPIM来说意味着什么外部存储并不天然具备安全属性它只是挂在XSPIM下面的一块物理存储介质。安全属性是由XSPIM的地址翻译逻辑或系统级安全控制器附加给外部存储地址区间的。也就是说映射在0x70000000的Flash区域可以被配置为Secure也可以被配置为Non-Secure取决于你在系统层面给这个地址区间打了什么样的安全标记。这个机制的实际价值在于你可以把外部Flash划分成两个区域靠近基址的Block用于存放安全固件和密钥剩余区域存普通应用代码。XSPIM访问前先检查目标地址的安全属性如果非安全代码试图读取安全区域中的敏感数据访问会被直接拒绝。3.2 应用安全区和应用非安全区的划分规划在STM32N6上应用安全区和应用非安全区的划分通常不是“物理器件”级别的划分而是“地址区间”级别的划分。同一片Flash里前1MB划给安全区后面的8MB划给非安全区这都是可以做到的。安全区里放什么安全启动代码、密钥存储、关键固件模块、以及需要被非安全代码调用的安全服务的实现。这个区域里的代码和数据必须对非安全侧完全隔离运行时只能通过安全区提供的入口函数来调用。非安全区则放RTOS、业务逻辑、通讯协议栈这些非敏感内容。入口函数是另一个关键设计。非安全区要调用安全区的功能不能直接跳转到安全区的任意位置必须经过NSCNon-Secure Callable区域。NSC区域是设在安全区边界上一个特殊的“可安全调用”窗口里面存放跳转指令这样非安全代码可以有控制地进入安全代码但又不能随意访问安全区的其他部分。3.3 XSPIM上配置安全属性的实操要点外部存储的安全属性在哪一层配置取决于具体的系统架构和XSPIM的设计。常见做法是在系统级安全控制器/内存保护控制器里把XSPIM映射出来的地址区间标记为Secure或Non-Secure同时在XSPIM的内部配置里也需要同步设置每个CS窗口的安全属性。实操时比较稳妥的顺序是先把XSPIM的地址映射配置完成确认两个CS窗口的基址和大小都对了再配置安全控制器把需要保护的区域标记为Secure最后再划分NSC区。初始化顺序反了会出现一个很隐蔽的现象代码明明写在安全区里启动时却跑不起来因为安全属性还没有生效系统已经把某些地址段当成非安全区在访问了。另一个需要注意的点是安全属性的粒度。安全控制器通常以一定大小的块为单位来标记比如每4KB一个安全块。这意味着划分安全区时起始地址和结束地址都要对齐到这个粒度。如果安全区的起始地址没有对齐实际生效的安全区域边界和你规划的会对不上后续排查时非常头疼。4. XSPIM初始化、读写与XIP启动的完整流程4.1 硬件设计与引脚规划在动软件之前先确认硬件引脚连接。XSPIM使用一组专用引脚包括时钟、数据线、片选和复位控制具体AF功能需要查阅对应型号的数据手册。布局布线时需要注意信号完整性时钟频率较高时数据线和时钟线的长度差尽量小片选信号要靠近芯片端上拉避免在高阻状态下悬空导致误触发。时钟源的选择也要在初始化之前想清楚。XSPIM的时钟源可以选择芯片内部PLL输出外部Flash和PSRAM的最高频率各不相同必须根据最慢的器件来限制XSPIM时钟频率。这个频率不仅影响读写速度也影响时序参数的取值范围。起步阶段建议先保守地用一个较低频率确认基础读写没问题后再往上拉。4.2 初始化代码的基本流程初始化XSPIM的典型顺序是这样的使能XSPIM时钟和对应GPIO时钟、配置GPIO复用为XSPIM引脚、配置XSPIM全局参数、配置各CS窗口的映射地址、用间接模式读取外部器件ID、验证器件类型和容量、然后启用memory-mapped模式并测试XIP执行。下面的伪代码说明了初始化顺序但寄存器名和具体配置字段以实际芯片参考手册为准/* 1. 使能时钟 */ __HAL_RCC_XSPIM_CLK_ENABLE(); __HAL_RCC_GPIOB_CLK_ENABLE(); /* 2. 配置GPIO复用为XSPIM功能 */ configure_xspim_pins(); /* 3. 配置XSPIM全局参数时钟分频、时序 */ xspim_handle.Init.ClockPrescaler 2; xspim_handle.Init.MemoryMode XSPIM_MEMORY_MODE_MAPPED; xspim_handle.Init.Protocol XSPIM_PROTOCOL_OPI_DDR; HAL_XSPIM_Init(xspim_handle); /* 4. 配置CS1窗口Flash基址0x70000000大小16MB */ configure_cs1_window(0x70000000, 16 * 1024 * 1024); /* 5. 配置CS2窗口PSRAM基址0x71000000大小4MB */ configure_cs2_window(0x71000000, 4 * 1024 * 1024); /* 6. 间接模式读取设备ID确认连通性 */ uint8_t jedec_id[3]; read_device_id(jedec_id); /* 7. 擦除Flash并写入测试数据 */ erase_flash_sector(0x70000000); program_flash_page(0x70000000, test_data); /* 8. 启用memory-mapped模式执行XIP测试 */ enable_memory_mapped_mode();注意上电后如果直接切换到memory-mapped模式就去取指但设备ID都还没验证一旦硬件连接有问题CPU会立即跑飞。我自己调试时的习惯是老老实实把“读ID-擦除-编程-回读校验”这个过程走一遍确认外部器件是好的再开XIP。这个步骤看着繁琐却能省下大量排查时间。4.3 XIP执行与缓存一致性的处理XIPexecute in place是从外部Flash直接取指执行这是STM32N6启动和运行的主要方式之一。为了提升取指性能XSPIM内部缓存会缓存外部Flash的内容。这里就引出了缓存一致性问题如果CPU执行代码的同时程序本身通过间接模式去写那些已经被缓存过的Flash地址缓存里存的是旧数据后续取指就会读到过期内容。实际工程中常用的办法是把Flash的在线编程操作全部通过间接模式完成并在编程完成后对相关缓存行做无效化操作。对于DMA和NPU访问外部存储的场景这套缓存管理逻辑同样适用。配置初期没有好好管理缓存出现“代码烧进去了跑起来却是旧逻辑”的怪现象时优先怀疑缓存问题。4.4 多设备模式下动态映射的配置示例多设备复用搭配动态映射时初始化流程稍显复杂。先在系统初始化阶段把两个Flash都配置到同一个窗口然后通过动态映射切换寄存器选择当前访问的是哪一块。切换操作前需要确保总线上的访问已经结束最好配合总线空闲标志来检查。/* 选择Flash A为当前设备 */ xspim_select_device(FLASH_A); /* 从Flash A读取启动配置 */ load_boot_config_from_current_device(); /* 完成A侧配置后切换到Flash B验证其内容 */ xspim_select_device(FLASH_B); verify_flash_b_integrity(); /* 切回Flash A继续运行 */ xspim_select_device(FLASH_A);这个流程在OTA升级场景里非常实用。系统从Flash A启动固件包下载完成后写入Flash B校验通过后把启动标志切到Flash B下次复位就从B启动。整个升级过程不需要操作外部物理开关全靠XSPIM的地址切换完成。5. 实际调试中的高频问题与解决办法5.1 现象外部Flash的JEDEC ID总是读不出来这是非常常见的起步问题。硬件连接正常但返回的设备ID是全FF或者全00。排查方向按优先级排序先确认GPIO复用是否配置正确XSPIM的引脚是否被其他外设占用再检查XSPIM时钟是否启用时钟分频是否导致频率过高最后检查协议模式是否选对比如器件是OPI协议但配置成了SPI模式。有个细节容易忽略有些Flash在连续读ID模式下需要先发一个Exit命令否则第二次读取时会在错误的状态机里出不来。复位后第一次读取通常正常第二次开始异常这种问题通常是器件状态机没有正确复位导致的。5.2 现象进入memory-mapped模式后执行代码跑飞或HardFault这个问题涉及的因素比较多。最常见的是时序参数太紧外部Flash的读时序无法在配置的时钟频率下完成数据采样。先把时钟分频调大再放宽读时序参数逐个排查。其次是缓存问题。代码烧进Flash后如果XSPIM缓存和Flash实际内容不一致会出现看似随机的跑飞现场。把缓存无效化后再执行问题就消失基本可以锁定是缓存陈旧数据导致的。第三个因素不要忽略安全属性配置和实际代码所在区域的Secure/Non-Secure属性对不上也会触发HardFault。比如代码在Secure区域但系统和XSPIM配置把这段地址标记成了Non-Secure非安全代码去访问安全资源总线错误就出来了。5.3 现象两个设备都挂上了但只有一个能访问成功这种问题通常发生在multiplexed配置下。先看两个CS窗口的基址和大小是否重叠地址解码是否发生冲突。再看XSPIM片选信号的逻辑电平是否正确有些器件的片选是低有效有些是低有效配置错了会导致器件从不响应。还有一种情况是时序参数在两个设备之间不兼容。Flash和PSRAM的高速参数往往差异很大如果XSPIM只有一套全局时序参数就必须按严格的那个器件来设置。配置完成后先分别用indirect模式单独访问每个器件确认各自连通性再启用memory-mapped模式。5.4 现象NPU访问外部存储性能远低于预期STM32N6上的NPU需要从外部Flash或PSRAM读取模型权重如果权重存放在外部存储且未配置好缓存性能会非常难看。先确认XSPIM是否工作在DDR模式带宽是否被协议配置限制。再检查缓存命中率实际性能是否符合模型运行需求。另一个容易出问题的是系统总线的优先级配置。CPU取指、DMA传输、NPU权重读取可能同时竞争XSPIM的访问权限如果总线仲裁优先级配置不当NPU的读取延迟会大幅增加。调这类问题时我一般先抓系统负载看各个master的比例再针对性地调整总线的优先级策略。5.5 常见问题速查表现象首要排查方向次要排查方向JEDEC ID读取异常GPIO复用与时钟协议模式和器件状态机XIP执行跑飞或HardFault时序参数与时钟分频缓存与安全属性多设备只有一个可访问地址映射重叠片选配置与全局时序NPU外部存储性能低XSPIM工作模式总线仲裁与缓存命中率6. 写在最后一些调试心得XSPIM这套东西说实话第一次接触的时候会觉得配置项多到让人头晕。但真正吃透之后会发现它的设计逻辑其实很清晰先把物理层的引脚和时钟搞定再把逻辑层的地址映射和安全属性搞定最后才谈得上性能优化。顺序反了后面全是坑。我在实际调试中有几个习惯这里分享给读者。第一每次换板子或者换焊接好的存储器件第一步永远是读JEDEC ID这一步能确认硬件链路和最基本的软件配置是否正确比直接烧代码然后跑飞了再抓救命效率高得多。第二multiplexed模式下两个器件的时序差异一定要在系统设计阶段就评估好硬件板回来后发现时序参数没法同时满足就只能在频域上妥协这个代价往往是实打实的跑分损失。第三安全区非安全区的划分不要只看当前功能要把后续可能的固件升级、密钥管理、权限收回都考虑进去因为外部存储上的安全属性调整涉及到整个启动链路的修改越晚改越被动。最后再说一个关于multiplexed配置的小技巧在系统调试初期不要一上来就启用动态映射。先把每个器件配置成独立的静态窗口把每个器件分别调通再考虑如何合并窗口和切换。这样即使后面切换出了问题也能快速回退到静态映射不至于整个系统卡住无法启动。