前几天把一个图形界面项目的内存扩展部分折腾通了。这个项目要在单颗MCU上跑一个分辨率不低的HMI界面,UI素材、帧缓冲、系统缓存、日志缓冲区全加在一起,内置RAM根本顶不住。当时看了好几种外部内存扩展方案,最后选定了一条路:在某款带XSPI接口的MCU上,用两片64MB的octal PSRAM,通过EXTENDMEM机制挂在同一个XSPI端口上,拼成连续的128MB内存空间。现在方案已经稳定运行了一段时间,读写带宽和长期可靠性都超出了我的预期。这篇就把完整的选型思路、软硬件设计、踩坑过程和实测数据一次说清楚,给同样在内存焦虑里的朋友一个可以直接抄作业的参考。
1. 内存不够用是天大的事:这次扩展到底要解决什么
1.1 项目里为什么突然需要128MB
你可能觉得MCU上用128MB内存有点夸张,但真当项目做到一定复杂度,内存就是这样被吃掉的。我这个项目的界面分辨率是1280x720,单是RGB888的帧缓冲就要1280x720x4字节,约3.5MB。这还不算什么,关键是界面上有大量预解码的图片资源,为了切换页面不卡顿,我选择把这些素材全部常驻在内存里,这部分直接占掉50多MB。再加上字体渲染缓存、解码中间缓冲、通信协议收发缓冲、日志缓冲,七七八八一算,总量轻松突破80MB。
内部RAM只有几MB,外部不扩展根本跑不动。如果只挂一个不能映射成内存地址的Flash,那还得靠手工发命令来读,每次刷新界面都要消耗大量CPU时间,实属无奈。所以最直接的需求就是:外部挂一个大容量、能随机读写、带宽不能太拉胯的RAM,并且最好能像内部RAM一样直接按地址访问,省掉一堆中间层。
1.2 三种外部内存扩展路线的对比
在确定方案之前,我把市面上常见的外部内存扩展方式都梳理了一遍,大致可以归成三类:
| 方案 | 典型接口 | 引脚开销 | 带宽表现 | 布线与驱动难度 | 容量扩展性 |
|---|---|---|---|---|---|
| 并口SDRAM | 16/32位并行总线 | 40根以上 | 很高,但要刷新 | 高,时序约束多 | 靠多片叠,PCB面积大 |
| QSPI PSRAM | 4线SPI / QPI | 6~9根 | 中等,约80~160MB/s | 低,驱动简单 | 单颗容量偏小 |
| Octal PSRAM | 8线XSPI | 10~12根 | 较高,约200MB/s以上 | 中等,需注意DDR采样 | 可多CS片选叠容量 |
SDRAM方案首先被我排除。虽然带宽和容量都有保证,但它需要地址线和数据线并行,引脚数量动辄四五十根,同时还要自己维护刷新时序,对PCB布局和代码复杂度都不友好。QSPI PSRAM倒是简单,但单颗容量上限低,想要凑到128MB得挂很多片,接口和布线都会变得很难看。最终留下来的就是Octal PSRAM走XSPI口。
选择Octal PSRAM的根本原因有三个:一是引脚数适中,XSPI端口的信号线就十来根;二是支持DDR模式,理论带宽足够支撑GUI刷新;三是用片选信号扩展容量很方便,两片挂在同一端口上就能凑出128MB,地址还能通过映射机制拼成连续区域。
2. XSPI和EXTENDMEM是怎么配合在一起的
2.1 XSPI到底是个什么接口
XSPI可以理解成SPI的“加宽”版本。普通SPI是4根线:时钟、片选、主出从入、主入从出;四线QSPI是加了两条数据线;而XSPI(也叫OctoSPI)把数据线扩展到8根,也就是DQ0到DQ7,配合方向控制线RWDS,在一个时钟周期内可以传输8位数据。再加上DDR模式,时钟上升沿和下降沿都能传一次数据,实际数据吞吐就能翻倍。
PSRAM正好是吃这套接口的典型设备。它内部还是靠电容存储的DRAM单元,但对外把所有控制逻辑都封装好了,你不需要像SDRAM那样自己去刷新,接口上看起来就是一个高速串行RAM。指令协议和普通Flash类似,但支持按字节/字随机读写,非常适合当内存用。
我用的这颗MCU,XSPI接口时钟频率最高可以跑到133MHz甚至更高,配合DDR模式,理论上读带宽能到266MB/s左右。当然实际要打折扣,因为还有命令相位、等待状态和总线仲裁的开销,但这些放到后面的实测部分再说。
2.2 EXTENDMEM的地址映射机制
EXTENDMEM这个名字,在MCU文档里的意思就是“扩展内存映射”。它的核心思想是:外部PSRAM不再是需要通过SPI命令一个一个字节去读写的“外设”,而是被直接映射到MCU的系统地址空间里。你声明一个指针指向0x90000000这种地址,CPU访问它,XSPI控制器会自动把读写转换成PSRAM的协议时序。
这样做最大的好处是软件层变得干净。如果不用EXTENDMEM,那每一笔读写都得走寄存器配置、发命令、写地址、读写数据的套路,效率低代码还难看。用了映射机制以后,外部PSRAM对CPU来说就是“一块大RAM”,不管是普通C代码里直接操作指针,还是DMA搬运数据,都跟操作内部RAM一样方便。
顺带一提,EXTENDMEM不只是把地址映射出来就完了,它还会处理缓存一致性的问题。XSPI控制器内部通常会有读缓存,写操作可以选择直写或写回策略,实际项目中我建议至少在初期使用直写模式,等整个系统验证稳定以后再考虑优化写性能。
2.3 为什么要用两片64MB拼成128MB
单颗128MB的Octal PSRAM在市场上非常少见,即使有,价格和供货也都很不友好。反过来,64MB的Octal PSRAM属于比较成熟的档位,选两片拼容量,是性价比和可获取性都很好的方案。
关键问题是怎么“拼”。XSPI接口一般会提供多个片选信号,比如CS0和CS1。两片PSRAM共享同一组时钟和数据线,但片选各接各的。在EXTENDMEM配置里,把CS0对应的区域和CS1对应的区域设置成连续的地址段,对CPU来说就是一个从低到高完整的128MB地址空间。这就是标题里“two octal PSRAM on one XSPI port”的本质:同一根总线,两个片选,地址连续映射。
这里有一个容易踩的坑,我后面会专门讲:如果EXTENDMEM里的设备大小、基地址或者CS映射配置错了,经常会出现两片地址重叠或者中间缺一段的情况,而这类问题在现场排查时往往最难发现。
3. 硬件设计:两片PSRAM挂到同一个XSPI端口
3.1 芯片选型时的关键参数
选型时我重点关注了几个参数。第一是容量和电压,我用的是64MB一颗的Octal PSRAM,1.8V供电。这里要注意,MCU的XSPI引脚电平必须和PSRAM匹配,很多芯片的XSPI电源域就是1.8V,如果你MCU默认3.3V,那就需要加电平转换,这不仅增加成本,还会影响高速信号性能,所以最好直接选1.8V兼容的MCU。
第二是支持的时钟频率和DDR模式。我选的PSRAM支持在133MHz下跑DDR,也就是数据引脚频率等效到266MHz。实际PCB设计时,我一开始跑的是100MHz DDR,先让系统稳定,后面再逐步提高频率,这样排查问题面更小。
第三是封装。Octal PSRAM常见的封装是BGA类的小封装,焊接对PCB工艺要求高一些。如果只是前期验证,可以用模块板或者转接板,但量产设计就一定要评估好焊接良率。
3.2 原理图上怎么连接两片PSRAM
原理图连接的核心思路是:所有数据信号共享,片选信号分开。XSPI的时钟CK、数据线DQ0到DQ7、方向控制线RWDS、还有复位等信号,直接并联接到两片PSRAM上。片选CS0接第一片PSRAM的CS,CS1接第二片的CS。上电时序上,两片PSRAM完全独立控制,方便初始化时分别配置。
有一点很容易被忽略:PSRAM的CS引脚在内部通常有上电复位逻辑,如果MCU的XSPI片选信号默认电平不对,PSRAM可能会在初始化阶段一直处于异常状态。建议在原理图上给CS加上必要的上拉电阻,电平选择跟PSRAM电源域一致,别在这种小地方省成本。
RWDS信号要特别提一下。这个信号是PSRAM用来告诉主机“我现在是读还是写,数据总线方向是怎样的”,在DDR模式下,它还会充当数据掩码参考。连接时一定要接对方向,有的工程师会把它当成普通GPIO来配置,结果数据全乱。
3.3 布局布线和电源处理
PCB层面上,XSPI跑DDR虽然频率不像DDR3那么夸张,但也不能随便拉线。我这次踩了一个明显教训:第一次布板时CK时钟线比DQ数据线长了将近30mil,在100MHz DDR下问题不大,一旦跑到133MHz就出现偶发读错。后来重新调整走线,要求CK到两片PSRAM的长度差别控制在10mil以内,DQ组内部也要尽量等长。
电源方面,Octal PSRAM瞬时电流不小,每个电源引脚都要靠近摆放0.1uF和1uF的去耦电容。1.8V电源在动态切换读写方向时会有轻微纹波,如果示波器看到VDD上有超过50mV的跌落,就需要加粗电源铜皮或者增加磁珠滤波。这点不会写在芯片手册里,但实测下来影响确实存在。
布局上,两片PSRAM尽量靠近MCU的XSPI引脚放置,走线时避免跨越分割的电源平面。如果PCB是多层板,把XSPI信号线走在完整地平面相邻层,信号质量会好很多。
4. 软件侧:XSPI接口初始化与EXTENDMEM映射
4.1 初始化流程和上电时序
软件初始化是这套方案最需要耐心的地方。Octal PSRAM上电后不是立刻就能用的,它有一整套启动时序要遵守。我的初始化顺序大致是这样:
- MCU上电后先延时200微秒以上,让PSRAM内部电源稳定。
- 发送Reset命令给PSRAM,然后再等待一段时间,不同芯片要求不同,我一般给500微秒。
- 等待PSRAM就绪以后,读取设备ID寄存器,验证通信是否正常。
- 根据读到的ID,确认器件容量和型号,然后配置XSPI控制器的基本参数。
- 发出DDR模式使能命令,让PSRAM进入DDR工作模式。
- 配置好采样延时,打开EXTENDMEM映射。
- 最后写一段全地址测试代码,用1MB步进写读校验。
这套流程里,最怕的是“上电后没有足够延时就发命令”,一旦出现这种情况,PSRAM会处在一种半死状态,表现为后续所有命令都无响应。我排查过一次,最后就是在上电延时上加了保险。
4.2 XSPI控制器的关键配置
XSPI控制器本身配置时,有几个参数直接影响能不能稳定跑起来。一是时钟分频,我初始阶段先配置成100MHz DDR,而不是直接拉满133MHz,就是为了先拿到一个正常工作的环境。二是命令类型,比如是Single/Dual/Quad/Octal,读和写都要设置成Octal模式。三是采样延时,控制器Read Sample Clock要配合PSRAM的tAC参数来调,值偏大偏小都会出现读数据错位。
下面是我初始化过程中核心的一段配置示意,不同MCU寄存器名称会不一样,但思路是通用的:
void xspi_psram_init(void) { // 1. 上电延时,确保PSRAM内部电源稳定 delay_us(200); // 2. 复位PSRAM,并等待复位完成 xspi_send_cmd(CMD_RESET); delay_us(500); // 3. 读取设备ID,验证通信 uint16_t id = xspi_read_reg(REG_DEVICE_ID); if ((id & 0xFF) != EXPECTED_ID) { error("PSRAM ID mismatch"); } // 4. 设置XSPI控制器寄存器 xspi_regs->CFG_CLK = DIV_100M; xspi_regs->CFG_IO = IO_8BIT; xspi_regs->CFG_DDR = ENABLE; xspi_regs->CFG_LATENCY = READ_LATENCY_6; xspi_regs->CFG_SAMPLE_PHASE = 2; // 需要实测调整 // 5. 发送DDR模式使能命令 xspi_send_cmd(CMD_ENTER_DDR); delay_us(10); // 6. 打开EXTENDMEM映射 extendmem_base_addr = 0x90000000; extendmem_total_size = 128 * 1024 * 1024; extendmem_enable(1); // 7. 全地址写读校验(略) psram_memory_test(); }需要特别强调的是,配置里的Read Latency要根据时钟频率来选,不能照抄参考代码。我一开始按最低值配,100MHz下还能跑通,但界面刷新时偶尔花屏,把Latency调高一级以后问题就消失了。这类参数就是靠实测来定的,谁也没办法给你一个全平台通吃的数值。
4.3 EXTENDMEM映射和缓存配置
EXTENDMEM映射这步,最关键的是把两个片选设备配置成连续的地址区域。我在这颗MCU上,把CS0对应的PSRAM映射在0x90000000到0x94000000,CS1对应的PSRAM映射在0x94000000到0x98000000,这样整体就是一个完整的128MB连续空间。
缓存策略上,XSPI控制器一般提供读写缓存使能位。我在最初调试阶段把写缓存关掉,采用直接写入PSRAM的方式,这样虽然速度略慢一点,但不容易出现缓存和外部存储不一致的问题。等系统稳定性验证通过以后,再根据需要打开写缓存,或者配合DMA批量写入来提高性能。
另外,EXTENDMEM一般会有一块窗口机制,可以用页表方式映射,也可以把整个外部存储映射进系统空间。我选择的是全映射,因为这样代码最简单,就是基地址加偏移量,不需要手动换页。
5. 验证和性能实测
5.1 地址连续性与读写验证
硬件和软件都配置好之后,别急着跑应用,先做一轮存储测试。我写了一个简易的内存测试程序,从0x90000000开始,每隔64KB写入一个固定值,然后回读比对。这个测试看起来简单,但能暴露大部分总线问题。
void psram_memory_test(void) { volatile uint32_t *base = (volatile uint32_t *)0x90000000; volatile uint32_t *base2 = (volatile uint32_t *)0x94000000; uint32_t i; uint32_t errors = 0; // 对CS0区域做地址-数据反相测试 for (i = 0; i < 64 * 1024 * 1024 / 4; i += 4096) { base[i] = i ^ 0x5A5A5A5A; } for (i = 0; i < 64 * 1024 * 1024 / 4; i += 4096) { if (base[i] != (i ^ 0x5A5A5A5A)) errors++; } // 对CS1区域做同样测试,主要验证地址是否重叠 for (i = 0; i < 64 * 1024 * 1024 / 4; i += 4096) { base2[i] = i ^ 0xA5A5A5A5; } for (i = 0; i < 64 * 1024 * 1024 / 4; i += 4096) { if (base2[i] != (i ^ 0xA5A5A5A5)) errors++; } if (errors == 0) { debug("PSRAM test passed\n"); } else { debug("PSRAM test failed: %d errors\n", errors); } }如果这段测试能全量通过,说明CS0和CS1的映射区域没有重叠,数据线也没有接错。我建议测试步长不要太大,因为有些地址线的焊接问题只有通过大跨度访问才能暴露出来。交叉使用0x55、0xAA这种特征值,能更有效地发现信号串扰问题。
5.2 带宽测量结果
单纯读写没问题还不够,还得看带宽是否满足应用需求。我用DMA方式跑了一组批量测试:一次搬运64MB数据,统计耗时,换算成实际带宽。在133MHz DDR模式下,实测结果如下:
| 操作 | 方式 | 实际带宽 |
|---|---|---|
| 连续读 | DMA burst | 约198MB/s |
| 连续写 | DMA burst | 约177MB/s |
| 随机读 | CPU指针访问 | 约120MB/s |
| 随机写 | CPU指针访问 | 约95MB/s |
连续读能到接近200MB/s,对GUI帧缓冲来说完全够用。随机访问的带宽低一些,这符合PSRAM的固有特性,毕竟每次访问都有命令开销和等待时间。所以如果你要做大量随机小数据访问,建议在PSRAM里做一个Cache或者用内部RAM做中转,别直接裸奔。
对比之前用QSPI PSRAM的体验,Octal接口带来的提升是立竿见影的。同样的DMA批量拷贝,QSPI方案大概只能跑到100MB/s上下,翻倍的效果在界面左右滑动时尤其明显,画面卡顿感几乎消失了。
5.3 在实际应用中的表现
跑通存储测试和带宽测试以后,我把HMI界面工程整个迁移到了PSRAM上。帧缓冲、UI素材缓存、日志缓冲全部放到这128MB里,内部RAM只保留关键中断处理和调度器用数据。
实际跑动以后,页面切换流畅度完全达到预期。之前为了省内存,需要压缩图片素材,导致放大后边缘发虚;现在内存宽裕了,素材全部用原始分辨率加载,显示效果直接上升一个档次。日志系统也终于可以放心地开全量调试输出,再也不用担心日志打多了把内存打爆。
稳定运行方面,连续跑了一周的长时间测试,没有出现内存数据被篡改或者偶发访问异常的情况。当然这跟布线质量、时钟频率余量都有关系,我会建议量产前再做一轮高温环境测试,把最恶劣工况下的稳定性数据收集起来。
6. 常见问题与排查技巧实录
6.1 读取内容偶发错乱
这个是我在开发中遇到最多的问题。典型现象是:写入数据以后,回读大部分对,但偶尔某几个字节变成0xFF或者随机值。排查时先看采样延时,调整XSPI控制器的Read Sample Clock相位通常能解决。还有一个因素是时钟频率过高,把133MHz降回到100MHz测试一下,如果问题消失,说明是信号完整性的原因而不是逻辑错误。
这种情况下我会用示波器抓DQ线上的信号,看看数据窗口是否足够宽。如果发现眼图明显收窄,就要回到PCB层面检查等长和串扰问题。现实中很多所谓“芯片不稳定”问题,最后都是布线或者采样配置引起的。
6.2 两片PSRAM地址重叠
两片PSRAM挂在同一个XSPI端口时,地址重叠是最容易发生的配置错误。表现是:往CS0区域写入数据,CS1区域读出来也变了,或者反过来。我一开始就犯了这个错,原因是EXTENDMEM的设备大小寄存器没写对,导致CS0映射区域占据了整个128MB空间,把CS1的地址完全给吞了。
检查方法是打印MCU外部映射区域表,确认CS0和CS1的基地址是否连续,设备大小是否设置成64MB而不是默认的128MB或者32MB。这类问题其实在寄存器层面很好解决,但难在要意识到“哦,是这个原因”,所以我把这个单独列出来。
6.3 上电以后读不到设备ID
如果你发完上电复位命令,读寄存器一直是0,或者超时,优先检查这几个点:CS引脚电平是否有上拉,PSRAM的Reset引脚是否被拉住了,上电延时是否足够。我遇到过一次,是PSRAM的Reset引脚被MCU的调试口初始化影响,导致PSRAM一直处于复位状态。后来改成用独立的GPIO控制Reset脚,问题就消失了。
还有一种情况是XSPI控制器没配置成Octal模式就开始发命令。很多MCU上电默认走SPI模式,只发Command Address不加数据,如果你没把IO配置成8线模式,PSRAM根本收不到有效命令。建议初始化时序里最先配置IO宽度,再去发任何命令。
6.4 信号完整性和功耗问题
高速DDR模式下,如果PCB走线过长或者没有参考地平面,信号会衰减得很厉害,具体表现为跑低频测试正常、跑到高频就崩溃。这时候别急着换芯片,先用短跳线飞线搭一个最小系统,对比一下带宽,基本就能判断是设计问题还是芯片问题。
功耗方面,PSRAM在正常读写状态电流不小,系统进入低功耗模式时一定要把PSRAM也切到Sleep状态,否则整机耗电会很难看。有些MCU的XSPI模块在睡眠时不会自动通知外部设备,需要软件里手动发Sleep命令,这个小细节很关键,不然低功耗产品根本过不了指标。
7. 一些后续扩展和优化方向
7.1 还想继续扩容怎么办
如果你觉得128MB还不够,其实还有继续扩展的空间。XSPI控制器一般都支持多个片选,有些MCU提供了4个CS信号,理论上一根端口上可以挂4片甚至更多PSRAM。只要地址映射配好,总容量就能线性增长到256MB甚至512MB。
扩容时注意两点:一是每一片PSRAM的电平、型号、时序要尽量一致,否则你只能将就最慢的那片,整体性能反而下降;二是片选之间的信号隔离要做干净,别让CS0和CS1之间产生串扰,尤其在DDR模式下。
7.2 性能优化的几个方向
对于带宽比较敏感的应用,有几个优化思路值得试一下。一是在XSPI控制器里打开写缓存,并用DMA批量提交,让PSRAM的写操作尽量连续化,避免频繁总线切换。二是把频繁访问的热数据放在外部PSRAM,但把索引表和关键状态变量留在内部RAM,利用局部性原理减少PSRAM随机访问的频次。三是用双缓冲机制,在刷新帧缓冲的同时让DMA预取下一帧,把读写并行起来。
这些优化其实没什么神秘的地方,核心就是减少PSRAM随机访问、增加连续访问占比。PSRAM再快也是串行接口,本质上是拿带宽换容量,软件层一定要配合好。
我个人在实际操作中的体会是,XSPI加Octal PSRAM这个组合,在MCU级系统里确实是目前性价比极高的大内存扩展路线。它不像SDRAM那样折磨布线,也不像QSPI那样卡在带宽和容量上,128MB甚至更大的空间可以彻底解放软件架构上的条条框框。整个方案从头到尾折腾下来,最值钱的教训就是:时序余量一定要留足,PCB布线别省钱,初始化流程别图快——这三条做好了,成功就是水到渠成的事。