简介:本资源是一套面向嵌入式开发工程师与STM32进阶学习者的实战型实验例程,聚焦STM32H743IIT6单片机通过QSPI接口高速读取W25Q64闪存芯片,并深度集成MDMA(多功能DMA)实现零CPU干预的数据搬运。该方案显著提升大容量外部Flash读取效率,适用于Bootloader升级、固件热加载、实时日志存储等对带宽与实时性要求较高的工业与物联网场景。压缩包共95个文件,含55个头文件(.h,定义寄存器映射与API接口)、28个源文件(.c,涵盖QSPI初始化、MDMA通道配置、命令序列控制及中断服务逻辑)、1个Keil MDK工程文件(.uvprojx)及配套启动脚本、链接脚本、调试配置等,整体大小954KB,结构完整、模块清晰,开箱即可编译运行。目前已有569人下载学习,配套代码已通过实际硬件验证,包含完整的错误检测机制、传输完成回调与基础数据校验逻辑,是掌握STM32H7系列高性能外设协同开发的优质参考范例。 "最近在调试一块自己打的H743IIT6板子,板载一颗W25Q64用来存开机画面和字库。最开始图省事,直接指针读0x90000000地址,CPU去搬,后面发现画面切换时CPU被拖住,帧率掉得很难看。后来把QSPI的读取链路改成MDMA搬运,才把这块外扩Flash真正用顺。这篇文章就把这套STM32H743IIT6 + QSPI + W25Q64 + MDMA的完整玩法写出来,包括引脚选择、QSPI寄存器配置、MDMA参数计算、缓存一致性处理,以及我调这块板子时踩过的几个典型坑。适合手里有H7开发板、准备外挂SPI Nor Flash做数据存储或代码XIP的朋友参考。"
1. 为什么要在H743外挂W25Q64,以及这套例程的定位
1.1 外扩Flash能解决什么实际问题
STM32H743IIT6虽然有2MB内部Flash、1MB RAM,在MCU里算大个子了,但真做产品时这两个资源都挺尴尬:2MB Flash放完固件、协议栈、UI资源之后往往剩下不到几百KB,而现在的图形界面、音频样本、字库动辄就好几MB。把图库和字库存进外部SPI Nor Flash是最常见的低成本扩容手段,一颗W25Q64(8MB)只要几块钱,还能继续往上堆W25Q128、W25Q256,容量弹性大。
另一个典型场景是离线日志和数据记录。内部Flash写入有擦写次数限制,而且擦除粒度大(H7的扇区是8KB起步),频繁写日志会快速消耗寿命。外挂Nor Flash的好处是Sector擦除最小4KB,写操作独立于程序存储区,即使程序跑飞也不容易把固件区搞坏。
还有一类玩法是把代码放到外部Flash做XIP执行(Execute in Place)。H743的QSPI支持内存映射模式,外部Flash内容映射到0x90000000,代码可以直接跳过去跑,配合MDMA还可以做代码的批量加载到RAM执行,不过这篇重点讲数据读取,XIP后面单独开篇再说。
1.2 选QSPI而不是SPI、SDIO、FMC的理由
很多刚接触H7的朋友会问:SPI也能挂Flash,干嘛非要QSPI?答案是带宽差距太大了。普通SPI模式单线传输,在100MHz时钟下理论带宽只有12.5MB/s左右;QSPI的Quad模式四根数据线同时传输,同样100MHz时钟能到50MB/s,对刷图片、滚动字库这类场景完全是两个量级。H743iit6的QSPI外设最高可以跑到时钟域的极限,实际工程里用100MHz很常见。
相比SDIO接SD卡或者FMC接并行NOR,QSPI的优势是引脚少、电路简单。Quad模式只需要6根信号线(CLK、CS、IO0~IO3),布线压力小,PCB面积也省。FMC并行Flash速度虽然猛,但需要十几根甚至二十几根线,在小板子上实在奢侈。SDIO挂TF卡容量大,但SD卡协议层复杂、上电时序长,而且要处理热插拔和坏块,做只读数据存储不如SPI Nor Flash省心。
1.3 例程的整体设计:内存映射+MDMA搬运
这套例程的核心思路可以概括成一句话:让Flash看起来像内存,让搬运交给DMA。先把W25Q64通过QSPI切到内存映射模式,外部8MB Flash直接映射到0x90000000地址段,CPU对这块区域做普通指针读写就能访问Flash内容,完全不需要手动拼命令、查状态寄存器。但内存映射模式下CPU直接反复读大块数据依旧会占用内核管线,于是例程再引入MDMA,把一整块数据从0x90000000批量搬到AXI SRAM,搬完触发中断告诉CPU"数据已就绪",CPU只需做校验和后续处理。
这样划分之后,CPU在整个读取过程中基本是闲置的,可以并行跑UI刷新、通信协议等任务。MDMA一次搬运4KB、16KB甚至64KB都不在话下,对大批量读取场景提升非常明显。接下来我把这套链路从硬件到代码拆开讲。
2. 硬件上的那些细节:引脚、连接、板级注意点
2.1 W25Q64关键参数与引脚
W25Q64是Winbond(华邦)的经典SPI Nor Flash,容量64Mbit也就是8MB。目前市面上常见的是W25Q64JV系列,供电范围2.7V到3.6V,和H743的3.3V IO电平可以直接对接。最高时钟频率133MHz,所以H7的QSPI时钟跑100MHz完全在芯片规格范围内。
芯片是SOIC-8或者WSON-8封装,8个引脚里真正干活的是这几根:
| 引脚名 | 功能 | 在Quad模式下对应的QSPI信号 |
|---|---|---|
| /CS | 片选 | QSPI_BK1_CS |
| CLK | 时钟 | QSPI_CLK |
| DI (IO0) | 数据输入/输出 | QSPI_BK1_IO0 |
| DO (IO1) | 数据输出/输入 | QSPI_BK1_IO1 |
| /WP (IO2) | 写保护/数据线2 | QSPI_BK1_IO2 |
| /HOLD (IO3) | 保持/数据线3 | QSPI_BK1_IO3 |
| VCC | 电源 | 3.3V,需要就近放0.1uF去耦电容 |
| GND | 地 | 地平面 |
注意IO2和IO3在普通SPI模式下分别是/WP和/HOLD功能,在Quad模式下自动切换为数据线。板子设计上最好把这两个引脚通过10k电阻上拉到3.3V,防止上电瞬间电平不确定导致Flash误进入写保护或保持状态。
2.2 H743IIT6的QSPI引脚映射与我的选择
H743的QSPI引脚是AF映射,同一个信号可以从好几组GPIO里选,具体由AFR(复用功能寄存器)决定。H743IIT6是LQFP176封装,引脚资源丰富,常用的一组QSPI信号可以放在GPIOF和GPIOG上:
| QSPI信号 | 我选用的引脚 | AF编号 |
|---|---|---|
| QSPI_CLK | PF10 | AF9 |
| QSPI_BK1_CS | PG6 | AF10 |
| QSPI_BK1_IO0 | PF8 | AF9 |
| QSPI_BK1_IO1 | PF9 | AF9 |
| QSPI_BK1_IO2 | PF7 | AF9 |
| QSPI_BK1_IO3 | PF6 | AF9 |
这组引脚在NUCLEO-H743ZI板子上也是这样连的,网上能找到大量现成工程可以直接参考,属于踩过坑验证过的组合。如果你自己画板,建议优先抄这组,因为ST官方评估板的原理图和例程基本都是按这个映射来的,遇到问题好对照。CubeMX里勾选QSPI后会自动列出当前引脚可用的AF组合,选完IO0~IO3、CLK、CS这六根,剩下的交给代码生成器就行。
还有一点容易被忽略:如果这组GPIO上还接了其他外设(比如PF8、PF9同时也是FMC的数据线候选引脚),务必检查有没有把两个外设的AF配到同一个引脚上。我遇到过有人把QSPI的IO0配到PE7,结果这个引脚同时又是以太网或者SDRAM的信号,导致两边都工作不正常。
2.3 容易被忽略的/WP、/HOLD和走线问题
Flash和MCU之间的连接距离越短越好,这句话我吃了不少亏。QSPI跑100MHz时,信号上升沿本身就很快,如果PCB走线过长或者用了杜邦线飞线,线间电容和串扰会直接变成采样错误。实测用杜邦线飞线,50MHz还能勉强跑,上100MHz就开始随机读错字节;换成PCB短走线(小于3cm)后稳定很多。
/WP和/HOLD这两个引脚在Quad模式下是数据线IO2和IO3,但如果板子上它们被拉低,Flash会处于写保护或者保持状态,最直观的现象就是读ID正常、读数据全是0xFF。建议板上直接把这俩引脚通过电阻上拉到VCC,调试阶段可以省掉一大半烦恼。如果确实有写保护需求,软件里用Status Register控制即可,不要用硬件拉低/WP的方式。
另外,Flash的VCC脚一定要放一个0.1uF陶瓷电容,而且要尽量靠近引脚。这颗芯片上电瞬间电流变化不小,电源纹波大了之后表现在数据上就是莫名其妙的多bit错误,查半天查不到原因。
3. QSPI初始化:时钟、寄存器、读命令选择
3.1 QSPI时钟从哪里来,分频怎么算
H743的QSPI是内核外设,它的内核时钟(QSPI kernel clock)不是简单走APB时钟,而是单独有一条时钟路径,可以选HSI、CSI或者PLL1Q。实际工程里最常用的做法是用PLL1Q,比如把PLL1Q配到200MHz,然后QSPI分频系数设为2,得到100MHz的外设时钟。
QSPI的SCK时钟 = QSPI内核时钟 / (2 × (PRESCALER+1))。注意这个公式里的PRESCALER就是CubeMX里配置的ClockPrescaler字段,如果内核时钟200MHz、Prescaler=2,那么SCK = 200 / (2×3) ≈ 33MHz,不是你以为的100MHz。100MHz对应的是Prescaler=1的情况,即200 / (2×2) = 50MHz?这里容易算乱。我直接说结论:想让SCK等于内核时钟的一半,Prescaler要填0,因为公式里PRESCALER是0就是除2。在HAL库的HAL_QSPI_Init里,ClockPrescaler这个参数就是这个PRESCALER值,我自己的板子PLL1Q跑200MHz,ClockPrescaler = 1时SCK约50MHz,ClockPrescaler = 0时SCK为100MHz。不同HAL版本对Prescaler的语义可能有细微差别,最稳的办法是初始化后拿示波器扎一下CLK引脚实测频率。
QSPI时钟源在CubeMX的Clock Configuration页面里选,路径是PLL1Q → QUADSPI。这个时钟还必须满足一个限制:SCK不能超过外挂Flash的最高时钟,W25Q64JV是133MHz,所以跑100MHz留有余量,没问题。
3.2 初始化参数逐项说明
QSPI的HAL初始化结构体里有几个参数非常关键,我逐个说清楚:
FlashSize:填写Flash容量的对数,W25Q64是8MB = 2^23字节,所以填23。这个参数直接决定内存映射模式下地址空间的覆盖范围,填小了访问不到Flash尾部,填大了地址计算会溢出到保留区。
ChipSelectHighTime:片选高电平时间,单位是QSPI时钟周期,可选1到8个周期。W25Q64的数据手册里要求tSHSL最小为30ns左右,当SCK=100MHz时一个周期是10ns,所以这个值至少取4才够。我为了留余量直接用5,实测很稳。很多人默认填1或者2,在低速时没问题,一上高速就出现偶发读错,十有八九是这里没调够。
SampleShifting:数据采样相位偏移。QSPI外设在读Flash数据时,采样点可以相对时钟沿做0、1/2、1、3/2周期的偏移。这个参数是高速调试的利器,Flash和MCU之间的走线延迟不一样,固定采样点可能采到边沿跳变附近的不稳定区,适当偏移半拍往往能解决。我的板子上100MHz时用QSPI_SAMPLE_SHIFTING_HALFCLK最稳。
FifoThreshold:FIFO阈值,控制FIFO空/满中断的触发水位,对普通内存映射模式影响不大,保持默认4即可。
3.3 读命令用0x6B还是0xEB
W25Q64支持好几种读命令,内存映射模式下QSPI每次访问Flash都会自动发你配置好的那条命令,所以选哪条直接决定传输效率:
0x03:普通读,地址和数据都是单线,最慢,但兼容性最好。0x0B:快速读,单线地址+单线数据,带dummy周期。0x6B:Fast Read Quad Output,地址单线发,数据四线回,即1-1-4模式。0xEB:Fast Read Quad I/O,地址也走四线,即1-4-4模式。
例程里我推荐先用0x6B把链路跑通,因为它不需要处理"模式位"(mode byte)的概念,配置最简单。0x6B的指令时序是:发送0x6B命令(1字节)→ 发送24位地址(单线)→ 8个dummy周期 → 四线返回数据。对应到QSPI寄存器就是IMODE=1线、ADMODE=1线、DMODE=4线、DCYC=8。
0xEB虽然在地址阶段也走四线,理论上能再省一点时间,但它要求发送完地址后再发1字节
本文还有配套的精品资源,点击获取