毫不夸张地说,外部存储总线往往是嵌入式系统里最容易被低估的瓶颈。很多项目CPU主频跑到几百兆,内存带宽也够,但一旦从外挂Flash读大块数据,整体性能立刻被打回原形。SST26VF064B和R7KA8D2KFLCAC这套组合,正好就是冲着“数据吞吐量”这个痛点去的:一颗是能跑到104MHz四线读的NOR Flash,另一颗是带xSPI高速存储控制器的高性能MCU。这篇内容会把原理、硬件连接、软件配置、性能优化和踩坑记录一次讲透,适合正在做GUI资源加载、音频素材回放、OTA升级、数据记录等场景的嵌入式工程师参考。
1. 认识两个关键器件:先搞清楚它们各自擅长什么
1.1 SST26VF064B:一颗能跑104MHz四线读的NOR Flash
SST26VF064B是Microchip旗下SST系列的一款64Mbit串行NOR Flash,换算过来是8MB容量。虽然容量放在今天不算大,但它的定位从来不是“装很多数据”,而是“把数据尽量快地读出来”。这颗片子支持单线、双线和四线SPI接口,最高工作时钟104MHz,在Quad I/O模式下理论峰值带宽能到52MB/s,这个速度在传统SPI NOR Flash里已经属于第一梯队。
需要多说一句的是,这颗Flash还支持SFDP(Serial Flash Discoverable Parameters),也就是说主控可以通过标准SFDP命令读取它的能力描述,自动识别命令集、等待周期、扇区大小等参数。这一点对于像RA8D2这样带xSPI控制器的主控特别友好,不用在代码里写死一套Flash专属参数。
除了读取快,SST26VF064B在低功耗和数据可靠性上也有不错表现,支持深度掉电模式、100K次擦写寿命、100年数据保持。供电电压根据后缀不同有1.8V和3.3V两个大类,选型时一定要和MCU的IO电平匹配,这点后面硬件章节会重点提醒。
1.2 R7KA8D2KFLCAC:以xSPI为核心看点的高性能MCU
R7KA8D2KFLCAC这个料号对很多人来说可能有点长,简单理解就是瑞萨RA8D2系列中的一款高配MCU,简称RA8D2平台。它基于Arm Cortex-M85内核,最高主频可以跑到480MHz,这在MCU领域已经属于“性能怪兽”了。但真正让它和“无与伦比的数据吞吐量”这句话产生关联的,是它集成的xSPI存储控制器。
xSPI控制器是串行Flash接口的高端形态,它不像普通SPI外设那样只支持单线或多线固定模式,而是支持灵活配置数据线宽度、指令序列、待机周期和内存映射地址。配合Cortex-M85本身较宽的存储总线、可选的Cache以及DMA控制器,RA8D2可以把外部NOR Flash映射成一段普通内存地址,CPU直接像访问SRAM一样去访问外部Flash,数据搬运还能完全交给DMA后台执行。
除xSPI之外,RA8D2还集成了网络、图像显示接口、模拟前端等丰富外设,所以它并不是单纯为“跑分”设计,而是真的可以支撑工业HMI、音频处理、边缘网关这类需要大量实时数据交换的应用。
1.3 这套组合适合谁:从GUI到OTA的典型场景
把SST26VF064B和RA8D2放在一起,最典型的受益场景有这么几类:
- GUI界面资源加载:图片、字库、图标等资源体积大,要求切换界面时快速读取,否则卡顿感非常明显。
- 音频素材播放:音频数据块从Flash到内存再到DAC,吞吐不足会造成爆音或者加载延迟。
- 启动镜像和执行代码:MCU可以从外部xSPI Flash映射执行代码,代码读取速度和Cache命中率直接决定启动时间。
- OTA与数据日志:需要把升级包或日志数据快速写入Flash,同时不能长时间阻塞系统其他任务。
这几种场景归纳起来就一句话:系统需要一块容量适中、读取快、接法方便的外部存储,而SST26VF064B和RA8D2恰好在接口和性能上高度互补。
2. 为什么SPI Flash会成为瓶颈:吞吐量到底卡在哪里
2.1 从单线到四线:接口模式的带宽账
很多人在评估SPI Flash吞吐量时,拿到的第一印象就是“SPI很慢”。其实这个认知多少是被传统单线模式误导了。以SST26VF064B为例,如果老老实实用最普通的0x03读命令,一次只能从一根数据线上取数据,104MHz下有效带宽折算下来约13MB/s。这个速度在几十KB级别的系统里够用,但面对几百KB甚至数MB的连续资源,等待时间就很可观了。
改用Fast Read 0x0B命令后,地址阶段和数据输出还是单线,但增加了地址后的等待周期,频率可以跑得更高,不过带宽本质上没有质的提升。真正拉开差距的是Quad Output Fast Read(0x6B)和Quad I/O Fast Read(0xEB)命令。0xEB命令在指令、地址阶段就使用四根数据线并行输入,数据输出阶段也是四线同时翻转,所以在相同104MHz时钟下,理论带宽直接到52MB/s,是普通读模式的4倍。
这里的本质逻辑很简单:时钟频率决定了每一位信号翻转的速度,而数据线数量决定了每个时钟周期能同时传出多少位。SST26VF064B之所以强调四线能力,就是为了在不继续提高时钟频率、不增加系统时序压力的前提下,把接口利用率拉到最高。
2.2 主控侧的隐藏开销:指令头、等待周期与缓存
接口模式决定了Flash侧的理论上限,但真正能拿到多少吞吐,还要看主控侧把这些数据接走时效率高不高。这里有几个容易被忽视的隐藏开销。
第一是指令头开销。每次读操作不只是数据输出,前面还得有命令字节、地址字节,某些命令还需要额外的等待周期(dummy cycles)。如果是随机地址小批量读取,指令头和数据体比例会非常难看,大量时间都花在“摆姿势”而不是真正传数据。所以SST26VF064B支持连续读模式,在地址连续时省掉重复的命令和地址阶段,这个特性在顺序读取大块资源时非常有用。
第二是CPU搬运开销。如果CPU用轮询方式一个字节一个字节地读,再存到内存里,即使Flash端有52MB/s能力,CPU也大概率被拖死。正确做法是用DMA把数据从xSPI接口的外设地址搬运到SRAM,CPU只在传输完成时收到中断。
第三是缓存命中率。Cortex-M85这类内核带有Cache,如果xSPI映射区被配置成Cacheable,连续执行/读取时很多访问可以命中缓存,实际感受到的读性能会远高于Flash接口本身。反过来,如果Cache策略配错,每次读都穿透到Flash,吞吐量就会又被打回原形。
2.3 把目标定准:单通道能到多少、双通道能到多少
按照SST26VF064B的数据手册参数,104MHz、四线Quad I/O模式、排除指令头损耗后的理想持续读取带宽是52MB/s。实际工程中,考虑到命令头、dummy cycle、刷新、总线上其他设备等开销,单通道稳定跑到40~45MB/s是非常正常的。
如果你觉得还不够,可以考虑双Flash分通道并行。将两颗SST26VF064B分别挂在主控的两个独立存储控制器或两个独立SPI主机端口上,数据在软件层做条带化分布,理论上峰值可接近2×52=104MB/s,实际工程值做到70~80MB/s就算相当成功。注意,这里强调的是“两个独立主机端口”,单纯把两颗Flash挂在同一个xSPI总线的不同片选上只能分时访问,带宽并不会翻倍。
3. 硬件连接与选型细节:想跑得快,先搭好路
3.1 引脚对照与最小连接:xSPI如何接SST26VF064B
把RA8D2的xSPI接口和SST26VF064B连接起来,看似只是拉几根线,但有几处不能想当然。SST26VF064B的引脚包括CS#、SCLK、SI/IO0、SO/IO1、WP#/IO2、HOLD#/IO3、VDD、GND。
对应到RA8D2的xSPI端口,通常是这样连接:
| SST26VF064B引脚 | 功能 | RA8D2 xSPI对应信号 |
|---|---|---|
| CS# | 片选 | xSPI CS0 |
| SCLK | 时钟 | xSPI SCK |
| SI/IO0 | 数据线0 | xSPI IO0 |
| SO/IO1 | 数据线1 | xSPI IO1 |
| WP#/IO2 | 数据线2 | xSPI IO2 |
| HOLD#/IO3 | 数据线3 | xSPI IO3 |
| VDD | 电源 | 对应电压域电源 |
| GND | 地 | 系统地 |
这里特别提醒,WP#和HOLD#在普通SPI模式里是写保护和暂停功能,但一旦切换到四线模式,这两个脚就被复用的数据线IO2和IO3。如果芯片在上电初始化阶段没有正确开启Quad模式,或者这两个引脚被外部电路强拉到了错误电平,Flash可能根本没法进入四线状态,读取会一直停留在单线模式。
3.2 双Flash并行方案:两块SST26VF064B怎么做吞吐叠加
想进一步提高吞吐量,一个实用思路是把两颗SST26VF064B接到两个独立存储控制器端口上。RA8D2这类芯片虽然xSPI接口强大,但不一定每个型号都有两个完整xSPI控制器,所以实际设计中可以灵活处理:一个端口用xSPI接主Flash,另一个端口用普通QSPI外设接从Flash。两片Flash在逻辑上可以做成独立分区,也可以做成地址交替的条带化存储。
条带化听起来高端,但工程成本不低。你需要把连续的地址块拆成两份,写数据时按奇偶块分到两个Flash,读数据时再交叉拼接。这样做对控制器、DMA和缓存一致性都提出了更高要求。我的建议是,只有当你把单通道的读取优化做到极致、确实需要再翻倍时才上这个方案;否则更划算的做法是用两片Flash做不同角色的分离,比如A片存代码和静态资源,B片存日志和OTA包,这样读写互不阻塞,系统整体的“有效吞吐”反而更高。
3.3 100MHz级SPI总线的PCB注意点
当SCLK跑到104MHz时,这已经不再是我们印象里的“慢速SPI”了,PCB设计必须按高频信号来对待。几个重点:
- 四条数据线IO0~IO3尽量做到等长,长度差控制在几百密耳以内,避免数据线和时钟到达Flash的时间错位过大。
- SCLK串联22~33Ω电阻,通常放在主控侧,用于抑制过冲;如果系统是1.8V电平,电阻值可以适当调小甚至去掉。
- Flash的VDD引脚旁边放一个100nF去耦电容,位置尽量贴近电源引脚;如果供电条件一般,建议再加一个4.7~10μF的体电容。
- 尽量减少信号线上的过孔和stub,不要为了布局美观把Flash放得离MCU特别远。
- 如果两块Flash都要跑高速,注意它们的地平面必须完整,不要在大电流或开关节点附近穿越。
这些点看起来不起眼,但实际调试中很多“偶发读错、吞吐上不去、高速下不稳定”的问题,最后都能在PCB设计上找到根源。
4. 软件配置与吞吐优化实战:把每一MB/s抠出来
4.1 第一步:让xSPI识别并初始化SST26VF064B
RA8D2的开发通常基于瑞萨FSP(Flexible Software Package)环境。FSP里提供了xSPI驱动,初始化步骤大体是配置通道、引脚、传输宽度、时钟分频和协议模式。
典型的初始化流程如下,代码以FSP驱动API为例,不同版本字段名可能略有差异,但思路通用:
/* xSPI配置结构体 */ xspi_cfg_t g_xspi0_cfg = { .xspi_cfg = NULL, /* 底层硬件配置 */ .channel = 0, /* xSPI0 */ .data_width = XSPI_DATA_WIDTH_4, /* 四线模式 */ .protocol = XSPI_PROTOCOL_QUAD_IO, /* Quad I/O协议 */ .clock_div = XSPI_CLOCK_DIV_1, .p_callback = xspi_callback, .p_extend = NULL, }; /* 打开xSPI驱动 */ fsp_err_t err = R_XSPI_Open(&g_xspi0_ctrl, &g_xspi0_cfg); if (FSP_SUCCESS != err) { /* 初始化失败处理 */ }初始化完成后,先不要急着做内存映射,先读取Flash的JEDEC ID确认通信正常。SST26VF064B的制造商ID是0xBF,代表Microchip。你可以用驱动提供的直接传输接口发送0x9F指令,然后读出3个字节:
uint8_t cmd = 0x9F; /* JEDEC ID指令 */ uint8_t jedec_id[3]; R_XSPI_DirectTransfer(&g_xspi0_ctrl, &cmd, jedec_id, 3); /* 判断 jedec_id[0] 是否等于 0xBF */如果读出来的ID是全0xFF或者全0,先检查硬件连接和片选时钟极性,不要急着往下写代码。ID能正确识别,说明引脚、时钟、电气配置都没问题。
4.2 第二步:开启Quad模式与连续读
SST26VF064B上电默认工作在标准SPI模式,要使用四线读取,必须先设置状态寄存器里的Quad Enable位。不同厂商Flash的QE位位置不一样,SST26VF064B一般是在状态寄存器bit 6。具体以数据手册的状态寄存器定义为准。
写入命令序列通常是:发送Write Enable(0x06),再发送Write Status Register(0x01)并带上新的状态寄存器值。这段操作用FSP直接传输接口也能做:
/* 假设已经有一个发送命令+数据的函数 */ uint8_t wren = 0x06; R_XSPI_DirectTransfer(&g_xspi0_ctrl, &wren, NULL, 1); uint8_t wsr_cmd = 0x01; uint8_t status_value = 0x40; /* bit6置1,其他位保持 */ uint8_t write_data[2] = {wsr_cmd, status_value}; R_XSPI_DirectTransfer(&g_xspi0_ctrl, write_data, NULL, 2);设置完QE位之后,xSPI驱动通常还需要通过SFDP读取Flash支持的读取命令、dummy cycle等参数。如果驱动支持手动覆盖,建议把读取命令固定为0xEB(Quad I/O Fast Read),并根据SST26VF064B数据手册设置好等待周期。0xEB命令的特点是命令和地址都通过四根IO传输,连续读模式下后续数据突发传输不需要重复发命令,这是拿到高吞吐的关键。
4.3 第三步:XIP内存映射与CPU缓存配置
xSPI控制器的核心优势之一就是支持内存映射读。配置好之后,外部Flash会被映射到MCU地址空间中的一段区域,比如从0x80000000开始。CPU可以直接用指针读取这段地址:
uint8_t *flash_base = (uint8_t *)0x80000000; uint32_t word = *(volatile uint32_t *)(flash_base + 0x1000);一行C代码,MCU会自动触发xSPI控制器去Flash里取数据,整个过程对软件透明。这种模式也就是常说的XIP(Execute in Place),代码甚至可以直接放在外部Flash里运行。
不过,XIP读取虽然方便,性能表现却很依赖Cache配置。Cortex-M85带有Cache时,建议把xSPI映射区域通过MPU配置为Cacheable,并且优先考虑Write-Through策略。原因很简单:外部Flash本来就是只读或只写缓存型数据,写透策略可以避免缓存回写产生的复杂一致性问题,读命中时性能又非常好。
MPU配置一般放在系统启动早期,类似这样:
/* 伪代码,实际需要按ARMv8-M MPU寄存器填写 */ MPU->RBAR = 0x80000000 | region_num | VALID; MPU->RASR = size_8MB | AP_RW_READONLY | TEX_1 | SHAREABLE_NO | CACHEABLE_WT;配完Cache后,连续读取同一段资源时,命中率会非常高,体感速度甚至接近内部SRAM。
4.4 第四步:DMA双缓冲搬运,解放CPU
XIP适合随机访问和代码执行,但如果你要把一整块资源从Flash搬到一个大Buffer里,比如开机加载字库、加载图片,最有效率的做法还是DMA搬运。
RA8D2的DMA控制器可以配置成从xSPI映射地址读取,数据写到SRAM缓冲区。合理配置DMA后,CPU只需要设置源地址、目的地址、传输长度,然后启动传输,传输完成会产生中断。双缓冲的思路是把一块大Buffer拆成A/B两个半区,DMA正在填充A区时,CPU处理B区,反之亦然,这样搬运和处理完全流水线化。
Pseudo代码思路如下:
/* 启动DMA从xSPI映射地址搬运到SRAM缓冲区 */ R_DMAC_Open(&g_dma0_ctrl, &g_dma0_cfg); uint32_t src = (uint32_t)(flash_base + image_offset); uint32_t dst = (uint32_t)buffer_a; uint32_t size = SECTOR_SIZE; R_DMAC_Start(&g_dma0_ctrl, src, dst, size);实际编码时要注意DMA源地址区域在系统总线矩阵上的属性,有些总线桥对xSPI映射地址支持突发传输,有些则效率偏低。如果测试发现DMA块传输速度反而比CPU循环读还慢,优先检查地址区域是否被配置成不可缓存或不可突发。
4.5 实测吞吐量:从设计值到工程值
理论值永远只是参考,我建议在板卡上做一个最简单的实测:把Flash全部擦除,写入固定数据,然后用DMA连续读取1MB数据,用定时器记录耗时,算出的MB/s就是实际可用吞吐。测试时要注意,不要用Flash里原有数据做测试,因为不同数据位翻转率对信号完整性和Flash内部读取时间的影响不同。
实际项目里,我见过几种典型结果:
| 条件 | 吞吐量 |
|---|---|
| 普通0x03单线读,无缓存 | 约10~12MB/s |
| Quad I/O 0xEB,无Cache,DMA无突发 | 25~30MB/s |
| Quad I/O 0xEB,开启Cache,DMA突发,双缓冲 | 40~45MB/s |
| 双Flash分通道并行,优化后 | 70~85MB/s |
单通道能稳定到40MB/s以上,这套组合的优化就已经很到位了。如果还想再压榨,方向不是继续调SST26VF064B,而是换支持Octal或DTR模式的Flash,配合RA8D2的xSPI能力做进一步升级。
5. 踩坑记录与排查速查表:这些坑我替你趟过了
5.1 明明接好了,为什么ID读不出来
这是最常遇到的第一个问题。硬件看起来都连对了,但0x9F读出来始终是0xFF或者0x00。排查顺序建议如下。
先看供电:SST26VF064B有1.8V和3.3V版本,如果MCU电平是3.3V而Flash是1.8V版本,IO电平不匹配会导致通信完全异常。对照料号后缀确认电压等级。
再看SPI模式:xSPI控制器配置里往往有CPOL和CPHA两个参数,SST26VF064B通常支持SPI Mode 0或Mode 3,如果主控初始化用了错误极性和相位,第一个时钟沿就把数据采样错了。
最后查CS#和复位:有些设计把Flash的CS#直接接地,这在单设备时看似没问题,但xSPI控制器可能用CS#电平做总线空闲判断,一直拉低反而导致控制器认为总线处于被占用状态。正确做法是让CS#由主控的片选信号驱动。
5.2 连续读丢数据,多半是等待周期没算对
Quad I/O模式读取不是发完地址就能立刻收数据,Flash内部需要几个等待周期来准备数据。这个等待周期数量因命令不同而不同,还和Flash工作频率有关。如果等待周期设置过短,主控会在Flash还没把数据输出到IO线上时就开始采样,结果就是低位连续读出现错位。
遇到“读前几个字节正常,后面越读越乱”这种经典症状,基本可以断定是dummy cycle设置有误。解决方法是查看SST26VF064B在SFDP里上报的dummy cycle值,或者直接查数据手册里0xEB命令对应频率下的推荐值,然后在xSPI驱动配置里覆盖。
5.3 吞吐量上不去,一条一条自查
如果吞吐量停留在个位数MB/s,不要怀疑芯片能力,先查配置。
第一,确认当前实际使用的读取命令是0xEB而不是默认的0x03。有些驱动为了兼容性默认走普通读,必须手动切换。
第二,确认xSPI数据宽度是否真的配成4线。如果只配了单线或双线,吞吐量会直接减半甚至更多。
第三,确认Cache是否生效。读同一个地址两次,如果耗时一模一样,说明Cache大概率没配置成功。
第四,确认DMA是否使能了突发模式。xSPI控制器和DMA配合时,突发长度对效率影响明显。
第五,看一下有没有其他外设和xSPI抢系统总线,尤其是Ethernet DMA和图形控制器,它们同时运行时可能把系统带宽吃掉不少。
5.4 程序从Flash执行时卡死:XIP与缓存一致性
把代码放在外部Flash直接执行,是xSPI最吸引人的玩法,但也是稳定性风险最高的玩法。代码执行过程中取指是连续且随机交织的,如果Flash在低速读模式下工作,取指延迟会导致CPU长时间等待;如果Cache配置成Write-Back,又可能产生脏数据问题。
我的经验是,代码执行场景下XIP区域必须配置Cacheable且为Write-Through,同时把Flash切换到Quad I/O连续读模式,等待周期要按最高工作频率设置。如果需要执行的是对时序极其敏感的中断处理代码,不建议全部放在外部Flash里,应该把关键中断函数放进内部SRAM,通过启动时拷贝实现“零等待执行”。
另外,还要注意Flash在擦写期间不能被读。如果代码在XIP运行的同时,后台有OTA任务在对同一个Flash做擦除编程,必须用驱动中的状态寄存器查询机制(0x05指令)确保Flash忙状态结束后再继续取指,否则轻则取指失败,重则系统死机。
5.5 常见问题速查表
| 现象 | 可能原因 | 解决方向 |
|---|---|---|
| ID全0xFF | 供电/IO电平不匹配、SPI极性错误 | 确认Flash电压版本、检查CPOL/CPHA |
| ID全0x00 | CS#控制异常、复位脚被拉死 | 检查CS#是否由主控驱动、复位时序 |
| 连续读偶发错位 | dummy cycle偏短 | 按数据手册调大等待周期 |
| 吞吐只有10MB/s左右 | 用了单线0x03读 | 改用0xEB命令并开启四线模式 |
| 开启Cache后数据不更新 | Cache一致性未处理 | 对写Flash后地址做Invalidate或者配置Write-Through |
| 双Flash方案没提升 | 两个Flash没有独立总线 | 检查是否挂在同一个端口的不同CS上 |
写在最后的一点体会
把这套组合真正调通的这段时间,我最大的感受是:SST26VF064B本身性能不俗,RA8D2的xSPI控制器也足够强,但两者之间任何一层配置失误,都会让最终吞吐天差地别。建议大家在项目初期就把“读取命令、等待周期、Cache策略、DMA模式”这几个关键参数固化到设计文档里,不要等到性能测试不过关再回头找原因。
如果后续还打算往上走,可以把SST26VF064B换成支持Octal或DTR模式的NOR Flash,RA8D2的xSPI控制器依然能驾驭,硬件连接上大概率也能兼容。只要软件层把Flash操作封装成独立驱动,升级存储设备时应用代码基本不用动,这套架构的生命周期会比你想象的长很多。