
1. 先算一笔账为什么非要用一个XSPI实例挂两片PSRAM1.1 单实例双片选省下的是真金白银先说说我遇到的场景MCU是Cortex-M85内核要做图形界面加语音识别内部RAM怎么都不够用只能把主意打到外部PSRAM上。评估了一圈发现512Mb的串行PSRAM单颗容量不够装两个屏的帧缓冲加音频环形缓冲再往上走1Gb的料号和供货又不行交期直接劝退。折腾一圈最后发现最划算的路径是让同一个XSPI实例同时驱动两颗PSRAM利用XSPI控制器自带的多个片选信号把两颗芯片挂在同一条总线上。为什么要纠结一个XSPI instance这件事原因很现实很多MCU只给了一个XSPI外设实例或者给了两个但另一个已经被NOR Flash占用了。XSPI外设不像普通串口那么简单它包含一大套寄存器组、时序控制逻辑、DMA接口和中断向量每多开一个实例不只是多一根CS脚的问题而是整套外设资源都要加倍。尤其在你只剩一个XSPI可用的情况下想让系统同时拥有外挂程序存储和大容量运行内存就只能在一个实例下面想办法。单实例双CS的好处是实实在在的。第一地址空间连续两颗PSRAM映射到同一段地址空间的不同区间CPU访问起来像访问一块大内存不用在东一块西一块的零散地址里跳来跳去。第二引脚省得多两套独立的XSPI接口需要两套CLK、两套DQ、两套CS而共享总线方案只是多一根CS1对引脚紧张的小封装MCU来说是救命级的优势。第三软件侧只有一个驱动实例、一套中断入口、一个DMA通道配置省掉的代码量和调试时间非常可观。1.2 什么时候必须放弃这个方案当然这个方案不是万能药。我在动手之前把放弃条件也想清楚了如果应用场景里两颗PSRAM需要同时满速访问比如一边在做摄像头图像采集另一边同时在跑AI推理那么单XSPI控制器本身就成了瓶颈因为同一时刻控制器只能服务一个片选这时候哪怕引脚再紧张也得咬咬牙上双实例。还有一种情况是第二颗PSRAM需要的CS引脚和MCU上的关键外设冲突。XSPI的CS1脚经常和其它高速外设复用比如Ethernet、SDHI之类的一旦冲突单实例双CS的方案就得推倒重来。我的原则是先查数据手册里的引脚复用表确认CS1在当前封装下没有被更重要的功能占用再开始画板。否则等你把PCB铺完了才发现CS1被网口占了那种绝望我经历过。另外时序上有个容易被忽略的点同一颗XSPI控制器在不同CS之间切换时序参数是需要额外时钟周期来重新同步的。如果你的访问模式是频繁在两个CS之间跳来跳去每一跳都是开销。2. 硬件连线同一条XSPI总线上接两颗PSRAM的接线细节2.1 共享信号和独享信号的完整清单PSRAM本身是一个没有地址线、靠命令访问的串行器件所以挂在XSPI总线上时绝大多数信号是可以共享的。我以常见的8线OPI接口PSRAM为例把接线画成一张表信号方向连接方式XSPI_CLKMCU到PSRAM同时接到两颗PSRAM的CK管脚XSPI_DQ[7:0]双向同时接到两颗PSRAM的DQ[7:0]XSPI_DQS双向同时接到两颗PSRAM的DQS如果器件支持XSPI_CS0MCU到PSRAM只接PSRAM_A的CE#/CS#XSPI_CS1MCU到PSRAM只接PSRAM_B的CE#/CS#XSPI_RESETMCU到PSRAM两颗都接或者各用独立GPIO这里最重要的原则就是CLK、DQ、DQS这些总线信号全部并联共享CE#/CS#片选信号必须一一分开绝对不推荐把两个CE#短接在一起当一颗用那样两颗芯片的输出驱动会同时作用在DQS和DQ线上读数据时直接打架轻则波形畸变重则烧毁IO。如果MCU的XSPI控制器有独立的复位输出建议接到两颗PSRAM的RESET#上并且在GPIO配置里把它设成独立控制。这样调试的时候可以只复位其中一颗不用两颗一起重新初始化。如果MCU没有专用复位脚用普通GPIO拉两根线过去也行代价只是多占两个IO但好处是你能单独操作任意一颗PSRAM这个自由度在后面联调阶段非常值钱。2.2 布局走线与电源去耦的实操建议XSPI跑到100MHz以上DDR模式时信号完整性就开始讲究起来了。CLK信号到达两颗PSRAM的走线长度尽量等长DQ和DQS等长这个等长不是指严格到毫米级但PCB Layout时至少要让第二颗PSRAM的信号路径比第一颗多出来的长度控制在几百密耳以内多出来的stub不要太长。实际布局上我推荐用菊花链而不是星型主控先走到PSRAM_A再从PSRAM_A旁边绕到PSRAM_BCLK线尽量走内层两端尽量靠近器件管脚处加匹配电阻。如果两颗PSRAM离得太远中间的stub反射在DDR模式下会非常麻烦。我第一次打样的时候PCB空间太挤PSRAM_A和PSRAM_B分列在PCB两侧XSPI_CLK走了很长一条绕线结果DDR读数据在高温下偶尔就错一位后来把布局改成贴着放问题才消失。电源去耦同样不能省。PSRAM在DDR模式读写切换的瞬间电流变化很陡VCC上如果去耦不足示波器能看到几十毫伏级别的毛刺轻则偶尔读错重则器件挂死。我的做法是每颗PSRAM的VCC管脚旁边放一个0.1uF和一个1uF的MLCC尽量靠近管脚VCC的过孔就近打不要共用很长的电源分支。另外两片PSRAM的VCC最好都从同一个稳压源出来中间用磁珠隔离也行但不要让其中一颗的电源走线穿过另一颗的下方。3. 初始化流程每一颗PSRAM都要单独配一次对3.1 为什么不能同时初始化两片PSRAM很多第一次接触这个方案的人会问能不能发一条命令让两颗PSRAM一起进入OPI DDR模式答案是绝对不能。PSRAM不像I2C设备有从机地址它完全依靠CS引脚来区分总线上的命令归属。总线上同一时刻只有被CS选中的那一颗芯片会解析并响应命令另一颗虽然也看得到时钟和数据线上的信号但只要它的CE#是高电平就会把总线上的命令当空气。更危险的做法是想当然地把两颗PSRAM的CE#短接在一起然后发命令这样两颗芯片会同时接收命令、同时驱动数据线读操作时两个输出驱动器同时往DQ上推数据总线冲突直接导致信号崩溃。所以初始化时只能一颗一颗来先选中CS0把完整的配置流程走完再把CS指针拨到CS1把同一套流程重复一遍。两颗之间不会有任何同步问题因为它们的内部状态机完全独立自己要对自己负责。3.2 以APS256XX为例的完整初始化序列我用的PSRAM是APMemory的APS256XX系列典型的8线OPI接口器件。上电后芯片默认工作在SPI SDR模式所以初始化过程大致分三个阶段先用SPI模式做复位和ID检查然后发送切换命令让芯片进入OPI DDR模式最后在OPI DDR模式下继续配置延迟和驱动强度等参数。完整的流程可以用下面这段伪代码来表达/* 伪代码接口名称以实际SDK为准 */ static void psram_reset(xspi_cs_t cs) { uint8_t reset_en 0x66; uint8_t reset 0x99; XSPI_DirectTransfer(cs, reset_en, 1, NULL, 0); XSPI_DirectTransfer(cs, reset, 1, NULL, 0); delay_us(150); } static void psram_check_id(xspi_cs_t cs) { uint8_t cmd 0x9F; uint8_t id[4] {0}; XSPI_DirectTransfer(cs, cmd, 1, id, sizeof(id)); /* 正常回读应该是厂家ID密度ID全0xFF或全0x00都说明物理链路有问题 */ } static void psram_enter_opi_ddr(xspi_cs_t cs) { /* 写配置寄存器0使能OPI模式DDR模式并设定驱动强度等位域 */ uint8_t cmd[] {0x71, 0x00, 0x00, 0x26, 0x40}; XSPI_DirectTransfer(cs, cmd, sizeof(cmd), NULL, 0); delay_us(200); /* 注意从此之后这颗PSRAM已经工作在OPI DDR模式 主控侧XSPI外设的协议配置也必须同步切换否则后续命令全部失效 */ } static void psram_init(xspi_cs_t cs) { psram_reset(cs); psram_check_id(cs); psram_enter_opi_ddr(cs); /* 切到OPI DDR之后继续按手册配置CR1/CR2/CR3等寄存器 包括Read Latency、Refresh Interval、Burst Length */ uint8_t cfg1[] {0x71, 0x00, 0x01, 0x02, 0x00}; uint8_t cfg2[] {0x71, 0x00, 0x02, 0x00, 0x00}; uint8_t cfg3[] {0x71, 0x00, 0x03, 0x00, 0x02}; XSPI_DirectTransfer(cs, cfg1, sizeof(cfg1), NULL, 0); XSPI_DirectTransfer(cs, cfg2, sizeof(cfg2), NULL, 0); XSPI_DirectTransfer(cs, cfg3, sizeof(cfg3), NULL, 0); delay_us(100); } /* 两片PSRAM分别初始化 */ void board_psram_init_all(void) { psram_init(XSPI_CS0); psram_init(XSPI_CS1); /* 初始化完成后把XSPI控制器切到Memory-Mapped模式 之后就可以直接通过指针访问两个地址区间 */ XSPI_MemoryMappedEnable(XSPI_CS0); XSPI_MemoryMappedEnable(XSPI_CS1); }这段流程里有几个细节值得展开说。第一Reset命令发完之后要等足够久PSRAM内部的复位状态机需要时间完成等太短会导致后续命令被丢弃尤其在低温环境下复位时间会更长。第二0x71这条写配置寄存器命令发出后芯片会立即切换工作模式主控侧必须紧接着把XSPI外设的协议模式从SPI SDR换成OPI DDR这个同步如果晚一步后面所有命令都串不进去。所以在工程上我习惯把切换前和切换后的配置分开用两个不同的XSPI设备描述对象来管理。还有一点ID检查那一行不要图省事跳过。两颗PSRAM都读一遍ID能在早期把焊接问题、虚焊、短路问题暴露出来省得后面系统跑起来出现莫名其妙的偶发错误再回头怀疑是软件问题。3.3 工程配置里最容易漏掉的CS1时序参数很多图形化配置工具里新建一个XSPI实例时只会默认生成CS0的Device配置CS1那一栏默认是禁用或者沿用CS0参数的。如果你没有单独给CS1填时序参数代码里访问CS1地址段的时候控制器会拿CS0那套时序去操作第二颗PSRAM而两颗PSRAM虽然型号相同工艺批次不同也可能导致Dummy Cycle和Read Latency有细微差异结果就是CS0稳如老狗CS1隔三差五读错。以FSP这种图形化配置环境为例在XSPI实例的属性页里每个Chip Select都有独立的一组Device Timing参数包括协议模式、时钟频率分频系数、Dummy Cycle数、Read/Write Latency、命令序列。CS1那一组参数你要单独配不能以为和CS0一样就完事。建议的做法是先把CS0调通然后把CS1的DummyCycles和Latency先设成比CS0大一档再逐步往下压直到能稳定通过压力测试再固定下来。4. 地址映射与访问策略把两片PSRAM当一块大内存用之前4.1 地址空间划分和CS自动切换原理XSPI控制器在Memory-Mapped模式下会把外部存储空间映射到CPU地址空间的特定区间。CS0和CS1各自对应的地址段在芯片手册的内存映射表里是写死的一般CS0从XSPI映射基地址开始CS1紧跟着在后面但不保证连续。有些MCU会在两个CS区域中间留一段保留地址这个坑我踩过当时写DMA缓冲区分配代码时假设CS1紧挨着CS0结果缓冲区横跨了两段映射区访问到中间空洞的地址立刻触发总线错误。正确的做法是初始化完两片PSRAM后先打印或者断点查看两个区域的基地址和大小确认中间有没有空洞再开始做地址规划。你把CS0区域的起始地址记为PSRAM_A_BASECS1区域记为PSRAM_B_BASE两片PSRAM在软件里就是两个完全独立的内存池即使地址空间看起来相邻也不要在逻辑上把它们合并成一片。4.2 Cache、DMA对齐与跨片选传输的注意事项CPU访问XSPI映射区域时如果MCU开了D-Cache就得小心缓存一致性问题。CPU写过PSRAM的数据如果不做Cache CleanDMA搬运时可能读到的是Cache里的旧数据反过来DMA写入PSRAM后CPU如果不能确认Cache Line已经被Invalidate读到的也可能是旧的。嵌入式里这类问题表现非常隐蔽程序逻辑看不出任何毛病但结果就是错的。我习惯把PSRAM映射区域配置成Write-Through或直接关闭Cache只在性能敏感的热路径上手动做Cache操作。如果你用的是带XSPI的Cortex-M系列MCU还可以用MPU把PSRAM区域设成Non-Cacheable或者Write-Back临时Clean。总之先保证一致性再考虑性能。DMA访问还涉及对齐问题。XSPI控制器通常对32位访问效率最高如果你的DMA搬运长度不是总线宽度的整数倍控制器会拆成多次非对齐事务性能掉得厉害。更麻烦的是跨CS的DMA传输也就是DMA传输长度跨越了CS0和CS1的边界这个在部分MCU上是不受支持的控制器内部无法在一个DMA序列里完成片选切换。我在项目里遇到过一次DMA读2MB数据从CS0区域读到CS1区域源地址跨越了CS边界结果末尾几十字节全乱了。后来的做法是无论如何不让单个DMA请求跨CS边界如果数据确实分布在两片PSRAM上就拆成两个DMA请求。5. 实测数据与带宽真相容量翻倍不等于性能翻倍5.1 测试环境与方法我先把话说在前面单实例双CS的PSRAM方案容量翻倍是真的带宽翻倍想都不要想。因为整个XSPI控制器只有一个数据通路CS0和CS1完全是分时复用同一时刻只能有一个片选在总线上传输数据。我的测试环境是XSPI时钟跑到133MHzOPI接口、8线DDR模式理论峰值带宽是133MHz乘以2DDR双沿再乘以8位等于2128Mbit/s也就是266MB/s。需要说明的是这已经是这类PSRAM接口的上限附近了实际能跑多少取决于命令开销和Dummy Cycle。测试方法是写一段简单的裸机测试代码用DMA在内存映射区做连续块读、连续块写统计吞吐率。测试数据块大小选1MB避开Cache的影响关闭中断用定时器计时。为了对比两片PSRAM和单片PSRAM的差异我分别测了只访问CS0和交替访问CS0/CS1两组数据。5.2 单双片数据对比与结论测试项单颗PSRAMCS0双PSRAM交替访问CS0CS1连续块读1MB约182MB/s约176MB/s连续块写1MB约121MB/s约118MB/s64B随机读约28MB/s约27MB/s双缓冲DMA读CS0写CS1不适用约110MB/s注意这是总吞吐数据很说明问题单颗PSRAM连续读能到180MB/s以上加上第二颗之后总带宽并没有变成双倍反而因为CS切换时的时序重新同步开销稍微掉了一点。写操作本身比读操作慢因为写命令的地址和命令头占用的总线周期更多。还有个容易混淆的概念是双缓冲翻倍有些人以为把两个缓冲放在两颗不同的PSRAM上一边读一边写就能让吞吐翻倍。但从上面的测试结果看读CS0写CS1这种双缓冲实际总吞吐只有110MB/s左右比单颗PSRAM的连续读带宽还低。原因很简单DMA在CS0发起读事务事务结束后CS1才能发起写事务中间还有片选切换开销整个流水线实际上没有并行度。5.3 基于带宽特性的应用建议明白了带宽真相之后应用策略就要跟着调整。如果你的系统里有两块显示缓冲区我的建议是两块缓冲区放在同一颗PSRAM上不要分开放两颗。因为显示驱动访问缓冲区时是连续读写如果两块缓冲分布在CS0和CS1两边每次切换屏幕缓冲区都会多一次片选切换开销长远看没有任何性能收益。反过来如果两块缓冲区用途完全不同比如一块是图像采集的帧缓冲一块是音频环形缓冲它们几乎不会在同一个时间片里被同时访问那就可以放心地分开放置这样能避免两个数据流在同一个PSRAM里互相争抢带宽。还有一点关于随机访问PSRAM的随机读带宽远低于连续读64字节随机读大概只有28MB/s。如果你的应用依赖大量分散的小块访问比如数据库索引、图形加速器的命令列表那就要非常谨慎。这种情况下加大DMA搬运粒度、把分散的小块数据先在内部RAM里聚合成大块再一次性写入PSRAM性能会有数量级的提升。6. 联调踩坑实录六个值得记录的现场问题6.1 第二片PSRAM读回来全是0xFF这是最常见的现象也是最容易排查的。第二片PSRAM在CS1地址段反复读取返回的数据永远是全0xFF。拿示波器看CS1波形发现CS1根本没有任何低电平动作。问题基本出在两个地方一是CS1地址段没有在XSPI外设配置里使能很多SDK默认只开CS0二是CS1引脚被GPIO复用成了其它功能需要在引脚配置里把XSPI_CS1功能打开。这两个问题在图形化配置工具里都很容易漏因为新建工程时工具只默认生成CS0的设备配置。处理方式也很简单先在引脚配置里确认XSPI_CS1已经映射到正确的物理引脚然后在XSPI外设配置里把CS1的Device参数完整填上包括协议模式、时序参数和地址段。填完之后不要急着跑应用先在CS1地址段的起始地址写一个已知值再读回来校验确认基本访问通路没问题再往下走。6.2 同型号不同批次高频下只有一片稳定两片PSRAM型号完全一样但一个是老批次料一个是新批次料。133MHz下CS0那颗跑几天都没事CS1那颗跑几个小时的连续读就开始随机出错出错间隔没有规律降低到100MHz又一切正常。这种问题十有八九是两颗芯片内部的时序校准有微小差异导致它们在不同的频率点上有不同的时序裕量。解决办法一是把CS1的Dummy Cycle或Read Latency单独上调一档。当时的配置是Dummy Cycle从6改成7CS1就稳定了。如果调Dummy还不行二是单独为CS1降一点时钟频率比如CS0跑133MHzCS1跑100MHz反正两颗的时序参数是独立的互不影响。这个坑提醒我即使同一型号批次差异也不能默认忽略。在量产阶段我建议把两颗PSRAM的时序参数都留出一档裕量以最差批次为准别为了追求那几MB带宽把自己逼到悬崖边上。6.3 低温上电初始化失败环境测试时发现-20℃低温下系统上电有概率出现PSRAM初始化失败热机就没事。示波器抓Reset引脚的波形发现Reset命令发完之后芯片还没完全恢复稳定紧接着的命令就已经发出去了。查数据手册发现低温下PSRAM内部的上电复位时间会比常温长很多。我原来在Reset之后只等了150us低温下不够。把Reset后延时加大到500us问题消失。另外如果PSRAM的RESET#脚是MCU GPIO控制的上电时要保证RESET#先保持一段低电平再拉高释放给芯片足够的启动时间。这里还有个经验如果项目要做环境测试初始化和时序验证一定要在低温条件下做一遍很多常温下完全没问题的参数在低温下会现出原形。6.4 写后立刻读却是旧数据现象是对PSRAM做写循环之后立刻读回的测试数据对不上但如果写完之后隔几百微秒再读数据就对了。第一反应是PSRAM写延迟问题后来发现根因在MCU的Cache和XSPI写缓冲上。CPU写数据进XSPI映射地址时数据先落在写缓冲或者D-Cache里还没有真正推到外部总线上立刻发起读操作时读请求走了另一条路径于是读到了外部存储器的旧值。解决办法有两种对于D-Cache在对PSRAM区域执行写完之后的读操作前执行Cache Clean操作把Cache Line强制写回或者把该区域的Cache策略改成Write-Through牺牲一点写性能换一致性。对于XSPI控制器内部写缓冲通常在驱动库里会有对应的等待函数写完之后调用一次等待总线空闲的API再发起读操作。6.5 VCC纹波导致的随机bit翻转这是最难排查的一类问题现象非常随机系统长时间跑下来偶尔会有某个bit反转但不是固定位置也不是固定时间用软件单步调试永远复现不了。最终用示波器抓到VCC上的纹波在DDR模式读写切换的瞬间有大约80mV的毛刺超过了PSRAM电源纹波规格。处理方式是增加去耦电容同时对PSRAM的IO驱动强度做降档设置。驱动强度太强会在切换瞬间产生更大的电流毛刺把驱动强度从最强档降一档之后纹波明显变小问题不再复现。这类随机性问题最忌讳上来就改代码先把电源、时钟、信号完整性排查清楚尤其是高速DDR模式下电源和信号问题导致的软错误远比代码逻辑问题多得多。6.6 DMA跨CS搬运数据错乱前面提过一次这里详细记录。当时写了个DMA任务要把一块2MB的数据从CS0区域搬到外部接口源地址不小心跨越了两个CS的边界。DMA启动后前1.5MB数据正常到了CS边界附近传输的数据开始错位最后一部分数据直接丢失。原因是XSPI控制器在单个DMA传输过程中不支持跨CS切换DMA内部地址连续递增但跨越到CS1的地址段后控制器用的还是CS0的时序参数加上两个CS的映射区中间可能还有空洞DMA访问到洞上就直接悬空。解决方式是检查所有DMA请求的源地址和目的地址确保单个DMA传输范围