搞过带界面嵌入式产品的人十有八九都遇到过同一个问题算力够了Flash 也够唯独 RAM 不够。明明只是加个菜单、刷个动画MCU 里那块 SRAM 就捉襟见肘。前阵子做项目手里正好有一颗 APS6404L-SQH-SNQSPI 接口64Mbit也就是 8MB。当时我的想法很简单既然 DDR 上不了、SD 卡太慢那把一颗 PSRAM 当“外挂内存条”用是不是就能绕开内置 SRAM 的瓶颈实际做完之后我只能说这思路真能救项目。这篇把从硬件连线到软件驱动、再到性能调优和踩坑记录完整写下来给同样想给 MCU 扩内存的朋友一个可照抄的参考。1. 为什么要给 MCU 外挂 PSRAM1.1 内置 SRAM 不够用的真实现状我最早被内存卡脖子是做一个 800×480 的 RGB LCD 界面色彩格式用 ARGB8888一帧画面要 1.5MB 左右。如果想把界面刷新得顺滑至少得做双缓冲那就是 3MB 直接没了。可是手头 MCU 的内置 SRAM 只有 512KB连单缓冲都塞不下更别提还要给系统任务栈、消息队列、协议栈留空间。那段时间我试过压缩位深、降分辨率、减少控件重绘区域效果都很勉强动画明显钝菜单滑动起来一卡一卡的。后来我又试过把显存放到 SPI NOR Flash 里。听起来可行实际上行不通NOR Flash 写之前要擦除擦除粒度是 4KB 起步一个界面重绘就要反复擦写寿命和速度双双崩盘。而且 Flash 随机写速度太慢一帧数据按 SPI 时钟 80MHz 算也要几十毫秒人眼一对比就知道画面撕裂严重。这个经历让我意识到缺 RAM 这事不能用存储去硬顶得用真正的 RAM 解决。1.2 为什么不是并行 SRAM也不是 DDR既然要扩 RAM最直觉的方案是挂一颗并行 SRAM。比如常见的 16 位 IS62WV51216512KB 容量价格不便宜引脚还特别多16 根数据线19 根地址线再加上片选、读写、使能等控制线二十多根 IO 直接占满小封装的 MCU 根本接不动。DDR 就更别提了需要专门的控制器 IPPCB 布线讲究阻抗匹配和等长MCU 几乎不可能直接用 IO 去驱动。QSPI PSRAM 的优势恰好在这里。它只需要时钟、片选、四根数据线外加电源和复位一共六根信号线就把 8MB 容量带起来了。任何带 QSPI 外设的 MCU不管是 STM32、GD32、ESP32-S3 还是雅特力、华大这类国产平台都能比较轻松地驱动。所以这颗 APS6404L-SQH-SN 在我眼里就是一个“引脚很省、容量刚刚好”的外挂内存条。1.3 8MB 属于大内存架构里的哪个级别顺便提一嘴最近总看到有人聊“本地部署千问 3.5 27B 要多大内存”那动辄几十 GB 的权重规模跟 MCU 场景完全不是一个量级。但“大内存架构”这个思路放在嵌入式里是通的MCU 拿 8MB 不是为了装大模型权重而是当中间缓冲层。比如轻量级语音识别模型、KWS 唤醒词模型权重通常几十 KB 到一两 MB这些放 Flash 没问题但推理时随机读取权重会导致大量等待放 PSRAM 之后整体推理速度提升非常明显。换句话说8MB 当“短期记忆工作区”非常舒服这不是堆容量而是解决内存架构中“随机访问性能”和“容量缺口”两个实际问题。2. 认识 APS6404 这颗“伪静态 RAM”2.1 PSRAM 与 SRAM、Flash 的本质区别APS6404 全称叫 PSRAMPseudo Static RAM伪静态随机存储器。内部结构其实是 DRAM 的一个变种靠电容存储电荷所以需要定期刷新但芯片内部自己做了刷新管理和时序控制对外接口完全按照 SRAM 的行为来呈现随机读写、按字节访问、写进去马上能读回来不需要擦除。这就把 DRAM 的容量优势跟 SRAM 的使用体验结合到了一起。跟 SPI NOR Flash 对比会更直观Flash 内部是浮栅晶体管写数据前必须擦除擦除是按扇区来的随机写非常痛苦。PSRAM 没有擦除概念写一个字节就是真的写一个字节8MB 容量随便刷也没有 Flash 那种十万次左右的擦写寿命问题。所以用在大缓冲区场景PSRAM 的体验跟内部 SRAM 基本一致只是走的是串行接口速度慢一些。2.2 引脚定义和电压等级这里藏着一个大坑APS6404L-SQH-SN 这个型号里L 表示低电压版本工作电压是 1.8V如果是 APS6404S 系列工作电压才是 3.3V。这个细节非常重要我身边真有同事只看了“APS6404”开头直接按 3.3V 去设计电源和电平硬件回来一上电芯片发烫读 ID 全是乱码。所以拿到芯片第一件事就是确认后缀到底是 L 还是 S。常规引脚一共就这么几类SCLK时钟输入最高可以到 133MHz实际频率看 PCB 走线质量。CEN#片选低有效跟 SPI Flash 的 CS# 用法一致。DQ0/DQ1标准 SPI 模式下的 DO/DIQSPI 模式下作为 IO0/IO1。DQ2/DQ3QSPI 模式下才用到的 IO2/IO3普通 SPI 模式下可以悬空或做 WP#/HOLD#。RESET#复位脚低有效正常工作时通过 10kΩ 电阻上拉到 VDD。VDD/VDDQ供电L 版本按 1.8VS 版本按 3.3V具体以数据手册为准。GND接地。我实际接线时一般这么连SCLK 接 MCU 的 QSPI_CLKCEN# 接 QSPI_CSDQ0~DQ3 接 QSPI 的 IO0~IO3RESET# 接 MCU 一个 GPIO上电初始化后拉高这样如果需要软件复位芯片也能控制。如果不想占这个 GPIO直接 10kΩ 上拉就行只是没有软复位能力。2.3 硬件连接与 PCB 布局的几个要点QSPI PSRAM 走线要求比普通 SPI Flash 严格尤其是跑 80MHz 以上时钟时。我的经验是DQ0-DQ3 尽量等长线和线之间优先包地尽量缩短 MCU 到芯片的走线距离最好控制在 2cm 以内超过 5cm 就开始有概率出错时钟线周围不要走大电流开关信号避免噪声耦合。有条件的话在四根数据线上各加一颗 22Ω 到 33Ω 的串联电阻能明显减少振铃代价是信号沿变缓但 80MHz 以内完全没问题。电源部分同样别图省事。APS6404 虽然电流不大但突发读写时电流变化很快靠近芯片电源引脚放一颗 0.1μF 高频去耦电容再加一颗 4.7μF 或者 10μF 的钽电容这是底线。如果供电线绕得远还应该在电源入口处加一个磁珠防止 PSRAM 的开关噪声窜到 MCU 的模拟电源上。我第一次画板子时把去耦电容放在了离芯片两厘米外结果读数据偶发出错加了就近电容后直接消失这就是典型的电源布局问题。3. 软件驱动与初始化流程3.1 上电后为什么不能直接读写很多人第一次接触 PSRAM以为跟 SPI Flash 一样上电就能读。实际上 APS6404 上电后内部电路要稳定而且工作模式默认是比较保守的 SPI 模式不是我们想要的四线 QSPI 模式。你要是不做任何初始化直接按 QSPI 四根数据线去传命令大概率读不到想要的数据。所以驱动第一步是先按标准 SPI 模式发几个配置命令把芯片切到 QSPI 模式并且把读延迟、burst 长度等参数设置好之后才能高效访问。这个配置过程不复杂但顺序比较敏感。我习惯这样处理先给 1.8V 上电等 10ms 左右让电源稳定然后把 RESET# 拉高释放复位再发读 ID 命令确认芯片在线最后发配置命令进入 QSPI 模式和设置等待周期。需要注意的是每次上电都要重新做一遍配置因为它没有非易失寄存器配置信息掉电即失。3.2 配置寄存器与命令码示例下面这段代码是基于常见 QSPI 外设的伪代码具体命令码各个批次可能有细微差异务必以芯片数据手册为准。我这里是展示整个流程长什么样/* 省略具体 MCU 的 QSPI 外设初始化时钟、引脚复用、片选极性等 */ void aps6404_init(void) { /* 1. 等待电源稳定 */ delay_ms(10); /* 2. 复位释放RESET# 由低拉高 */ gpio_reset_pin(APSRESET_PIN, HIGH); delay_ms(1); /* 3. 发送 0x9F 读 ID确认芯片在线 */ uint8_t id[8] {0}; qspi_transmit_receive(0x9F, 8, id); printf(APS6404 ID: %02X %02X %02X\n, id[0], id[1], id[2]); /* 如果 ID 异常检查供电、电平、接线 */ /* 4. 写配置寄存器进入 QSPI 模式并设置等待周期 */ /* 命令码和配置位参考 datasheet常见写法是 0x35 加配置字 */ qspi_send_command(0x35, 0x0004); /* 5. 配置完成后可以做一个简单的写读回验证 */ uint8_t test 0xA5; qspi_write_byte(0x00000000, test); uint8_t read_back qspi_read_byte(0x00000000); if (read_back ! test) { printf(APS6404 r/w check failed\n); } }这个流程里最容易被忽略的是片选的默认电平。QSPI 外设初始化时如果片选极性配置反了发给芯片的命令根本进不去表现就是读 ID 永远返回 0xFF。我调试时喜欢先把时钟降到 1MHz 甚至更低排除信号质量和时序问题确认命令通路顺畅了再往上提频率。3.3 把 PSRAM 接入裸机内存池和 RTOS heap驱动能读能写之后真正的工程问题是怎么把这 8MB 用起来。裸机环境下最简单粗暴的方式是在链接脚本里把一块地址区域定义为“外部 RAM”然后自己实现一个内存池把 malloc 指向它。如果你的 MCU 支持 memory-mapped QSPI也就是把外设地址空间映射到 CPU 寻址范围那更省事直接把 PSRAM 的起始地址当作一个普通全局数组指针甚至可以把大数组直接定位到这块区域。我用 RTOS 的时候通常的做法是把 PSRAM 的一部分划给系统堆另一部分留给显存或大缓冲。比如在 FreeRTOS 里用 heap_4 的时候可以专门给一个分段内存池不要让所有 malloc 都走 PSRAM因为有些驱动要求内存物理连续且访问延迟低放 PSRAM 反而拖慢速度。建议的分配策略是任务栈、中断相关缓冲、驱动需要高频访问的小数据结构放内部 SRAM帧缓冲、音频环形缓冲、大数据缓存放 PSRAM。这样各取所长。4. 性能实测这 8MB 到底能跑多快4.1 理论带宽和实际吞吐QSPI 接口是四条数据线同时传时钟 80MHz 时理论上每秒能传 80M × 4bit 320Mbit 40MB/s时钟到 133MHz 就是 66.5MB/s。但实际跑起来不可能满速因为每次访问都有命令头、地址、等待周期还有 QSPI 是半双工读和写不能同时进行。我在一块设计良好的板子上实测过操作模式时钟频率实测吞吐备注1MB 顺序读80MHz约 32MB/s无 DMACPU 搬运1MB 顺序写80MHz约 24MB/s写延迟比读大4KB 块随机读80MHz约 18MB/s地址切换开销明显1MB 顺序读133MHz约 45MB/s需要优化布线否则不稳定从这组数据能看出PSRAM 的顺序读写性能远远超过 SPI Flash 的读取速度更不要说 Flash 写入时的擦除耗时。但它跟内部 SRAM 还是有差距的内部 SRAM 通常能跑到几十到几百 MB/s 级别所以只适合当“慢一点但容量大的内存”不适合放要求极致低延迟的操作。4.2 影响吞吐量的三个关键因素第一个是命令开销和等待周期。QSPI 读操作需要先发命令、发地址再插入等待周期最后才开始读数据。等待周期配置少了时钟沿和数据对齐不上读出错误配置多了带宽被白白浪费。这个参数需要根据实际时钟频率和 PCB 走线长度去调。第二个是地址递增方式。连续地址读的时候芯片内部可以预取吞吐明显高如果每次跳到不同页页切换会引发额外等待。所以设计数据结构时尽量让批量数据在内存里按顺序存放避免频繁跳页。第三个是 DMA 还是 CPU 搬运。CPU 手动搬运一次数据需要执行多条指令中间还要读状态寄存器经常跑不满 QSPI 带宽。用 DMA 后CPU 只需要发起一次传输数据由总线直接搬运实测吞吐能提升 30% 以上。所以只要 MCU 支持 DMAPSRAM 的数据搬运就应该交给 DMA。4.3 我实际用的几个调优参数如果 PCB 走线短且干净我会直接上 133MHz 时钟如果走线稍长或者板子干扰多就保守一点用 80MHz。等待周期我一般从数据手册推荐值附近开始然后做压力测试连续读写 1MB 随机数据校验是否正确如果出错就加等待周期或者降频直到稳定。burst 长度也值得调有 8/16/32/64 等选项越长越适合批量搬运但短突发随机访问时反而因为预取过多而浪费所以按应用场景来选显存搬运选长 burst小块随机读选短 burst。另外如果 MCU 支持 memory-mapped QSPI强烈建议打开这个模式尤其是跑 LVGL、TouchGFX 这类 GUI 框架的时候。因为 GUI 内核会频繁访问像素数据如果你每次都用命令函数去读写那调用开销就够吃一壶了映射成内存之后框架可以直接用指针访问相当于把 PSRAM 当成普通内存用代码改动量小得多。5. 实战案例与工程集成方法5.1 RGB LCD 显存外移动画流畅度直接上台阶我那个 800×480 的 RGB 屏项目最终是这么分工的MCU 内部 SRAM 放系统任务、GUI 框架内核的工作内存PSRAM 里划分两块各 1.5MB 的区域作为显示双缓冲。DMA 把 PSRAM 中的当前帧搬到 RGB LCD 接口GUI 在后台往另一块缓冲画图画完再切换。LCD 控制器持续从缓冲读取MCU 不会被刷新时序卡死。这个方案做完之后的体验变化非常明显滑动菜单不再撕裂动画帧率能稳定在 30fps 以上。而且因为 8MB 容量足够我甚至可以把很多不常用的窗口界面提前渲染成位图缓存放到 PSRAM 里切换界面时直接用 DMA 拷贝一整块像素速度比现场重绘快得多。这在以前只有 512KB SRAM 的时候想都不敢想。5.2 音频采集与播放的环形缓冲另一个典型场景是音频。一个 48kHz、16bit、单声道的音频流一秒钟的数据量是 96KB。之前的方案是边采边处理中断频率高CPU 被频繁打断数据稍微一来不及处理就丢。把环形缓冲放到 PSRAM 之后我可以把采样数据一路往 PSRAM 里写攒够一个比较大的数据块再批量处理或者搬运中断频率大大降低数据连续性也变好了。如果是播放场景也一样把整段 WAV 文件或者压缩音频的解码输出缓冲放在 PSRAM解码器一边解码一边写入I2S 中断从 PSRAM 读数据送去播放两者互不卡顿。8MB 的容量对 85 秒左右的 PCM 立体声音频没问题对缓冲方案更是绰绰有余。5.3 轻量本地推理模型的临时缓存最近嵌入式上跑 AI 模型的场景多起来了最常见的是语音唤醒和简单图像分类。模型权重如果放 Flash推理时如果频繁随机读取权重Flash 随机访问延迟非常高一次卷积可能被卡得明显停顿。把权重拷到 PSRAM 之后随机访问速度快了一个数量级推理耗时改善明显。另外模型推理过程中的中间特征图就是在网络层之间流转的数据这些数据往往比较大尤其做图像分类输入一张 224×224×3 的图经过卷积输出一堆特征图每个特征图几十 KB 到几百 KB全部堆在内部 SRAM 不现实。把这些中间张量放在 PSRAM只在最终结果回传时用内部 SRAM整个推理就能顺畅跑起来。这也是为什么我会强调8MB 不是用来“装下大模型”而是用来“给推理过程当临时工位”。6. 常见问题与排查实录6.1 读 ID 总是 0xFF 或者乱码这个问题最常见我遇到的时候一般按顺序排查先看供电确认是 1.8V 还是 3.3V 版本用万用表量芯片电源角再看片选确认上电后 CEN# 不是浮空或者被意外拉低然后查时钟线用示波器看 SCLK 上有没有正常的方波信号最后把时钟降到最低再做读 ID 测试。实际项目里我碰到过两次都是因为片选极性配反还有一次是 RESET# 悬空导致芯片一直处于复位状态。6.2 数据读回偶发错误前面正常后面跳变这类问题跟信号完整性关系最大。表现为大量数据搬运时中间偶尔出现某个字节变成 0x00 或者 0xFF。第一次遇到时我还怀疑芯片体质不好后来仔细查了 PCB发现 DQ2 走线比其他三根长了很多而且经过一个过孔换层周围还有一路 PWM 信号。把 DQ2 走线调整之后问题消失。如果硬件改不了软件上的应急措施是把时钟从 133MHz 降到 80MHz基本能压住大部分信号质量问题。6.3 PSRAM 掉电丢数据不能当存储用这个看似常识但真有人把配置参数往 PSRAM 里写断电再开发现丢了然后怀疑是“写入没生效”。PSRAM 的本质是 DRAM 变化掉电不保存数据只能在运行期间用。所以工程上要区分角色Flash 存代码和持久化参数PSRAM 只做运行时的临时数据。如果需要在掉电前保存关键数据一定要先搬到 Flash 或者外部 EEPROM。6.4 和 SPI Flash 共用 QSPI 总线时的冲突很多 MCU 只有一个 QSPI 控制器外面同时挂了 NOR Flash 和 PSRAM靠片选切换。听起来没问题实际用起来要注意切换片选后芯片的命令模式是独立的Flash 可能处于 QPI 模式而 PSRAM 可能还停留在配置前的状态直接读会互相干扰。我的做法是每次切换芯片后先重新发送目标芯片的读取命令或者在系统初始化时统一设置好两个芯片的模式避免运行中来回切。6.5 把代码放在 PSRAM 里执行导致系统崩溃有些 MCU 支持从外部内存执行代码也就是 XIPPSRAM 理论上也可以映射进来。但实际操作中代码放在 PSRAM 里执行运行效率往往不高而且如果 QSPI 被 DMA 频繁占用取指和 DMA 搬运会互相抢占总线导致系统卡顿甚至崩溃。我的原则是代码优先放内部 Flash 或内部 SRAMPSRAM 只放数据。中断服务函数绝对不能放 PSRAM否则中断响应延迟会高到不可接受。最后再分享一个实操习惯每次拿到新的 PSRAM 板子我先不接 GUI 或者音频逻辑单独跑一个内存自检程序用伪随机序列写满整个 8MB再读回校验循环跑一百轮。这个步骤能快速暴露供电、走线、时序配置的大部分问题。等自检稳定了再把它接入到业务代码里。磨刀不误砍柴工这一步省掉的话后续排查问题会难受得多。