STM32H743 QSPI驱动W25Q64及MDMA读取实战解析
简介本资源是一套面向嵌入式开发工程师与STM32进阶学习者的实战型实验例程聚焦STM32H743IIT6单片机通过QSPI接口高速读取W25Q64闪存芯片并深度集成MDMA实现零CPU干预的数据搬运。解决高性能外置Flash访问中带宽瓶颈与实时性不足的典型痛点适用于Bootloader升级、固件热加载、日志缓存等工业与IoT场景。压缩包共95个文件含55个头文件.h定义外设寄存器与配置宏、28个源文件.c覆盖QSPI初始化、MDMA通道配置、命令序列控制及中断服务逻辑另含Keil工程文件.uvprojx/.uvoptx、启动脚本.s、调试配置.dbgconf及构建辅助脚本.bat结构完整、开箱即用。资源包大小为954KB目录层级清晰含标准HAL驱动、CMSIS核心支持与RTE组件便于理解H7系列外设协同机制。目前已有569人学习下载提供可直接编译运行的完整工程附带关键注释与错误处理范式助开发者快速掌握QSPIMDMA高效率数据通路的设计与调试方法。 搞嵌入式这几年我越来越觉得一句话特别真实MCU 算力再强没有一块像样的外部存储应用就永远只能在“够用”和“将就”之间徘徊。最近我在 STM32H743IIT6 上整理了一套基于 QSPI 接口 W25Q64 的读取工程顺手把 MDMA 通道也加进去了核心就是把外设数据用 MDMA 搬进内存CPU 几乎不参与。这个组合在 H7 上非常典型适合做 GUI 图库映射、OTA 升级缓存、日志转存这类场景。资料里带的是完整实验例程源码但如果你只拿源码去烧录而不理解背后的 QSPI 时序、W25Q64 指令体系、MDMA 请求映射那大概率改不了几个参数就会卡住。所以这篇我打算从标题里的四个关键词展开把 H743 上 QSPI 和 W25Q64 的底层原理、MDMA 联动方式、例程代码怎么搭、以及我在实测中踩过的坑一次说清楚。1. 这套组合要解决什么问题H743 上的高速外部 Flash 读取方案1.1 为什么是 QSPI而不是普通 SPI 或 SDIOSTM32H743IIT6 这颗料Cortex-M7 内核跑到 480MHz板载 2MB Flash、1MB RAM资源在 MCU 里算非常豪华。但遇到两种情况内部 Flash 还是不够用一是装 GUI 用的图片素材和字库随便一套稍有规模的中文字库就是几 MB二是做产品日志或者固件升级缓存需要大块连续可擦写的存储空间。过去很多人用普通 4 线 SPI 接外部 flash比如 W25Q64 的标准 SPI 模式但普通 SPI 只有一根数据输出线即使跑满 80MHz每字节还是要 8 个时钟周期。QSPI 模式下W25Q64 可以四根数据线同时输出同样的时钟频率理论吞吐量直接翻四倍。虽然 W25Q64 本身就是一颗 SPI NOR Flash不存在“原生 QSPI”这个说法它的 Quad 模式本质上还是拿那几根引脚复用成双向数据线但对 MCU 侧来说控制器的内存映射能力和突发读效率完全不是一个体验。SDIO 方案也能挂 Flash 或者 TF 卡但 SDIO 引脚多、协议栈重、还需要额外的命令初始化流程杀鸡用牛刀。做嵌入式产品时QSPI 是容量、速度、引脚占用之间最平衡的选项。STM32H743 的 QUADSPI 控制器本身就支持单线、双线、四线模式还支持把外部 Flash 映射到 CPU 地址空间直接按指针访问这是普通 SPI 没办法给的体验。1.2 为什么选 W25Q64W25Q64 是华邦 NOR Flash 家族里很经典的一颗容量 8MB64Mbit支持标准 SPI、Dual SPI、Quad SPI分频后接口时钟可以支持到很高频率。选它做实验材料有一个很现实的原因便宜、好买、资料烂大街。淘宝几块钱一片随便造不心疼非常适合用来验证 H743 的 QSPI 外设逻辑。对于实验例程来说W25Q64 的 8MB 容量也刚好覆盖大部分边界测试3 字节地址就能完成全部操作不需要像 W25Q256 那样处理 4 字节地址模式。如果换成 256Mbit 的芯片地址宽度、QE 位、读命令序列都不一样反而增加了干扰因素。当然这颗芯片也有一点要特别注意它的 Page Program 是 256 字节一页Sector 擦除是 4KBBlock 擦除是 32KB/64KB。这些参数决定了你在写驱动时写入数据必须按页拆分、做跨页处理擦除时还要避开正在使用的扇区。例程里我把这些底层操作全部封装好这样业务层调用时根本不用关心 W25Q64 内部的组织方式。1.3 MDMA 在这里扮演什么角色STM32H743 里的 DMA 不只是 DMA1/DMA2 那套老东西还带了一个更强的 MDMAMaster DMA。MDMA 可以看成是能直接访问大多数 AHB/APB 外设的“搬运队长”它支持任意到任意的传输还能用链表描述符连成一条传输链让复杂搬运任务不需要 CPU 一根一根地指挥。在 QSPI 读 Flash 的场景里MDMA 干的事情很简单QSPI 控制器从 W25Q64 读出来的数据会先进入一个 FIFOMDMA 负责把这个 FIFO 里的数据搬到你指定的内存 buffer。每次从 Flash 到 FIFO 的填充速度可能不稳定但 MDMA 可以挂在外设请求信号上来一点搬一点搬运完成后触发中断告诉 CPU。如果不加 MDMA纯 CPU 轮询方式就得反复读 QSPI 状态寄存器、检查 FIFO 是否非空、然后手动搬运 4 字节。代码倒是简单但每次等待和判断都会消耗主频读取 8MB 数据时差距非常明显。加了 MDMA 之后CPU 把初始化和启动命令发出去就可以去处理别的任务了数据搬完再被中断拉回来这种模型在实时系统里尤其重要。2. 硬件链路与底层原理读懂 QSPI、W25Q64、MDMA 三者之间的关系2.1 STM32H743IIT6 的 QUADSPI 控制器QUADSPI 在 H7 系列里是一个独立外设它有一套完整的指令序列状态机。你在驱动里要做的不是像普通 GPIO 模拟 SPI 那样一根引脚一根引脚地操作而是填好命令结构体、使能传输剩下的时序由硬件自动生成。ST 的 HAL 库里对应的是一个QSPI_CommandTypeDef结构体里面包含指令、地址、数据长度、地址大小、指令模式、地址模式、数据模式、备用字节模式以及操作类型和指令宽度这些字段。例程里的QSPI_ReadData函数本质上就是组装这样一个结构体然后调用HAL_QSPI_Command和HAL_QSPI_Receive*。QUADSPI 支持两种读取模式间接模式和内存映射模式。间接模式下你给控制器发命令数据会流进 FIFO然后你可以用轮询、中断或 DMA 把 FIFO 里的数据取走内存映射模式下你直接把外部 Flash 当作一块只读内存CPU 访问一个映射地址硬件自动发起读命令数据“看起来”就像从内部 Flash 读出来一样快。2.2 W25Q64 的指令体系与状态机嵌入式圈子里统计过90% 的 QSPI 问题出在读不到数据而读不到数据的原因里又有很大一部分是“忘了让 Flash 进入 Quad 模式”。W25Q64 的寄存器里有一个 QE 位Quad Enable位于状态寄存器 2 的 bit1。这个位默认是 0只有把它写成 1芯片才允许四线指令比如 0x6B、0xEB 这类 Quad 读命令。所以初始化流程里有一个关键步骤写状态寄存器 2。先发0x06写使能命令再发0x31Write Status Register-2把 SR2 置成 0x02把 QE 位拉起来。写完还要通过0x05读状态寄存器 1轮询 BUSY 位确保 Flash 内部操作完成。读命令的选择也影响效率和时序。普通读0x03每次都要发命令地址没状态切换速度极慢快速读0x0B可以加 dummy 周期适合提高时钟频率四线快速读0x6B支持从引脚上直接并行输出数据四线 I/O 快速读0xEB则是地址和 dummy 周期也走四线减少命令阶段占用的时钟数。例程用的方式以0x6B为主实际项目里如果追求极限性能可以考虑0xEB但要确认主控支持连续读模式Continuous Read Mode以及退出连续模式的方法否则会搞出一堆协议兼容问题。2.3 MDMA 如何与 QSPI 联动很多第一次上手 H743 的人会想当然QSPI 是外设MDMA 是 DMA那直接配置 DMA 就好。实际上H7 系列把 QUADSPI 的 DMA 请求信号接到了 MDMA 路径上如果你在 CubeMX 里选 DMA1/DMA2根本找不到 QUADSPI 的请求源或者配置出来也是错乱的。这里有一个很关键的背景H7 有两个 DMAMUX 域其中 DMAMUX1 给常规 DMADMAMUX2 给 MDMAQUADSPI 的请求要挂在 MDMA 这边。联动逻辑也不复杂。QSPI 从 Flash 读数据时FIFO 里每积累到一定字节数硬件就会拉一个 DMA 请求信号。MDMA 看到这个信号后自动把 FIFO 里数据搬到目标内存地址然后继续等下一个请求。当所有预定长度的数据都搬完后MDMA 产生传输完成中断你在中断回调里置一个标志应用程序就知道读取结束了。这里要注意一个细节MDMA 的传输粒度必须和 QSPI FIFO 的数据宽度匹配。H743 的 QSPI FIFO 是 32 位宽的所以 MDMA 的TransferSize应该配置成 32 位长度用字节数除以 4。如果你配置成 8 位MDMA 读到的数据不仅字节序会乱连续读模式下还可能丢请求导致读出来的数据量比预期少。3. 从零到一QSPI MDMA 读取例程的工程搭建与代码解析3.1 引脚分配与时钟树配置用 CubeMX 去搭建工程最省事。我使用 STM32H743IIT6在 CubeMX 里选 QUADSPI 外设时它会自动分配引脚默认是 PA2CLK、PB6NCS、PB2BK1_IO0、PB10BK1_IO1、PB11BK1_IO2、PD7BK1_IO3。不同板子可能有差异所以第一件事一定要打开原理图核对确认这些引脚没有被其他外设占用。时钟树方面QSPI 的外设时钟可以直接选择 AHB3 或系统时钟分频。我在例程里把 QSPI Kernel Clock 配置成 100MHz 附近再在 QUADSPI 初始化里用分频系数降到 50MHz 左右这样 W25Q64 在四线模式下非常稳。很多人一上来就上 133MHz结果读取偶尔出错其实问题不一定在 Flash而在板子走线和采样相位。对实验例程来说稳定比极限速度重要超频的事留给 PCB 达标的产品再说。CubeMX 里还有几个跟 DMA 相关的勾选项在 QUADSPI 的 DMA Settings 里把 TX/RX 通道都选成 MDMA而不是 DMA1/DMA2。这一步选错后面代码生成出来就不会有 MDMA 初始化代码HAL 库调用也会直接卡在等待超时。3.2 QUADSPI 初始化参数的计算与配置初始化代码集中在MX_QUADSPI_Init或对应的 HAL 配置函数里。关键参数是这几个ClockDivider时钟分频。H743 的 QUADSPI 时钟源经过这个分频系数再送到外部 Flash。一般不要低于 2分频越小时钟越高但 PCB 信号质量要求也越高。FlashSize外部 Flash 的首个逻辑地址。W25Q64 是 8MB2 的 23 次方等于 8MB所以这个值填 23。填错会导致地址空间映射异常。FifoThresholdFIFO 阈值也就是触发 DMA 请求的门槛。我习惯配 4意味着 FIFO 里积累到 4 个字节一个 32 位字就触发一次搬运。SampleShifting采样移位。这个参数影响的是读取数据时控制器在什么时刻去采样总线电平。如果数据总是偶尔错误可以在这里调整采样沿。ChipSelectHighTime片选高电平时间。两次传输之间的间隔太短Flash 状态机可能没准备好所以这个值也不要配 0。Flash 容量参数的计算我推荐直接写成根据板级宏定义可调的比如#define FLASH_SIZE_64MB (23U) // 2^23 8MB这样以后换更大容量的 Flash只改一个宏再配合 4 字节地址切换逻辑驱动层不用大改。3.3 W25Q64 基础操作封装我习惯把 W25Q64 的所有底层操作单独放到一个文件例如w25q64.c对外只暴露几个接口W25Q64_Init、W25Q64_Read、W25Q64_Write、W25Q64_EraseSector。内部实现时先把 QSPI 命令结构体定义好。写使能是最常用的小函数。HAL 库提供了HAL_QSPI_Command可以把指令直接发给 Flashstatic void W25Q64_WriteEnable(void) { QSPI_CommandTypeDef cmd; cmd.Instruction 0x06; cmd.AddressMode QSPI_ADDRESS_NONE; cmd.DataMode QSPI_DATA_NONE; cmd.DummyCycles 0; cmd.InstructionMode QSPI_INSTRUCTION_1_LINE; HAL_QSPI_Command(hqspi, cmd, HAL_QSPI_TIMEOUT_DEFAULT_VALUE); }写使能之后写状态寄存器 2 打开 QE 位然后等 BUSY 位清零。这里有个细节修改 Flash 状态寄存器时需要先读取旧状态寄存器的值再保留其他位否则会误清掉保护位。四线快速读的封装稍微复杂一点。命令是0x6B地址宽度 3 字节地址模式是四线数据宽度是你希望读取的字节数。因为 W25Q64 的地址是 3 字节FlashSize 23 就是这个 3 字节地址的基础。3.4 MDMA 通道配置与请求映射CubeMX 生成工程后MDMA 初始化代码一般长这样MDMA_HandleTypeDef hmdma_qspi_rx; void MX_MDMA_Init(void) { hmdma_qspi_rx.Instance MDMA_Channel0; hmdma_qspi_rx.Init.Request MDMA_REQUEST_QUADSPI; hmdma_qspi_rx.Init.TransferTriggerMode MDMA_BLOCK_TRANSFER; hmdma_qspi_rx.Init.Priority MDMA_PRIORITY_VERY_HIGH; hmdma_qspi_rx.Init.Endianness MDMA_LITTLE_ENDIAN; hmdma_qspi_rx.Init.SourceInc MDMA_SRC_INC_WORD; hmdma_qspi_rx.Init.DestinationInc MDMA_DEST_INC_WORD; hmdma_qspi_rx.Init.SourceDataSize MDMA_SRC_DATASIZE_WORD; hmdma_qspi_rx.Init.DestDataSize MDMA_DEST_DATASIZE_WORD; hmdma_qspi_rx.Init.DataAlignment MDMA_DATAALIGN_PACK; hmdma_qspi_rx.Init.BufferTransferLength 1; hmdma_qspi_rx.Init.BlockCount 1; HAL_MDMA_Init(hmdma_qspi_rx); }请求方向要反过来看读取时 QSPI 是源内存是目标。所以源地址是 QSPI 的 FIFO 数据地址目标地址是你的 buffer源不增加、目标按 4 字节递增。Request字段必须设置成 QUADSPI 对应的请求这一步我非常不建议手动裸写寄存器最好在 CubeMX 中显式选中 MDMA 后让代码生成器自动填否则请求号容易搞错。MDMA 的 Buffer 大小需要注意它和普通 DMA 的传输长度概念不同。配置 BlockCount 为 1、每次 Block Transfer 完成后触发中断整体传输完成后也要处理中断状态。例程里我用的是HAL_MDMA_Start_IT起一个完整块传输配合中断回调判断完成。3.5 读取流程整合与代码结构实际调用流程是这样初始化 QSPI 和 MDMA。W25Q64 掉电唤醒0xAB命令。读状态寄存器等待 Flash 就绪。调用W25Q64_Read内部组装0x6B命令调用 HAL 层HAL_QSPI_Command再调用HAL_QSPI_Receive_DMA。等待 DMA 完成标志读取 buffer 里的数据。HAL 库的HAL_QSPI_Receive_DMA内部会自动把 MDMA 的源地址指向 QSPI FIFO但这里有个隐藏行为它默认使用你 CubeMX 生成的 MDMA 句柄。所以如果你在 CubeMX 里没把 QUADSPI 的 DMA 关联到 MDMA这个函数调用的句柄就是空指针直接 HardFault 或者卡死。在工程结构上我会把“QSPI 硬件操作”和“W25Q64 业务协议”分层qspi_if.c维护 hqspi 句柄、提供底层的 HAL 包装w25q64.c调用 qspi_if 层实现读、写、擦、状态机轮询app_main.c业务层比如保存一组日志、再读出来做校验。这样的分层方便以后把 W25Q64 换成 GD25Q64、MX25L6445EM 这类同规格芯片时只需要改 w25q64.c 里头的命令字。4. 踩坑实录半个月调试中最常见的 7 个问题4.1 MDMA 请求映射与 QSPI FIFO 事件的匹配我在第一次做这个例程的时候卡在“HAL_QSPI_Receive_DMA 一调用就超时”。逻辑上命令发了、Flash 也响应了但 MDMA 就是不动。后来逐个排查发现 CubeMX 的 DMA Settings 里虽然能看到 QUADSPI 的选项但默认分配的是 DMA1不是 MDMA。症状非常典型程序能初始化但一旦执行带 DMA 的读取就卡在某个等待标志上。原因是 QUADSPI 的 FIFO 请求事件根本没有路由到正在等待的 DMA 通道上。解决方法是回到 CubeMX在 QUADSPI 的 DMA Settings 里手动勾选 “MDMA” 作为 DMA Request重新生成代码。如果是手动移植 ST 例程还要检查 MDMA 初始化里的Request值必须和芯片参考手册中 DMAMUX2 映射表格对应。简单判断方法是看 MDMA 中断回调有没有被触发一次都没触发基本就是请求映射问题触发了但数据错其实是数据宽度或者地址递增方向问题。4.2 Cache 一致性H7 上不能忽略的 D-CacheH743 有 D-Cache而 MDMA 搬运数据到内存时数据是通过总线直接写入内存的不会经过 CPU 的 Cache。如果你的 MPU 把这块内存配置成了 Cacheable 或 Write-BackCPU 从同一地址去读时命中的还是老旧的 Cache 行数据读出来全是旧值或乱码。这个问题只会出现在开启 D-Cache 时而且很隐蔽。正确的做法是在 MDMA 搬运完成后对目标内存做一次SCB_InvalidateDCache_by_Addr在写入数据前如果需要 CPU 保证数据能 DMA 出去则要SCB_CleanDCache_by_Addr。SCB_InvalidateDCache_by_Addr((uint32_t *)buffer, length);在例程里我把这个操作放在接收完成回调之后这样可以保证 CPU 拿到的就是 DMA 搬进来的新数据。硬编码关闭 D-Cache 能偷懒一时但会影响整个系统性能尤其 H7 这种 MHz 级别的核开 Cache 和不开 Cache 的跑分差距非常大。4.3 读数据全是 0xFF / 前几字节错乱遇到“读出来所有数据都是 0xFF”第一反应不要急着怀疑 QSPI 时序。先确认 W25Q64 是不是进入了掉电模式或者芯片压根没被片选命中。用示波器看 NCS 引脚如果发现 NCS 电平一直是高大概率是片选极性配置反了如果 NCS 有低电平但 MISO 始终为高则检查 QSPI 的 DataMode 是否配置成了四线而 Flash 又没有使能 QE 位。前几个字节错乱则大多跟 FIFO 阈值和采样移位有关。例如读命令发出后Flash 需要几个 dummy 周期如果你在命令结构体里配的DummyCycles比芯片手册要求少控制器拉到前导数据还没稳定就开始采样自然读错。W25Q64 的0x6B命令需要 8 个 dummy 周期0xEB需要 6 个这些数字必须按手册来。4.4 常见问题速查表我整理了这段时间调试时遇到的高频问题按症状、原因、处理方式做成一个速查表方便大家直接对照排查症状可能原因处理方式读回来的数据全为 0xFFFlash 处于掉电模式或 QE 位未使能发送 0xAB 唤醒写状态寄存器 2 使能 QE卡死在 HAL_QSPI_Receive_DMADMA 请求未关联到 MDMA检查 CubeMX 中 QUADSPI 的 DMA Request 是否为 MDMA数据长度减半或丢字节MDMA 数据宽度与 FIFO 宽度不匹配将 TransferSize 配置为 32 位长度按 4 字节对齐数据与读取缓冲错位DummyCycles 配置过多或过少对照 W25Q64 数据手册核实读命令的 dummy 周期数偶发数据错误D-Cache 未 invalidate在 DMA 完成后调用 SCB_InvalidateDCache_by_Addr擦除后写不进去忘了写使能 0x06或状态寄存器保护位打开每次写操作前发 0x06检查 SR1 的 BP 位换一块板子就完全不通引脚复用冲突或时钟树不一致核对 CubeMX 引脚分配确认没有外设冲突这些坑算不上高深但每个都能消耗你一个下午甚至一天。当初要是有人直接给我这样一张表我能少走不少弯路。5. 实测效果与性能优化建议5.1 实测条件与性能数据我在自己的板卡上做了一个简单测试读取 W25Q64 前 4KB 数据分别用三种方式对比耗时。第一种是普通 SPI 模式的轮询读取时钟约 40MHz第二种是 QSPI 间接模式配合 CPU 轮询第三种是 QSPI MDMA。测试条件比较朴素H743 主频 480MHzQSPI 时钟大约 50MHz关闭编译器优化用 DWT 计数器计时。结果如下读取方式4KB 耗时CPU 占用情况普通 SPI 轮询约 1.1ms全程占用QSPI 间接模式轮询约 0.3ms全程占用QSPI MDMA约 0.25ms仅启动和终断占用MDMA 方式和 QSPI 轮询在这个小数据量下的差距不明显因为 4KB 的数据量还不足以拉开差距但换成 8MB 全片读取差距就非常明显了。MDMA 在读取过程中CPU 可以继续跑其他任务比如刷新屏幕、处理串口协议。对于真实产品来说“CPU 占用率下降”比“单次读取微秒级提升”更有意义。5.2 进一步优化方向如果后续项目需要从 QSPI Flash 读大量数据比如 GUI 需要从外置 Flash 加载整张背景图我不建议继续用间接模式加 DMA。更好的方式是切换成 QSPI 内存映射模式把外部 Flash 直接映射到0x90000000起始的地址空间然后用指针直接访问。这种方式下CPU 的 D-Cache 也可以参与缓存配合SCB_CleanDCache和SCB_InvalidateDCache维护一致性读取速度能再上一个台阶。内存映射模式也不是没有代价。它适合“读多写少”的场景因为写操作仍然要走间接模式如果你在内存映射模式下做写擦操作必须先把 FMODE 切回间接模式。例程里我保留了两种模式的切换接口但默认用间接模式加 MDMA主要考虑是逻辑链路更清晰适合教学理解。如果要把接口时钟拉到 80MHz 以上还要重点检查 PCB 走线CLK 和数据线尽量等长减少串扰在 CubeMX 里微调SampleShifting找到最稳定的采样点。每个人的板子布线不一样没有一个绝对的“最优”参数只能在示波器辅助下去试。我后面还会继续把这套驱动裁剪成纯寄存器版本去掉 HAL 库的冗余调用专门为生产环境做一个轻量实现。到那时候再跑一次 8MB 全片读取横向对比数据会更有说服力。最后再分享一个实操习惯调 QSPI 这种外设逻辑分析仪和示波器不能省。哪怕只有一个最便宜的逻辑分析仪也要把 CLK、NCS、IO0、IO1 四路信号抓下来看一轮很多配置问题一眼就能看出来比反复改参数烧录快得多。我自己就是在把波形看懂之后才真正理解了 QSPI 的时序关系后面再换其他型号 Flash 和 MCU心里都有底。本文还有配套的精品资源点击获取