
我最近在调试一块 STM32N6 的板子板载两颗八线 PSRAM每颗 64MB总容量 128MB用来给 NPU 跑模型和图片缓冲。本来在 CubeMX 里配置 XSPI 是很常规的操作可当我打开外设配置页面把 Memory Type 选成 PSRAM、把设备容量设成 64MB×2 之后却惊奇地发现文档里提到的“EXTENDMEM”功能根本没有对应开关。翻了半天界面既没有按键也没有下拉框甚至不知道这个功能该在哪里打开。这个问题在 STM32N6 刚发布那阵子几乎人人会遇到尤其是在用 CubeMX 生成工程的时候。很多人会怀疑是不是芯片不支持或者得换更高配的型号。但实际上芯片本身是完全支持扩展内存模式的问题只出在 CubeMX 工具的 GUI 层——它暂时没有暴露这个功能的配置入口。这篇文章就把这个事情的来龙去脉、底层原理以及绕开 UI 限制、手动补齐 EXTENDMEM 初始化代码的完整方案讲清楚。适合正在调 STM32N6 外扩 PSRAM 的嵌入式工程师、AI 部署方向的技术人员以及所有被 CubeMX 某些“隐藏功能”坑过的朋友。1. 先从“这块芯片到底能装多少货”说起1.1 128MB 外部 RAM到底给谁用STM32N6 不是一款普通的 MCU。它有一颗 Cortex-M55 内核主频能跑到 800MHz 量级另外还带了一个叫 Neural-ART 的 NPU 加速器。官方定位就是往端侧 AI、视觉识别、音频处理这类场景走。可是 AI 模型这东西大家心里都有数——权重、激活值、中间特征图哪个都吃内存。片内 SRAM 再多也就那么几百 KB 到几 MB真正跑一个像样一点的分类网络或目标检测网络内存立刻捉襟见肘。所以外挂 PSRAM 就成了刚需。两颗八线 PSRAM 堆出来 128MB 的可寻址空间能干的事情一下就多了把神经网络模型权重直接加载到外部 PSRAM 里运行时由 NPU 通过 AXI 总线访问做摄像头采集的帧缓冲存多帧图像做预处理、后处理、图像拼接音频领域可以缓存大段波形数据做降噪、关键词识别甚至可以把部分应用代码放到外部 PSRAM 执行腾出片内 Flash 和 SRAM 给高优先级任务。我在实际项目中的应用场景是跑一个 YOLO 类的轻量检测模型外加双摄输入。模型权重约 20MB两路 720p 的帧缓冲加起来大约 3.7MB再加上 NPU 推理时的中间张量内存峰值跑到 40MB 以上。如果没有这 128MB PSRAM片内内存早爆了项目根本走不下去。1.2 CubeMX“没有开关”的真正含义很多人一看到 CubeMX 里没有对应配置项第一反应是“芯片不支持这个功能”。这是完全可以理解的毕竟绝大多数人在使用 MCU 时会把 CubeMX 当作一种官方“权威”的配置参考。但事实上CubeMX 只是一个图形化配置代码生成器它不可能做到把所有芯片寄存器的所有功能都映射到 GUI 上。尤其是一些较新的型号、刚发布的系列CubeMX 的更新节奏往往会落后于芯片手册的更新节奏。STM32N6 的 EXTENDMEM 就属于这种情况。查看参考手册时你会发现 XSPI 外设确实有扩展内存模式的描述可以通过修改特定寄存器和初始化序列来启用。但在 CubeMX 的图形界面中ST 工程师可能因为 UI 规划或者是初期版本遗漏没有把这一项加进去。而 HAL 驱动库中对应的函数其实已经写好了只是需要你在用户代码区手动调用。这就引出一个非常核心的嵌入式工程思维工具可以用来提升效率但工具不等于权威。芯片手册、参考手册、HAL 驱动源码才是最终依据。GUI 里没有入口不代表这个功能不存在很多时候是用代码补齐即可。2. XSPI 的 EXTENDMEM地址映射、八通道和双片选2.1 外部存储地址空间与 AXI 总线要理解 EXTENDMEM先要把 STM32N6 的外部存储地址映射看清楚。芯片内部有一颗 AXI 互联总线CPU、DMA、NPU 都挂在上面。XSPI 外设作为 AXI 总线上的一种从设备通过地址窗口将外部存储映射到系统内存空间中。在大多数 STM32 高端型号中XSPI/OSPI 的外部映射地址通常位于 0x70000000 到 0x9FFFFFFF 这个区间。不同芯片的具体基地址略有差异。比如在 STM32H7 上XSPI1 默认映射在 0x90000000XSPI2 映射在 0x70000000。STM32N6 也沿用了类似的思路只是具体基地址需要以你手上的参考手册内存映射表为准。EXTENDMEM 这个功能往往和地址窗口的选择有关。标准模式下一个 XSPI 接口可能只映射一部分存储大小扩展模式下可以映射更大的存储容量或者映射多个外部设备。STM32N6 搭配两颗 PSRAM、总容量 128MB 时如果不启用扩展模式可能只能访问到前 64MB甚至会出现地址回绕、访问异常。启用后整个 128MB 地址空间才会被完整暴露给 AXI 总线。从实际使用体验来看启用扩展模式后CPU 可以直接用指针操作外部 PSRAM就像操作一个普通的 uint8_t 数组一样方便。NPU 和 DMA 也不需要对 XSPI 有特殊了解直接按照内存地址访问即可。2.2 Octal PSRAM 与 XSPI 接口参数八线 PSRAM也就是 Octal PSRAM本质上是 HyperRAM 或类似 APMemory 生产的 APS256XXN、ISSI 的 IS66WVS4M8BLL 这类芯片。它们使用 8 根数据线DQ0-DQ7可以工作在 SDR 或 DDR 模式常见的最高时钟频率可以到 100MHz 甚至更高配合 DQS 信号来同步数据采样。单颗容量可以做到 64MB、128MB 甚至更大。XSPI 外设与这类 PSRAM 通讯时需要配置几个关键参数数据线宽度八线模式即 Data 8 bits时钟极性CPOL和时钟相位CPHA不同 PSRAM 厂商的时序需求可能略有不同一般遵循数据手册中的推荐值DQS 延时DDR 模式下需要调整 DQS 采样窗口确保数据能够稳定读取片选极性有的 PSRAM 芯片低有效片选配置时必须匹配。这些参数在 CubeMX 中是可以配置的因为 XSPI 本身作为一个外设是正常的问题只是出在扩展内存模式的使能入口上。所以你可以先通过 GUI 把基础通讯参数配好再在代码里补扩展内存初始化。另外提醒一句PSRAM 和 NOR Flash 虽然都用 XSPI 接口但命令集和时序差异很大。配置时 Memory Type 一定要选对选成了 Flash 就会导致 PSRAM 无法正常应答读写全失败。2.3 双片选下如何合出 128MB关于两颗 PSRAM 的接法有两种常见方案。第一种方案是使用两个独立的 XSPI 接口每颗 PSRAM 各挂一个接口分别映射到不同的地址区间。这种方式的好处是总线压力分散两个接口可以并行访问速度更快。缺点是引脚占用多初始化代码也更复杂。第二种方案是使用同一个 XSPI 接口的双片选CS0 和 CS1两颗 PSRAM 共用数据线和时钟线通过不同的片选信号来区分访问目标。这样引脚占用少但同一时刻只能访问一颗芯片带宽受限。STM32 的 XSPI 支持 Dual Mode即通过地址位的区分自动在两个片选之间切换。EXTENDMEM 在这种方案下尤其重要因为你需要让整个 128MB 地址空间在两个片选间无缝切换而不是只访问其中一个。就我个人的项目来说我使用的是第二种方案一个 XSPI1 接口、CS0 接 PSRAM0、CS1 接 PSRAM1。这样在 CubeMX 里配置一套 XSPI 外设就够引脚也省。调试完成后访问 0x90000000 到 0x90000000128MB 的地址范围系统会自动按照地址的高位切换到对应的片选。3. 实操配置CubeMX 生成骨架手写代码补 EXTENDMEM3.1 CubeMX 侧需要做的配置在动手写代码之前先把 CubeMX 里的基础配置做正确。这里的思路是让 CubeMX 先搭好 XSPI 外设的大框架等代码生成后我们再在用户代码区补齐 EXTENDMEM 的特定配置。我以 STM32CubeMX 6.10 左右版本 STM32N6 固件包为例配置步骤如下。首先在 Pinout Configuration 页面中找到 XSPI1使能它。然后配置引脚把相应的 CLK、DQ0-DQ7、DQS、CS0、CS1 引脚分配到目标 board 的 PSRAM 接线引脚上。引脚分配错误非常常见尤其是 DQS 和 CS 引脚接反的时候初始化代码看起来没什么问题但读写时数据就是乱的。接着在 Parameter Settings 里配置Memory Type 选 PSRAMData Size 选 8-bitDual Mode 至少选 Enabled如果使用双片选Clock 频率按 PSRAM 手册确定一般先保守一点例如 50MHz 起步按照 PSRAM 的 datasheet 设置 Read/Write Dummy Cycle 等参数。这些参数生成后CubeMX 会生成MX_XSPI1_Init()函数主要是把外设的基本配置写到寄存器里。但这时候你会发现生成的代码中并没有任何关于扩展内存初始化的部分。这正是我们需要补充的内容。3.2 生成工程后添加扩展内存初始化代码CubeMX 生成的工程里MX_XSPI1_Init()之后一般还需要手动调用HAL_XSPI_AdvancedMemoryMappedInit()将 XSPI 切换到内存映射模式。对于普通的 XSPI Flash这一步就已经足够。但对于扩展内存模式、也就是需要完整访问 128MB PSRAM 的情况还需要再做一些寄存器配置。先贴一下典型的基础配置代码void MX_XSPI1_Init(void) { hxspi1.Instance XSPI1; hxspi1.Init.FifoThreshold 4; hxspi1.Init.DualMode XSPI_DUALMODE_ENABLE; hxspi1.Init.MemoryType XSPI_MEMORYTYPE_PSRAM; hxspi1.Init.DeviceSize XSPI_DEVICESIZE_64MB; if (HAL_XSPI_Init(hxspi1) ! HAL_OK) { Error_Handler(); } }这里有一个非常容易忽略的坑DeviceSize 字段。CubeMX 的界面上如果只让你设置一个设备的大小默认可能只有 64MB。但我们要访问的是两颗 PSRAM 合计 128MB扩展模式下地址空间必须是 128MB否则 CPU 访问到后半段时地址会被截断或回绕导致访问出错。所以需要手动把地址空间相关的寄存器调整到 128MB。具体来说在 HAL 初始化之后对 XSPI 的 DCR 寄存器Device Configuration Register做一次扩展配置把 DEVSIZE 位段设置为对应 128MB 的值。如果你使用的是 HAL 层的接口就需要对hxspi1.Init.DeviceSize做修改或者直接写寄存器。接下来是内存映射模式启动static void XSPI_StartMemoryMappedMode(void) { XSPI_AdvancedMemoryMappedInitTypeDef memcfg {0}; memcfg.TimeOut 0; memcfg.WriteThreshold 4; memcfg.CSHighTime 1; memcfg.InterruptEnable XSPI_INTERRUPT_ENABLE; memcfg.ABDEnable XSPI_ABD_ENABLE; if (HAL_XSPI_AdvancedMemoryMappedInit(hxspi1, memcfg) ! HAL_OK) { Error_Handler(); } }在 STM32N6 的 HAL 驱动里XSPI_AdvancedMemoryMappedInitTypeDef封装了内存映射模式下的大量时序配置。其中 WriteThreshold 和 CSHighTime 的值直接影响写入性能建议根据自己的 PSRAM 时序参数手动调整不要照抄默认值。如果需要显式配置扩展内存使能相关的寄存器可以在HAL_XSPI_AdvancedMemoryMappedInit()调用之前直接对 XSPI 的控制寄存器进行读写操作。例如 XSPI_CR 中可能会有用于启用扩展地址解码的位段打开后整个 128MB 才能被完整映射。这里我额外说明一下不同芯片版本寄存器的名称和位段定义可能有差异。你在写代码前一定要打开 STM32N6 的参考手册找到 XSPI 寄存器的描述并根据具体的位段定义填写。我上面提到的思路是一个通用流程具体位名要以手册为准。3.3 一个可运行的最小例程为了让整个流程更直观我写一个最小可运行的测试例程。这个例程做的事很简单在 main 函数里初始化 XSPI然后向扩展内存区域写入一组数据再读回来校验最后点一个 LED 表示校验通过。#include main.h XSPI_HandleTypeDef hxspi1; /* 映射后的外部PSRAM基地址具体以N6手册为准 */ #define PSRAM_BASE_ADDR 0x90000000UL #define PSRAM_TEST_SIZE (16 * 1024) /* 16KB测试区域 */ void Error_Handler(void) { while (1) { /* 错误处理挂死并留出调试位置 */ } } void MX_XSPI1_Init(void) { hxspi1.Instance XSPI1; hxspi1.Init.FifoThreshold 4; hxspi1.Init.DualMode XSPI_DUALMODE_ENABLE; hxspi1.Init.MemoryType XSPI_MEMORYTYPE_PSRAM; hxspi1.Init.DeviceSize XSPI_DEVICESIZE_128MB; /* 扩展模式总容量128MB */ if (HAL_XSPI_Init(hxspi1) ! HAL_OK) { Error_Handler(); } } static void XSPI_StartMemoryMappedMode(void) { XSPI_AdvancedMemoryMappedInitTypeDef memcfg {0}; memcfg.TimeOut 0; memcfg.WriteThreshold 4; memcfg.CSHighTime 1; memcfg.InterruptEnable XSPI_INTERRUPT_ENABLE; memcfg.ABDEnable XSPI_ABD_ENABLE; if (HAL_XSPI_AdvancedMemoryMappedInit(hxspi1, memcfg) ! HAL_OK) { Error_Handler(); } } int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_XSPI1_Init(); XSPI_StartMemoryMappedMode(); uint32_t errors 0; volatile uint8_t *psram (volatile uint8_t *)PSRAM_BASE_ADDR; for (uint32_t i 0; i PSRAM_TEST_SIZE; i) { psram[i] (uint8_t)(i 0xFF); } for (uint32_t i 0; i PSRAM_TEST_SIZE; i) { if (psram[i] ! (uint8_t)(i 0xFF)) { errors; } } if (errors 0) { HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET); } else { Error_Handler(); } while (1) { } }这个例程虽然简单但已经把关键点全部涵盖XSPI 初始化、扩展到 128MB 地址空间、内存映射模式启动、映射后直接像访问数组一样读写 PSRAM。3.4 验证是否成功读写测试与性能验证代码跑起来之后不要只看 LED 亮没亮。我强烈建议你做几轮更深入的验证确认扩展内存真的稳定可用。第一全地址空间扫描。16KB 的测试区域太短只能验证基础通路。你应该对整个 128MB 空间做一次大步长扫描比如以 4KB 为步长在每一页写入特征值并读回校验。这样能确认两个片选之间的切换是否正常以及扩展映射是否覆盖了整个区域。第二CPU 直接访问的速度测试。使用 DWT 计数器或者简单的 timing 函数测量连续读写 1MB 数据的时间计算出有效带宽。我实测在 50MHz 时钟下八线 PSRAM 的连续读带宽大概在 20-40MB/s 量级写入会略低一些。如果测出来的数字明显偏低基本可以判断延迟参数配置有问题。第三NPU 实际推理测试。内存读写正常只是第一步真正要在 NPU 上跑模型还需要确认 NPU 的 AXI 访问路径能正常到达 PSRAM。建议将模型权重所在的缓冲区地址配置到 PSRAM 区域跑一轮推理看看是否有识别错误、内存保护异常或总线错误。这一步往往能暴露 CPU 访问正常但 NPU 访问不正常的问题。4. 常见问题与排查技巧4.1 复位后外部 PSRAM 读不到数据这个问题非常常见。现象是程序烧进去后第一次跑一切正常但只要按一下复位键外部 PSRAM 里的数据就全丢了或者直接访问失败。要知道 PSRAM 的本质是易失性存储器断电后数据必然丢失。但复位不代表断电有时候原因是复位后 XSPI 外设回到默认状态还没有重新初始化CPU 就马上通过 AXI 去访问映射地址了。这时候访问到的是一块未初始化的外部设备大概率会触发总线错误或读到垃圾数据。排查思路是确认系统启动流程中 XSPI 的初始化发生在任何外部内存访问之前。如果你在 startup 文件或存储控制器初始化里动了手脚必须保证先MX_XSPI1_Init()再进入内存映射模式最后才允许 CPU 访问 PSRAM 地址区域。另外有一种情况是PSRAM 本身有上电初始化延迟。有些芯片上电后需要几个毫秒才能接受外部命令。此时如果 XSPI 初始化太早芯片还没准备好后续访问就会失败。解决方法是延时一段时间再启动内存映射模式或者在板子上确认 PSRAM 的供电时序是否满足手册要求。4.2 两个片选之间互相干扰使用双片选方案时CS0 和 CS1 都由同一个 XSPI 外设控制。如果片选切换逻辑不正确可能出现访问 PSRAM0 时 CS1 也拉低或者片选信号毛刺导致 PSRAM 芯片输错数据。这种情况下先用示波器观察两个片选信号的实际波形确认切换时序是否干净。如果片选之间有毛刺或交叠需要检查 XSPI 的片选极性设置是否与 PSRAM 芯片的片选逻辑匹配。多数 PSRAM 的 CS 是低有效但有些芯片可能要求高有效或者有额外的使能引脚需要控制。还有一种低级错误是通过 GPIO 软件控制片选。之前有个同事为了方便把片选接到了普通 GPIO 上想通过代码切换。但这样只能用于低频测试不能用于高速读写因为软件切换的延迟和不确定性远远满足不了 XSPI 的时序要求。必须使用 XSPI 外设自带的片选逻辑。4.3 安全区与非安全区、DMA 访问STM32N6 带 TrustZone 功能A、B 两套安全世界非安全世界的地址映射可能对 XSPI 区域有影响。如果你的工程启用了 TrustZone并且外部 PSRAM 被配置为非安全地址区域但 CPU 当前运行在安全状态访问时可能被防火墙拦截表现为总线错误或者数据读取为全零。另外DMA 访问外部 PSRAM 时除了地址映射问题还需要关注 cache 一致性问题。M55 内核有 D-Cache如果 CPU 写了一段数据到 PSRAM之后让 DMA 去读而 D-Cache 没有写回DMA 可能读到旧数据。反之DMA 写入数据后CPU 通过 Cache 读会读到过期的缓存行。解决方法是按照 ST 推荐的方式做 Cache Clean/Invalidate 操作或者在启动时把对应的内存区域配置为不缓存device memory 或 non-cacheable。4.4 扩展内存初始化顺序最后说一个经常被忽略的宏观问题扩展内存的初始化顺序。如果工程里同时使用了 XSPI Flash 和 XSPI PSRAM或者使用了多个 XSPI 接口初始化顺序必须合理安排。原则上所有 XSPI 外设的上电初始化要在进入 main 函数后尽早完成最好在创建 RTOS 任务、启动调度器之前完成。因为一旦调度器启动多个任务可能同时访问外部存储如果某个外设还没初始化总线访问会发生不可预知的错误。另外在低功耗模式下XSPI 外设可能会在睡眠/停止模式期间被关闭电源或时钟。唤醒后需要重新执行初始化流程。这个坑尤其隐蔽因为唤醒后看起来系统还活着但一访问 PSRAM 就死机或卡在等待中断状态。建议在低功耗管理代码中为 XSPI 增加重新初始化逻辑。最后再分享一个实操经验如果你在配置过程中发现怎么调都不稳定先别急着改代码回头看一眼原理图和 PCB。我之前遇到过一次数据错乱排查了三天最终发现是 PSRAM 的供电去耦电容放得太远纹波太大导致高速读写时信号采样出错。硬件上的问题软件再怎么写也弥补不了。另外XSPI 信号的走线长度尽量等长DQS 和时钟信号要做好包地这些都是高速外部存储的稳定工作前提。希望这篇内容能帮你少走一些弯路。