拿到STM32H7S78-DK这块板子之后,我第一个念头就是板载Flash不够用。虽然官方评估板出厂带了一颗XSPI接口的NOR Flash,但做UI资源存储、日志记录或者OTA备份时,容量总在关键时刻卡脖子。换一颗更大的、性能更好的外部Flash就成了顺理成章的需求。
MX66UW1G45G就是这样进入视线的。这是一颗1Gbit容量的八线NOR Flash,支持XSPI接口,理论带宽比常规QSPI高出一倍,正好配H7S系列引以为傲的外部存储扩展能力。但配置过程比想象中曲折——网上关于这块板和这颗Flash结合的中文资料很少,大多讲的是W25Q128或者MX25LM51245G,直接照搬会踩到不少隐藏的坑。这篇文章把我从硬件接线到CubeMX配置,再到实际读写擦除和故障排查的完整过程整理出来,希望能帮到正在折腾同方案的人。
1. 先搞清楚MX66UW1G45G是一颗什么样的Flash
1.1 容量、电压和封装:它为什么不是普通W25Q128
很多人选外部Flash时习惯性拿W25Q128做参照,MX66UW1G45G和这类常规QSPI Flash有本质区别,不能只当作“容量更大的W25Q128”来用。
首先是电压域。MX66UW1G45G的工作电压是1.8V,不是3.3V。这个细节决定了硬件设计:如果你把3.3V的Flash插到专为1.8V设计的XSPI总线上,不仅读取数据全乱,长期工作还会损坏芯片。STM32H7S78-DK的XSPI Flash供电走的是板载1.8V电源域,和GPIO的电平互相匹配,所以换这颗Flash时不需要额外加电平转换,但如果你自己画板子,这里一定要拿万用表确认供电电压。
其次是容量和寻址。1Gbit等于128MByte,这个容量超过16MB之后,地址线宽度就不再是普通的24位了。XSPI控制器支持4字节地址模式,但需要你在命令层面显式配置,否则访问超过16MB的地址,数据会回绕到你根本想不到的位置上。
第三是封装。MX66UW1G45G常见封装是WLCSP或BGA类小型封装,手工焊接难度比SOIC-8高不少。评测板上如果预留的是8脚封装位,你得考虑转接板或者直接选同系列其他封装型号。这倒不是Flash本身的问题,但选型阶段忽视封装,后面会非常被动。
1.2 接口模式:单线、四线、八线和DTR的差别
XSPI接口的全称是扩展串行外设接口,它在物理上复用了标准SPI的信号线,但支持多种IO模式切换。
单线SPI模式(1-1-1)是Flash上电后的默认状态,所有命令、地址、数据都走一根线,协议简单但速度最慢,最大带宽受限于时钟频率和单线传输能力。四线QPI模式(4-4-4)命令、地址、数据都走四根线,这是很多QSPI Flash的极限。而MX66UW1G45G真正的主场是八线OPI模式(8-8-8),数据相位一次传8位,配合DTR(双倍数据传输率,时钟上下沿都采样),理论带宽直接翻倍。
DTR模式需要额外留意DQS信号。XSPI控制器要读取DQS来校准数据采样窗口,否则在高速时钟下数据容易采错位。MX66UW1G45G支持DQS输出,STM32H7S78-DK的XSPI控制器也支持DQS输入,这是它能跑高频的关键。如果你只是低速读写,DTR模式可以不开,但那样就把这颗Flash的性能浪费了大半。
配置流程上的坑在于:Flash上电后默认的是单线SPI模式,你要先通过命令序列切换到OPI模式,且切过去之后,所有后续命令都必须按八线协议发,控制器这边的配置要同步。哪怕漏配一个数据引脚,命令就会变成一堆乱码。
1.3 为什么是XSPI而不是FMC/QSPI
有读者会问:STM32H7已经很能打了,为什么还要单独搞一个XSPI外设?这要从H7S系列的定位说起。
FMC(灵活存储控制器)主要用于并行NOR/SRAM/LCD接口,引脚占用多、布线麻烦,不适合做小而快的代码存储。传统QSPI控制器虽然能挂外部Flash,但大多数实现只做到四线,带宽上限摆在那里。H7S系列把原来的OSPI升级成XSPI,本质上是一个更灵活的八线SPI控制器,既能做标准SPI、QPI,也能做OPI,配合Memory-Mapped模式,可以让CPU直接像访问内部Flash一样访问外部Flash,代码直接XIP执行。
STM32H7S78-DK板载的XSPI Flash正是通过这个控制器挂载的。芯片内部BootROM甚至会在上电阶段自动从XSPI启动,这意味着你把程序放到外部Flash里,板子也能正常引导。这一点是选择XSPI方案相比普通外部存储最大的价值。
2. STM32H7S78-DK上的硬件准备与信号路径
2.1 板载XSPI引脚分配与接线核查
拿到板子第一步不是写代码,而是翻开原理图,把XSPI相关的信号线逐个找出来。STM32H7S78-DK上XSPI的引脚虽然已经连到板载Flash,但如果你要替换成MX66UW1G45G,或者外接Flash模块,必须确认引脚复用关系。
常规XSPI信号包括:
| 信号名 | 功能说明 | 注意事项 |
|---|---|---|
| XSPI_CLK | 时钟输出 | 高速模式下对信号完整性敏感 |
| XSPI_NCS | 片选,低有效 | 两个Flash可以共用总线时分复用 |
| XSPI_DQ0-DQ3 | 四线数据 / 命令地址 | 低四位数据在OPI模式下也参与命令 |
| XSPI_DQ4-DQ7 | 八线高位数据 | 仅OPI/8线模式使用 |
| XSPI_DQS | 数据选通 | DTR模式必须,SDR模式可选 |
| XSPI_RESET | 复位 | 可通过GPIO控制,也可接固定上拉 |
在STM32H7S78-DK上,XSPI信号默认接到板载Flash,数据手册里标注的引脚复用功能是确定的。如果你不换板载Flash,只是增加一颗外部MX66UW1G45G,可以把两片Flash的CLK、DQ、DQS共用,NCS分开接不同GPIO。注意两片Flash的电压域必须一致,不然总线电平冲突。
2.2 1.8V供电域和电平匹配
MX66UW1G45G是1.8V器件,这点在硬件设计上几乎是一票否决项。STM32H7S78-DK板上的XSPI Flash供电来自板载1.8V LDO或DC-DC,引脚电平也是1.8V,所以直接替换或并联第二颗Flash不会出现电平不匹配的问题。
但如果你是从零设计底板,用了支持3.3V的XSPI Flash,或者把MX66UW1G45G接到3.3V供电轨上,那就要小心了。MX66UW1G45G的绝对最大额定电压一般是2.4V左右,超过这个值可能直接损坏。办法是加电平转换芯片,比如TXS0108E这类自动方向感应的转换器,把主控侧的1.8V和Flash侧的1.8V统一,或者反过来用双向电平转换把3.3V转1.8V。只要电平域不一致,数据传输线上的信号完整性也会受影响,实测会出现偶发读错误。
2.3 焊接和布线时要注意的信号完整性
如果只是换板载Flash,焊接主要看封装。MX66UW1G45G如果是BGA类封装,建议用热风枪配合助焊剂操作,焊盘对准后加热时间不要过长,避免焊点桥接。焊完后用放大镜检查XSPI_DQ线之间有没有短路。
自己画板子的话,XSPI_CLK走线要短而直,避免过孔太多。时钟频率在100MHz以上时,过孔引入的寄生电容会改变信号边沿,导致采样不稳定。DQS信号和数据线尽量等长,偏差控制在几百密耳以内。MX66UW1G45G的八线模式下8根数据线同时翻转,如果线长差太多,采样窗口会被压缩。
另外别忘了Flash的VCC去耦电容。常规做法是每颗Flash的电源引脚放一个100nF和一个4.7uF电容,靠近引脚放置。这颗Flash容量大,写入时电流需求变化剧烈,去耦电容不能省。
3. CubeMX配置:图形化界面里最容易被忽略的几项
3.1 创建工程时的外设选择和时钟树
用STM32CubeMX创建STM32H7S78-DK工程时,最容易被忽略的是XSPI1外设的使能方式。很多人会习惯性找QSPI或FMC,结果在H7S系列上找不到QSPI选项。H7S系列的外设名是XSPI,配置界面在Connectivity分类下。
使能XSPI1后,时钟树里会出现XSPI Kernel Clock选项。H7S系列的XSPI时钟源可以来自PLL1、PLL2或HSE等,理论上可以配置到较高频率。我的建议是先不要直接拉满,把XSPI时钟输出频率设在100MHz上下(SDR模式)或者50MHz(DTR模式),先把功能跑通,再慢慢拉高验证稳定性。晶体和PLL配置不正确时,XSPI跑高频会出现“第一次读写正常,过几分钟就出错”的奇怪现象。
如果你手头没有MX66UW1G45G的CubeMX器件选项,可以在外部Flash型号下拉列表里找同系列的MX25LM51245G或者MX25UM51345G。XSPI控制器配置不依赖具体Flash型号,命令序列是你自己在代码里发的,CubeMX只是帮你生成外设初始化代码。
3.2 XSPI参数配置里的关键项
CubeMX的XSPI配置页面参数很多,我不是每个都建议动,但下面几个必须理解清楚:
| 参数项 | 推荐配置 | 说明 |
|---|---|---|
| Clock Prescaler | 根据时钟树算好分频 | 决定实际XSPI时钟频率 |
| Fifo Threshold | 4或8字节 | 影响CPU中断频率 |
| Memory Type | Micron/Standard可选 | 影响部分时序默认值 |
| Chip Select High Polarity | Disabled | NCS低有效 |
| Clock Mode | Mode 0或Mode 3 | 配合Flash CPOL/CPHA设定 |
| Sample Shift | Half Clock或1/4 Clock | 采样点微调,调试利器 |
Clock Prescaler根据你在时钟树设置的Kernel Clock来算。我的实测经验是:刚上手时把XSPI实际输出时钟控制在50MHz,配置稳定后再提升,不要一上来就跑极限频率。Sample Shift这个参数经常能解决读取数据错位问题,当Flash手册给的时序和实际芯片有偏差时,微微调整采样点就能恢复正常。
Memory-Mapped模式在CubeMX里需要单独勾选Enable Memory Mapped,并且要配置一个地址重映射区域。H7S系列的XSPI映射地址范围一般是0x90000000开始,代码里可以通过这个地址直接读Flash内容。
3.3 引脚复用和初始化顺序
CubeMX会在你使能XSPI1时自动分配引脚,但你需要确认GPIO速度等级。XSPI是高速接口,GPIO Speed要选Very High,否则信号边沿太缓,高频下容易误判。
初始化顺序也很关键。MSP初始化函数里,XSPI的时钟要和GPIO时钟一起打开;如果用了DMA,DMA时钟也要提前使能。我见过一个案例,XSPI初始化本身没问题,但因为GPIO时钟比XSPI时钟晚打开,导致第一次读取Flash ID失败,程序卡死在等待响应上。
另外,H7S系列的PDDS(Power Down Debug Support)模式会影响外部Flash供电。调试器中进入低功耗模式后,XSPI Flash可能处于掉电状态,恢复调试时会发现Flash内容读不出来。这个不是配置问题,是调试模式下的正常现象,遇到时重新上电即可。
4. 代码实现:从初始化到能读JEDEC ID
4.1 XSPI初始化结构体与底层驱动
CubeMX生成的初始化代码框架很完整,但XSPI的初始化结构体里有些字段需要结合MX66UW1G45G的数据手册手动调整。典型的HAL初始化代码类似这样:
XSPI_InitTypeDef XspiInit = {0}; XspiInit.ClockMode = XSPI_CLOCK_MODE_0; XspiInit.ClockPrescaler = 4; // 假设Kernel Clock 200MHz,输出50MHz XspiInit.FifoThreshold = 4; XspiInit.SampleShift = XSPI_SAMPLE_SHIFT_HALFCLK; XspiInit.MemoryType = XSPI_MEMORY_TYPE_MICRON; XspiInit.DeviceSize = 27; // 2^27 = 128MByte XspiInit.ChipSelectHighPolarity = XSPI_CS_HIGH_POLARITY_DISABLED; XspiInit.FreeRunningClock = XSPI_FREE_RUNNING_CLOCK_DISABLED; XspiInit.ClockMode = XSPI_CLOCK_MODE_0; if (HAL_XSPI_Init(&hxspi1) != HAL_OK) { Error_Handler(); }DeviceSize这个字段代表Flash容量以2的幂次表示。MX66UW1G45G是128MByte,也就是2的27次方,所以填27。如果填错,Memory-Mapped模式下地址空间会算错,访问尾部地址时可能读不到预期内容。
4.2 发送命令读取JEDEC ID
初始化完成后第一件事就是读JEDEC ID。这个命令序列简单,容易验证通信链路是否打通。MX66UW1G45G的标准命令是9Fh,返回3个字节的制造商ID和设备ID。
XSPI_CommandTypeDef cmd = {0}; uint8_t id[3] = {0}; cmd.OperationMode = XSPI_OPERATION_MODE_INDIRECT; cmd.InstructionMode = XSPI_INSTRUCTION_1_LINE; cmd.AddressMode = XSPI_ADDRESS_1_LINE; cmd.DataMode = XSPI_DATA_1_LINE; cmd.Instruction = 0x9F; cmd.Address = 0; cmd.AddressSize = XSPI_ADDRESS_24_BITS; cmd.DataSize = 3; cmd.DummyCycles = 0; cmd.DQSMode = XSPI_DQS_DISABLED; cmd.NbData = 3; HAL_XSPI_Command(&hxspi1, &cmd, HAL_XSPI_TIMEOUT_DEFAULT_VALUE); HAL_XSPI_Receive(&hxspi1, id, HAL_XSPI_TIMEOUT_DEFAULT_VALUE);读取完成后对照数据手册验证ID。MX66UW1G45G的JEDEC ID首字节一般是C2h,这是Macronix的厂商代码。如果你读到的ID全是FF或者全是00,先别怀疑Flash芯片,多半是通信没建立好。这时候用示波器看XSPI_CLK和XSPI_DQ0的波形,能快速定位问题。
读ID成功说明基础链路正常,但还远没到可以放心读写的程度。别忘了现在还是单线SPI模式,后面还要切换到八线模式和4字节地址模式。
4.3 切换到四字节地址模式和OPI模式
MX66UW1G45G容量128MByte,寻址空间超过16MB,必须使用4字节地址。切换指令是B7h(Enter 4-Byte Address Mode)。切完之后,所有后续命令的地址位宽都要按32位处理。
OPI模式切换则要看MX66UW1G45G的具体命令集。一般流程是先发Enable命令,再发切换命令,比如0x06使能写状态寄存器,然后写状态寄存器或扩展配置寄存器来选择OPI模式。具体命令参考数据手册的“Command Set”章节。MX66UW1G45G的OPI模式支持8-8-8完整协议,切换成功后,连读ID命令本身都要从单线变成八线发送。
这里有一个非常容易踩的坑:有些Flash在切换到OPI模式后,需要重新拉低NCS再拉高完成模式锁定。你必须在代码里严格控制片选时序,否则Flash可能只在当前会话有效,掉电后回到默认模式。配合Memory-Mapped模式时,需要先完成模式切换,再配置Flash进入XIP(Execute In Place)状态,然后才能在映射地址直接读Flash。顺序反过来,映射地址读到的全是垃圾数据。
// 进入4字节地址模式 cmd.Instruction = 0xB7; cmd.InstructionMode = XSPI_INSTRUCTION_1_LINE; cmd.AddressMode = XSPI_ADDRESS_NONE; cmd.DataMode = XSPI_DATA_NONE; HAL_XSPI_Command(&hxspi1, &cmd, HAL_XSPI_TIMEOUT_DEFAULT_VALUE); // 然后按手册指示切换到OPI模式切换模式后,整个XSPI控制器也要同步切换数据模式和地址模式。HAL封装里通过修改XSPI_CommandTypeDef的InstructionMode、AddressMode、DataMode字段来控制。要特别注意的是,切换后的第一个读命令就要用八线模式发送,如果控制器还停在单线模式,命令根本发不出去。
5. 读写擦的完整命令链与故障排查实测
5.1 页编程和扇区擦除的命令序列
对MX66UW1G45G正常的读写流程,离不开三条核心命令:Write Enable、Page Program、Sector Erase。Flash的特性是先擦后写,不能直接往非FF区域写入数据。
写操作前必须发06h命令使能写状态寄存器。写状态寄存器内部的WEL(Write Enable Latch)位会被置1,允许后续写操作。如果省略这步,写入命令会被Flash忽略,状态寄存器的WIP位一直不为零,程序就卡在等待写入完成的循环里。
扇区擦除命令视扇区大小而定。MX66UW1G45G支持4KByte扇区擦除(20h)、64KByte块擦除(D8h)等。注意地址是4字节还是3字节,取决于你前面是否开启了4字节地址模式。16MB以内的地址,用3字节没问题;超过16MB的区域,必须用4字节地址模式。
擦除和写入的“完成”检查,通过Read Status Register命令实现:
uint8_t status = 0; cmd.Instruction = 0x05; cmd.InstructionMode = XSPI_INSTRUCTION_1_LINE; cmd.AddressMode = XSPI_ADDRESS_NONE; cmd.DataMode = XSPI_DATA_1_LINE; cmd.DataSize = 1; cmd.DummyCycles = 0; HAL_XSPI_Command(&hxspi1, &cmd, HAL_XSPI_TIMEOUT_DEFAULT_VALUE); HAL_XSPI_Receive(&hxspi1, &status, HAL_XSPI_TIMEOUT_DEFAULT_VALUE); if ((status & 0x01) == 0) { // WIP位为0,写操作完成 }实际调试时不要把status轮询的间隔写得过密,可以在循环里加一点延迟,否则会占用大量CPU带宽。1Gbit的Flash全片擦除可能要数秒甚至数十秒,如果程序没有超时机制,看起来就像死机了一样。
5.2 内存映射模式的配置与Cache一致性
Memory-Mapped模式是XSPI最大的卖点之一,你在XSPI映射地址区域直接进行指针读取,而底层XSPI控制器自动从Flash搬运数据。STM32H7S78-DK上映射地址通常从0x90000000开始。
配置Memory-Mapped模式的关键是让XSPI控制器了解Flash当前的协议模式。如果Flash停留在单线SPI模式,你的内存映射读取命令也要用单线模式;如果已经切换到OPI模式,读取命令就要用八线模式。CubeMX的XSPI配置页面可以设置Memory-Mapped模式的默认命令参数,之后调用HAL_XSPI_MemoryMapped启动。
Cache一致性是这里绕不开的问题。H7S系列CPU有D-Cache,如果你先通过间接模式(Indirect Mode)写Flash数据,然后立刻通过Memory-Mapped地址去读,此时D-Cache可能返回旧的缓存数据,而不是Flash里的新数据。解决办法是操作Flash后用SCB_CleanDCache_by_Addr或SCB_InvalidateDCache_by_Addr做Cache维护。
还有一个经典现象:程序通过XSPI映射地址读取一个不断增长的数据文件,读出来的内容却始终是旧版本。这不是Flash有问题,而是数据被D-Cache缓冲了。排查时用调试器断开缓存再看,数据可能就正常了。
5.3 我在实际调试中遇到的三个坑
第一个坑是Flash型号选错导致命令集不匹配。MX66UW1G45G和MX25系列虽然不是完全不同的家族,但OPI模式下扩展寄存器的地址、默认值、Dummy周期数都有差异。按MX25UM51345G的配置去初始化MX66UW1G45G,读ID是正常的,但切换到OPI之后数据全是乱的。最后把两个数据手册放在一起逐行对比,才发现是Dummy Cycles配置差了两个周期。
第二个坑是XSPI时钟频率拉太高导致偶发错误。我把时钟从100MHz直接调到150MHz,跑内存映射读取时发现,连续读一个大数组,中间会随机出现几个字节错误。一开始怀疑Flash质量问题,后来把时钟降到100MHz就恢复了正常。查了Flash手册才发现这颗Flash在DTR模式下的最高频率标称值并不是所有组合都能达到150MHz,需要满足特定的VCC和温度范围。
第三个坑是调试器的复位时序。STM32H7S78-DK连接ST-Link调试时,如果启用调试器的“Reset and Run”选项,复位瞬间XSPI Flash可能还没完成上电初始化,程序就已经跑到XSPI启动分支,导致启动失败。解决办法是在调试器设置里关闭复位后自动运行,或者上电后在main函数里增加一段延时,等待Flash稳定。
6. 吞吐量优化与可靠性设计经验
6.1 时钟、Dummy周期和DQS采样
当基础读写功能稳定后,可以把注意力放到性能上。STM32H7S78-DK上的XSPI跑满带宽时,外部Flash的读取速度可以达到上百MB/s量级,但这需要几个条件配合。
时钟是带宽的基础,但不是唯一因素。Memory-Mapped模式下的连续读性能在很大程度上受Dummy Cycles影响。Dummy Cycles是Flash在地址相位之后、数据相位之前插入的等待周期,用来适配Flash内部的访问延迟。Dummy周期太少,Flash还没准备好数据,控制器就采样,读出来是错的;Dummy周期太多,带宽被白白浪费。MX66UW1G45G在不同频率和模式下对Dummy Cycles的要求不一样,数据手册有一张对应关系表,配置时一定要对着查。
DQS采样在DTR模式下非常重要。DTR模式下数据在时钟上下沿都采样,对采样窗口的精度要求极高。DQS信号由Flash输出,控制器利用它捕获数据。如果DQS布线过长或者被其他信号干扰,读取稳定性会下降。板卡如果走线做不到理想,可以尝试在控制器配置里把采样点从中间移到1/4处,有时候能避开毛刺。
6.2 掉电保护和磨损均衡的取舍
外部NOR Flash在写入时如果突然掉电,可能导致正在擦除或编程的扇区数据损坏。对于存代码的场景,这种损坏是灾难性的,因为可能导致系统无法启动。
XSPI接口本身没有内置掉电保护机制,需要软件层面配合。简单方案是分区域管理:把存储区划分为代码区、配置区、日志区。配置区写入频率高,建议使用双备份方案,也就是写A区前先检查B区,写完后做标志校验。日志区则用循环写入策略,不覆盖同一个扇区,而是按顺序向后写,写满后再擦除最老的数据。
磨损均衡对NOR Flash来说同样重要。NOR Flash的擦写寿命一般在10万次左右,如果程序频繁往同一扇区写配置数据,那个扇区会很快耗尽寿命。MX66UW1G45G容量大,空间充裕,完全可以通过地址映射把一个固定逻辑地址循环映射到不同物理扇区,简单实现磨损均衡。
6.3 适合MX66UW1G45G的典型应用
128MByte的NOR Flash,在MCU场景里算大容量了。除了保存固件做OTA升级,最常见的用途是存UI资源。H7S系列主打图形显示,如果从外部Flash直接读取图片和字库,Memory-Mapped模式可以把资源当作内存直接用,简化代码逻辑,还能提升加载速度。
另一个典型场景是数据记录。比如工业设备运行日志、传感器采样数据,连续写入外部Flash。XSPI的高带宽和NOR Flash的随机访问特性,让日志存储和按时间检索都变得很高效。相比SD卡,NOR Flash没有文件系统碎片问题,掉电时数据更可靠,只是成本高一些。
我自己在这块板上做的是把字体库和开机Logo放到MX66UW1G45G,代码放在内部Flash,XSPI映射地址区域直接读取资源。整个工程跑下来,UI启动速度比之前的QSPI方案快了将近一倍。
最后分享一个调试时的实用技巧
如果你在配置MX66UW1G45G时遇到读ID正常、但读写总是失败的情况,我建议不要死磕代码,先写一个最简单的测试函数:读取JEDEC ID,然后反复执行“擦除-写入-回读”同一个扇区。如果回读数据稳定,说明XSPI基础链路没问题,问题多半出在命令序列或模式切换上。把每条命令的Instruction、AddressMode、DataMode打印出来,和Flash手册逐条核对,很快就能找到差异。
另外,MX66UW1G45G的寄存器配置选项很多,如果CubeMX里没有现成模板,别怕。把Flash数据手册的默认值作为起点,配合采样点调整和Dummy周期修改,大部分问题都能慢慢试出来。XSPI调通了之后,这套配置可以直接复用到同系列的板卡上,后期收益非常高。