
1. 内存不够用的那一天我决定给 MCU 外挂内存1.1 真实项目里的内存焦虑我之前做过一个便携式数据采集器MCU 用的是 RP2040内部 RAM 一共 264KB听起来不算特别小对吧但实际项目里要跑 LVGL 界面、要同时缓存好几路传感器数据、还要做曲线绘制和历史回放内存瞬间就紧得不行。LVGL 在 320x240 的屏幕下光一个 RGB565 帧缓冲就要 150KB264KB 还没开始存业务数据就已经快见底了。再加上 UI 控件、字体缓存、DMA 缓冲区跑一会儿就 OOM程序直接死给你看。那段时间我一直在跟内存斗智斗勇把全局变量改成局部变量、能省的 buffer 尽量省、图像资源全部压缩放 Flash……但说实话这种抠内存的方式只能缓解不能根治。你要跑稍微像样一点的应用比如 GUI、图像缩放、音频缓存、机器学习推理缓冲MCU 内部那几百 KB 根本不够看。后来我决定换个思路既然内部内存不够就给 MCU 外挂一颗大内存。市面上常见的选择是 SPI 接口的 SRAM、并行 SRAM、FRAM还有 PSRAM。我最终选的是 APS6404L-SQH-SN一颗 64Mbit也就是 8MB的 SPI PSRAMSOP-8 封装引脚少接线简单驱动起来也比较顺手。这颗料解决了我后面一整个项目的内存焦虑。1.2 外挂存储选型从串行 SRAM 到 PSRAM在动手之前我先把几种方案摆在一起对比过这里直接放结论省得大家再去一个个查类型容量接口典型型号优势劣势串行 SRAM32KB~1MbitSPI23K256接口简单容量太小价格偏高并行 SRAM1Mbit~16Mbit并行总线IS61LV25616速度快引脚太多布线痛苦FRAM8KB~8MbitSPI/并行FM25V10几乎无限擦写速度快贵大容量不好买SPI PSRAM16Mbit~64Mbit以上SPI/QPIAPS6404L容量大便宜引脚少有延迟需要命令阶段我最后选 PSRAM核心原因就是容量和成本。64Mbit 的 PSRAM 在价格上和几百 KB 的串行 SRAM 差不了多少但容量差了几十倍。另外 SOP-8 封装对硬件工程师太友好了飞几根线就能在面包板上跑起来画 PCB 也不会增加多少布线压力。1.3 为什么锁定 APS6404L-SQH-SNAPS6404L 这颗料在国内外开源社区里出镜率很高尤其常见于 RP2040、ESP32、STM32 这类板子的内存扩展方案。它的基本盘是 64Mbit 容量、SPI 接口、支持 SPI/QPI 两种访问模式、内部自动刷新外部看起来和 SRAM 一样静态。我选它还有一个很重要的原因有现成的软件库可以参考踩坑的人多、解决方案也多不至于遇到问题只能自己硬扛。另外它的工作频率最高能跑到 100MHz 以上具体看手册里不同模式下的上限QPI 模式下吞吐量能到几十 MB/s对于驱动 LCD 和做数据缓存来说已经非常够用了。后面我会把规格、接线、驱动、实测数据和踩坑记录全部摊开讲。2. 看懂 APS6404L伪静态 RAM 的规格与机制2.1 引脚、封装和电气特性APS6404L-SQH-SN 的封装是 SOP-88 个引脚一天就能把原理图设计完。引脚功能如下VCC供电常见的是 3.3V 版本也有 1.8V 版本买料的时候一定要确认后缀标记接错电压会直接烧。GND接地。CS#片选低电平有效。SCK时钟。SI/SIO0单 SPI 模式下是数据输入QPI 模式下是双向 IO0。SO/SIO1单 SPI 模式下是数据输出QPI 模式下是双向 IO1。WP#/SIO2单 SPI 模式下是写保护QPI 模式下是双向 IO2。HOLD#/SIO3单 SPI 模式下是保持输入QPI 模式下是双向 IO3。这颗料本质上是一个长得像 SRAM 的 DRAM。它内部是动态存储单元靠内部自刷新维持数据但对外接口完全不需要你关心刷新时序。你把它当成一颗普通的 SPI SRAM 用就行发命令、给地址、读数据或者写数据完事。有一点要特别提醒PSRAM 的命令协议和我平时用的 SPI NOR Flash 很像但地址、命令码和时序有一些差异。千万不要拿 Flash 的驱动直接怼上去大概率读不出正确数据。2.2 SPI/QPI 模式下的命令与地址组织APS6404L 的容量是 64Mbit也就是 8MB 字节。8MB 地址需要用 24 位表达所以每次访问都要发 3 个字节的地址。它支持两种工作模式SPI 模式默认的单线模式SCK 一个时钟传 1 bit。QPI 模式四个 IO 同时使用SCK 一个时钟传 4 bit吞吐量理论上是 SPI 模式的 4 倍。切换到 QPI 模式通常需要先发 Write Enable0x06再发进入 QPI 模式的命令0x35。退出 QPI 模式则用 0xF5。这些命令码和常见 SPI NOR Flash 的 QPI 切换逻辑很相似但细节以数据手册为准不建议完全凭记忆硬写。读写命令方面单 SPI 模式我用得最多的是0x03普通读0x0B快速读带一个 dummy cycle0x02写QPI 模式下命令、地址、数据都是四线传输命令码可能和单线模式不同所以在驱动里要显式区分当前模式。2.3 内部自刷新不需要操心的刷新时序很多人第一次用 PSRAM 都会问一个问题DRAM 不是需要周期性刷新吗这个要不要我自己定时刷新答案是不需要。APS6404L 内部自带刷新电路外部完全不感知。你不需要分配定时器去刷它也不需要担心刷新时访问会冲突它会在内部自己协调。所以从使用者的角度它就是一颗静态 RAM你的代码完全不用关心断电后会丢数据这种事——它本来就是易失性的断电不保数据这点跟 SRAM 一样。还有一个需要知道的点PSRAM 的写入在命令层会有一定的访问延迟不像并行 SRAM 那样接上地址线就能出数据。因此在设计上层代码时尽量用批量读写而不是一次读一个字节否则带宽全被命令和地址开销吃掉了。这个后面写驱动部分会细说。3. 硬件接线与 PCB 上的那些细节3.1 标准 SOP-8 接法接线这件事理论上非常简单但实际做的时候有几个地方容易踩坑。先放最小系统接法VCC 接 3.3VGND 接地。CS# 接 MCU 的一个普通 GPIO软件控制片选。SCK 接 MCU 的 SPI 时钟脚。SI/SIO0 接 MCU 的 SPI COPI主机输出或 GPIO。SO/SIO1 接 MCU 的 SPI CIPO主机输入或 GPIO。WP#/SIO2 和 HOLD#/SIO3 在纯 SPI 模式下可以接上拉电阻如果要切换到 QPI 模式这两个脚也要接到 MCU 的 GPIO 上。很多开发板的 Arduino 库默认只用 SPI 模式所以 SIO2 和 SIO3 接不接影响不大。但如果你打算用 QPI 模式这两个引脚就必须连到 MCU 的可控引脚上。我在第一次画电路板的时候想省 GPIOSKIP 了这两根线后面想上 QPI 就后悔了只能飞线补。另外我在面包板上验证的时候习惯在 WP#/SIO2 和 HOLD#/SIO3 上各加一个 10kΩ 上拉电阻。SPI 模式下这两个引脚不能悬空否则可能引入随机干扰导致读写时序错乱。上拉之后能明显减少不稳定现象。3.2 电平匹配、去耦电容和 33Ω 的串联电阻电平问题是我最想强调的。APS6404L 有 3.3V 版本也有 1.8V 版本如果你的 MCU 是 3.3V IO那就买 3.3V 版本直接连。如果 MCU 是 5V IO比如老款 Arduino AVR 系列那就不能直接连需要加电平转换或者串电阻分压。PSRAM 的时钟频率摆在那里信号质量直接影响读写稳定性别在这里省事。去耦电容方面我习惯在 VCC 和 GND 之间贴一颗 100nF 加一颗 10uF并且靠近芯片电源引脚放置。PSRAM 在批量写的时候电流变化很剧烈如果去耦做不好VCC 会产生大幅度纹波轻则丢数据重则直接死机。我后面在排错案例里会单独讲一次因为电源纹波导致的随机丢字节问题。还有一个细节SCK、CS#、SIO 这几根线的串联电阻。在 PCB 上走线很短的话可以不加但如果像我一样经常用杜邦线飞线调试建议在 SCK 和 SIO 线上各串 22~33Ω 电阻。这个电阻能抑制信号反射尤其是在 60MHz 以上时钟下效果很明显。3.3 不同 MCU 平台外挂的差异我在这颗 PSRAM 上试过三种平台接线逻辑基本一致但驱动写法差别不小STM32直接用硬件 SPI DMAHAL 库自带 DMA 功能很方便。注意 SPI 时钟极性 CPOL 和相位 CPHA 要严格和 PSRAM 手册匹配我通常用 Mode 0。RP2040可以用 PIO 来实现自定义时序也可以直接走硬件 SPI但速度会被 SPI 外设限制。PIO 方案能做到更高的频率和更灵活的控制。ESP32硬件 SPI 也能用但 ESP32 的 SPI 外围设备和缓存系统有点绕需要关掉一些默认的缓存策略否则读出来的数据会有偶发错误。不同平台的 GPIO 电气特性略有差异但核心思想是一样的保证 CS# 片选时序干净、SCK 频率在 PSRAM 允许范围内、SIO 双向切换正确。硬件基础打好之后软件层面的事就相对好办。4. 驱动代码从裸机轮询到 DMA 搬运4.1 最基础的单字节读写序列先来一段最基础的裸机轮询代码。这段代码可以直接在 STM32 或者 Arduino 环境下改造成自己的底层 SPI 接口。逻辑非常简单拉低 CS#发命令发 3 字节地址然后读写数据。#define PSRAM_READ 0x0B #define PSRAM_WRITE 0x02 void psram_read(uint32_t addr, uint8_t *buf, uint32_t len) { cs_low(); spi_write_byte(PSRAM_READ); spi_write_byte((addr 16) 0xFF); spi_write_byte((addr 8) 0xFF); spi_write_byte(addr 0xFF); spi_write_byte(0x00); // dummy cycle while (len--) { *buf spi_read_byte(); } cs_high(); } void psram_write(uint32_t addr, const uint8_t *buf, uint32_t len) { cs_low(); spi_write_byte(PSRAM_WRITE); spi_write_byte((addr 16) 0xFF); spi_write_byte((addr 8) 0xFF); spi_write_byte(addr 0xFF); while (len--) { spi_write_byte(*buf); } cs_high(); }注意读命令我用的是 0x0B 快速读而不是 0x03 普通读。快速读多了一个 dummy cycle但可以支持更高的时钟频率。在低速下两者都能用但一旦把 SPI 时钟拉到 60MHz 以上建议优先用快速读。写命令 0x02 没有 dummy连续写效率相对高。这里我没有发 Write Enable 命令我在实际使用中直接读写是可以的但如果你的 PSRAM 手册明确要求写操作前必须发 Write Enable就在 CS# 拉低后先发 0x06再发写命令。4.2 切换 QPI 模式让吞吐量翻倍单 SPI 模式下最大的瓶颈就是命令和地址占用的时钟周期。每次随机读都要发 1 字节命令 3 字节地址然后才轮到数据。假设你只读 1 字节总共要 5 个字节的时钟周期才能换回 1 个字节效率极低。QPI 模式好很多。四个 IO 同时传数理论上同样的时钟频率下有效吞吐接近原来的 4 倍。切换代码大致如下void psram_enter_qpi(void) { cs_low(); spi_write_byte(0x06); // Write Enable spi_write_byte(0x35); // Enter QPI mode cs_high(); }进了 QPI 模式之后所有后续命令都必须走四线模式。你的 spi_write 函数也要换成 QPI 版本也就是四个 IO 同时写 4 bit 数据。这对底层 SPI 外设是有要求的普通 3 线 SPI 外设做不到所以我一般直接用 GPIO 位带操作来模拟 QPI 时序或者用 MCU 支持的 Dual/Quad SPI 模式。如果只是单纯想提速还有一个折中方案保持命令阶段在 SPI 模式数据阶段切 QPI。这样做实现复杂度会高一些但兼容性更好适用于不支持完整 QPI 命令的外设。4.3 DMA 批量搬运的正确姿势轮询模式下每个字节的进出都在占 CPU大量搬运时 CPU 基本被 SPI 拖死。我通常的做法是把命令 地址 数据拼成一个连续的内存 buffer然后用硬件 SPI 加 DMA 一次性发出去。以 STM32 为例读操作时我需要 DMA 同时处理 SPI 的发送和接收。发送的数据包括命令、地址、dummy以及之后的填充字节随便发什么目的是产生时钟接收的数据则丢弃掉命令和地址阶段的内容。代码伪逻辑如下void psram_dma_read(uint32_t addr, uint8_t *dst, uint32_t len) { uint8_t tx_buf[5 len]; tx_buf[0] PSRAM_READ; tx_buf[1] (addr 16) 0xFF; tx_buf[2] (addr 8) 0xFF; tx_buf[3] addr 0xFF; tx_buf[4] 0x00; // dummy memset(tx_buf[5], 0x00, len); // 填充字节 dma_transfer(tx_buf, dst, 5 len); }实际项目里我不会一次性分配 len 5 这么大的栈上数组而是用静态缓冲区或者分两段 DMA第一段发送命令和地址第二段收发数据。分两段的好处是不用拷一个大 buffer也方便处理跨页读写的缓存对齐问题。有一个坑我踩过DMA 搬运过程中 CS# 如果被其他中断来的代码拉高整个传输直接中断PSRAM 的状态机就错乱了。所以执行 DMA 的关键段时尽量把高优先级中断关掉或者用一个互斥锁保护避免日志串口、定时器回调在里面添乱。5. 实测数据、性能瓶颈与三个排错案例5.1 我测到的真实带宽我用逻辑分析仪和示波器配合在一个 STM32F4 平台上做了简单测试SPI 时钟分别设到 30MHz 和 60MHz结果如下模式时钟频率4KB 连续读耗时估算带宽SPI30MHz约 3.7ms约 1.1 MB/sSPI60MHz约 2.1ms约 1.9 MB/sQPI60MHz约 0.6ms约 6.8 MB/s这里测的是纯读取数据耗时不包含上层 memcpy 和业务计算。理论上 QPI 模式在 60MHz 下上限应该接近 30MB/s但实际中 PSRAM 内部有预取和刷新逻辑命令切换也不是完全零开销能跑到 6~10MB/s 已经算不错了。如果时钟频率能拉到 100MHz 以上再优化一下地址连续性和 DMA 缓冲区对齐冲到 15MB/s 是有可能的。对驱动 LCD 来说这个带宽是够用的。320x240 RGB565 全屏刷一帧的数据量是 150KB按 6.8MB/s 算大约 22ms 一帧再加屏幕刷新本身的延迟跑个 30fps 左右的简单界面问题不大。5.2 坑 1读 ID 拿回 0xFF 的完整排查第一次上电我写了一个简单的读 ID 函数想确认 PSRAM 是否正常响应结果读回来的 ID 是 0xFF 0xFF 0xFF。排查过程花了不少时间这里直接复盘先量供电VCC 3.3V 正常GND 连通正常。再用示波器看 CS#发现片选拉低时间和 SCK 的关系有点奇怪但还在逻辑正常范围。然后上逻辑分析仪抓完整波形发现 SPI 时钟极性和相位是模式 3而我的代码发出去的命令也是在模式 3 下写的。PSRAM 虽然支持模式 0 和模式 3但我的手写 GPIO 模拟时序里数据建立和采样沿没算对导致命令根本没被 PSRAM 正确识别。最后把所有 SPI 通讯统一改成模式 0CPOL0CPHA0代码里也强制指定问题立刻消失。这个问题给我提了个醒很多 SPI 外设默认配置其实是从别的驱动里继承来的换一颗芯片之后必须先确认时钟模式不能想当然。5.3 坑 2写入偶尔丢字节问题出在电源纹波有段时间在跑连续写测试每写 10KB 数据再读回来校验时总会出现几个字节对不上但位置不固定。我第一反应是代码逻辑有 bug反复检查 DMA 配置和地址计算都没问题。后来用示波器挂 VCC发现写突发开始瞬间PSRAM 的 VCC 上出现一个接近 200mV 的负向毛刺周期正好和写 burst 的重复频率对上。问题找到了我的 PCB 上 PSRAM 的去耦电容离芯片太远而且只有一颗 100nF根本没有足够电荷支撑突发写电流。解决办法是在 PSRAM VCC 和 GND 之间就近补了一颗 10uF 陶瓷电容又在 SCK 和 GND 之间加了一颗 4.7nF 的滤波电容抑制时钟沿上的振铃。之后再跑压力测试写满 8MB 再读回校验零错误。5.4 坑 3DMA 被中断干扰引发的随机错误还有一次更隐蔽系统里加了一个高频率的定时器中断每 50us 触发一次中断里会做一点日志缓存。结果 PSRAM 的 DMA 读操作开始出现随机性错误错误位置总在 DMA 传输的中间段。查完波形发现中断服务程序里无意中调用了 SPI 外设的重配置函数虽然它操作的是另一个 SPI 实例但芯片内部的 SPI 时钟总线有共享部分导致 PSRAM 的 SCK 上多了一个毛刺片选还没拉高PSRAM 就以为收到了多余的命令状态机错乱。解决办法很粗暴DMA 传输期间屏蔽所有会操作 SPI 外设的中断并且规定任何中断服务程序里不允许访问 SPI 总线。把这套规矩写进代码后再也没出过这个问题。6. 外挂这 8MB 后可以玩些什么6.1 给 LVGL 当帧缓冲和资源仓库我最满意的用法是给 LVGL 当帧缓冲。之前 150KB 的 RGB565 帧缓冲在内部 RAM 里要省着用现在直接放到 PSRAM 里去内部 RAM 全部留给业务逻辑和任务栈。LVGL 的 draw buffer 甚至可以做双缓冲用 ping-pong 的方式刷屏画面撕裂明显减少。PSRAM 有 8MB除了帧缓冲之外还能把字体、图标图片、中文点阵全部存在里面上电加载不用每次从 Flash 读取解码。LVGL 运行效率提升非常明显至少从一卡一卡变成相当流畅。6.2 当大容量环形缓冲和音频缓存另一个适合的场景是做数据记录和音频缓存。比如一段 16bit 44.1kHz 单声道的音频8MB 大概能存下至少 40 秒的原始 PCM 数据。MCU 采集完再一起通过网络或者 TF 卡导出比实时流式处理灵活很多。我之前用的传感器数据环形缓冲内部 RAM 只能开 16KB 的数组记录频率高了就丢数据。放到 PSRAM 之后开了 2MB 的环形队列再配上 DMA 自动搬运连续采集几个小时都不会丢点。这个体验和之前完全不是一个量级。6.3 一点个人建议最后说点个人体会。给 MCU 外挂大内存这件事门槛其实不高但有个前提你得提前规划好数据通路。PSRAM 毕竟是 SPI 接口适合批量搬运不适合像内部 RAM 那样频繁随机访问。如果你把每个变量都丢到 PSRAM 上程序跑起来反而会慢得离谱。合理的设计是高频率访问的变量留在内部 RAM大块数据缓存、帧缓冲、历史数据放在 PSRAM。把 PSRAM 当成数据仓库而不是运行内存体验会好很多。我实际用下来APS6404L-SQH-SN 这颗料作为 MCU 的外拓内存是相当稳的。如果你最近也在被内存不够用折磨不妨试试给开发板外挂这么一颗 8MB 的大内存硬件改动不大收益却非常明显。等你真正跑起来大概就和我一样再也回不去抠内存的日子了。