尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

STM32H743 QSPI+MDMA实现外部Flash高速数据搬运

STM32H743 QSPI+MDMA实现外部Flash高速数据搬运 简介本资源是一套面向嵌入式开发工程师与STM32进阶学习者的实战型实验例程聚焦STM32H743IIT6单片机通过QSPI接口高效读取W25Q64 Flash的完整实现方案重点解决高速外部存储器访问中CPU负载高、传输效率低等典型痛点适用于工业控制、数据记录及Bootloader开发等对实时性与带宽有要求的场景。压缩包共95个文件含55个头文件.h定义外设结构与宏配置、28个源文件.c实现QSPI初始化、MDMA通道配置、命令序列发送、中断服务及数据校验逻辑另有Keil工程文件.uvprojx、启动代码.s、调试配置.dbgconf及烧录脚本.bat等关键支撑文件整体大小为954KB。已有569人下载学习提供从底层寄存器配置到HAL库调用的全流程参考目录结构清晰分层Drivers/Startup/User含CMSIS核心支持与HAL驱动模块便于快速移植与二次开发。 最近调STM32H743IIT6手头正好有一颗W25Q64就顺手把QSPI和MDMA这套组合完整跑了一遍。这个实验说白了就是用QSPI接口挂一颗8MB外部Flash然后用MDMA直接把Flash里的数据搬运到内部SRAM全程不占用CPU。折腾完这一圈我对H7的存储架构、QSPI的命令时序、MDMA的突发传输算是彻底摸透了这里把整个思路和代码细节都整理出来。如果你正在做H7系列的板子刚好需要外扩Flash存字库、图片、固件或者日志数据又不想让CPU在搬运数据的时候被拖住那这篇内容应该能帮到你。整个实验涉及的核心点比较多QSPI的间接模式、内存映射模式、W25Q64的扇区擦除和页编程、MDMA的软件触发搬运一环扣一环任何一个地方配置不对都跑不出正确结果。我会从硬件选型逻辑讲到寄存器配置再给出可直接抄作业的代码最后把实测中容易踩的坑全部列出来。1. 项目整体设计与方案选型1.1 为什么是STM32H743IIT6 W25Q64STM32H743IIT6这颗料在H7系列里属于中高端型号Cortex-M7内核跑480MHz片上带了2MB Flash和1MB SRAMLQFP176封装也不算特别难焊。很多人选H7不仅仅是看主频更看重它那一坨高速外设其中就有QUADSPI简称QSPI这是H7相比F1/F4系列一个很实用的升级点。W25Q64是华邦的SPI Nor Flash容量8MB工作电压3.3V最大支持104MHz的时钟频率标准SPI、Dual SPI、Quad SPI全都支持。8MB这个容量非常微妙跑一个轻量级GUI字库绰绰有余存几个字库文件或者音频采样绰绰有余做Bootloader的App备份区也够用淘宝价格还便宜。更关键的是它的命令集是行业通用风格换别的品牌Nor Flash基本也能兼容适合用来做实验。选这两个组合主要原因是它们之间的QSPI接口能跑很高的带宽而且引脚占用极少。H743的QSPI外设支持四线IO只需要6个引脚CLK、CS、IO0~IO3就能访问一颗8MB Flash对比同样容量的并行NOR Flash或者SDRAMPCB布线压力小非常多。对于H743这种176脚起步的芯片来说省下来的引脚可以留给以太网、摄像头、显示屏这些更重要的外设。1.2 QSPI和普通SPI的本质区别很多人第一次接触QSPI会很疑惑它和单片机上的SPI外设到底差在哪最直观的区别是引脚数量。普通SPI是1收1发加时钟和片选一次只能传1位数据QSPI把数据线扩展到了4根一个时钟周期能传4位数据极限吞吐大约是普通SPI的四倍。但QSPI真正的核心价值不在引脚数而在它独有的“内存映射模式”Memory-Mapped Mode。这个模式开启之后外部Flash会被映射到MCU的地址空间中比如STM32H7的QSPI映射基地址是0x90000000。CPU直接对着这个地址读写就像访问内部Flash一样不用手动发命令、读数据、轮询状态。这个特性在做代码原地执行、字库指针索引、或者把外部Flash当成普通只读存储器时非常方便。和普通SPI对比一句话总结普通SPI适合跑协议指令交互QSPI适合把Flash变成一块“看得见摸得着”的内存区域。本实验的核心就是利用QSPI的内存映射模式把W25Q64变成一块挂在0x90000000的外部只读存储器。1.3 为什么QSPI偏要配MDMA先说说如果不配MDMA直接让CPU去读QSPI映射区会怎样。代码写起来确实简单一个for循环从0x90000000逐个字节拷到数组里就行。但问题在于CPU每次读QSPI映射地址都要等QSPI外设从Flash串行取回数据。Flash的串行时钟哪怕跑到100MHz折合每字节也要几十个时钟周期CPU在循环里一次一次地等整个480MHz的内核就被这个慢速外设拖住了。你开着定时器中断或者跑着实时系统任务调度就会出现明显波动。这时候MDMAMaster DMA的价值就出来了。MDMA是STM32H7引入的新一代DMA控制器挂在AXI总线上可以软件触发启动传输不需要外设产生请求信号。这意味着你可以把源地址直接指向0x90000000的QSPI映射区目标地址指向内部SRAM配置好数据长度然后启动一次块传输。传输过程完全由MDMA总线控制器接管CPU可以继续跑任务传输完成后触发一次中断通知CPU处理数据。这个方案解决了三个问题一是CPU不再被慢速Flash访问阻塞二是MDMA支持突发传输能连续从QSPI FIFO里拿数据平均带宽比CPU逐字节读取高不少三是传输完成后才打断CPU实时性可控。所以这个实验本质上是“QSPI解决存储扩展问题MDMA解决数据搬运效率问题”两个技术点刚好互补。2. QSPI与W25Q64的关键操作细节2.1 W25Q64的常用命令与时序W25Q64的命令集看着一大堆但只要不是做Flash驱动库常用的就那么几个。我把实验中用到的命令整理成了一张表方便对照命令名指令码地址长度数据长度说明Write Enable0x06无无写使能写任何寄存器/编程/擦除前必须发Read Status Register0x05无1字节读状态寄存器bit0为BUSYbit1为WELRead JEDEC ID0x9F无3字节返回EF 40 17可确认芯片型号Page Program0x0224位最多256字节单线页编程跨页边界需要拆分Sector Erase0x2024位无4KB扇区擦除Block Erase0xD824位无64KB块擦除Read Data0x0324位可变标准SPI读速度慢适合调试Quad Output Fast Read0x6B24位可变4线数据输出读实验中使用的主读命令命令时序看起来有点绕但我总结一个通用规律大部分命令都是“先发命令字节再发地址如果需要再插dummy周期如果需要再收发数据”。STM32的QSPI外设已经把这些时序用硬件做成了模板我们只需要在HAL库的QSPI_CommandTypeDef结构体里填好“命令用几根线发、地址用几根线发、数据用几根线发、dummy周期几个”外设会自动生成完整时序不需要手动翻转GPIO去模拟。2.2 CubeMX中QSPI的初始化配置在STM32CubeMX里初始化QSPI其实非常快但参数含义如果不搞清楚后面出问题根本不知道从哪查起。我实验里用的配置如下参数说明直接写在注释里hqspi.Init.ClockPrescaler 2; // QSPI时钟分频H743的QSPI输入时钟经过分频后得到Flash时钟 hqspi.Init.FifoThreshold 4; // FIFO阈值4字节配合MDMA突发读取 hqspi.Init.SampleShifting QSPI_SAMPLE_SHIFTING_NONE; hqspi.Init.FlashSize 23; // 8MB Flash2^23字节不同HAL版本计算方式稍有差异 hqspi.Init.ChipSelectHighTime QSPI_CS_HIGH_TIME_1_CYCLE; hqspi.Init.ClockMode QSPI_CLOCK_MODE_0; // CPOL0CPHA0W25Q64支持的模式 hqspi.Init.FlashID QSPI_FLASH_ID_1; hqspi.Init.DualFlash QSPI_DUALFLASH_DISABLE;这里重点解释两个参数。FlashSize用于告诉QSPI外设“我这个Flash有多大”这样硬件才知道内存映射模式下地址上限在哪。W25Q64是8MB也就是2^23字节我在工程里写23。但注意有的资料里会写成22原因是ST参考手册里对FSIZE位域的定义是2^(FSIZE1)字节而CubeMX不同版本生成的代码又有差异。你只要记住一个原则如果发现Flash高地址区域访问不到把FlashSize减1再试如果所有地址都能正常读取就不影响使用。FifoThreshold是接收FIFO的水位阈值。QSPI外设内部有一个16字节的FIFO当接收数据量达到这个阈值时会产生标志。阈值设太小时外设频繁置标志影响DMA效率设太大时短数据传输要等FIFO攒够才可读响应变慢。实验里取4字节是一个比较均衡的值。2.3 读写W25Q64的完整操作流程QSPI外设配置好之后还不能直接进入内存映射模式因为一片刚上电的Flash处于未知状态必须先通过间接模式把Flash擦干净、把要读的数据写进去。整个流程分成五步读JEDEC ID确认QSPI时序配置正确、Flash硬件连接没问题。发送写使能命令将状态寄存器的WEL位置1。发送扇区擦除命令把目标地址所在的4KB扇区擦成0xFF。轮询状态寄存器的BUSY位等擦除完成。发送页编程命令把数据写入Flash。这一步的代码用HAL库封装得很干净。读ID的过程如下uint8_t id[3] {0}; QSPI_CommandTypeDef cmd {0}; cmd.InstructionMode QSPI_INSTRUCTION_1_LINE; cmd.Instruction 0x9F; // JEDEC ID命令 cmd.AddressMode QSPI_ADDRESS_NONE; cmd.AlternateByteMode QSPI_ALTERNATE_BYTES_NONE; cmd.DataMode QSPI_DATA_1_LINE; cmd.DummyCycles 0; cmd.NbData 3; HAL_QSPI_Command(hqspi, cmd, HAL_MAX_DELAY); HAL_QSPI_Receive(hqspi, id, HAL_MAX_DELAY); if (id[0] 0xEF id[1] 0x40 id[2] 0x17) { // W25Q64确认连接成功 }读ID返回3个字节分别是厂商ID0xEF、类型0x40、容量0x17代表64Mbit。如果读到的不是这组数据基本可以判定是接线、时钟极性或者分频系数的问题后面排错章节会详细讲。擦除和写使能的关键在于“先使能、再操作”的顺序。W25Q64为了防误写几乎所有的编程和擦除操作之前都必须先发0x06写使能命令。如果漏发后续命令会被Flash忽略这是新手最容易踩的坑。而擦除之后必须轮询BUSY位直到Flash内部操作完成才能进行下一步。4KB扇区擦除典型耗时在150ms左右轮询超时时间我习惯给500ms以上。页编程则需要特别注意跨页边界问题。W25Q64的页大小是256字节页编程命令一次最多写入256字节如果写入的地址范围跨越了页边界比如从0x00FF开始写20字节Flash会拒绝工作或者数据错乱。正确做法是每写一页就检查剩余长度拆分地址。我在实验里写了一个简单的封装函数每次调用都自动计算当前页剩余空间保证单次写入不超过页边界。2.4 内存映射模式的进入方式当数据已经写入Flash并通过间接模式读校验之后就可以把QSPI切到内存映射模式。这个模式下CPU不需要再关心读命令怎么发直接把0x90000000当成一个只读地址段访问就行。进入内存映射的HAL代码如下QSPI_MemoryMappedTypeDef mem_cfg {0}; mem_cfg.TimeOutActivation QSPI_TIMEOUT_COUNTER_DISABLE; mem_cfg.TimeOutPeriod 0; HAL_QSPI_MemoryMapped(hqspi, mem_cfg, HAL_MAX_DELAY);表面看就这几行但QSPI外设内部已经根据初始化阶段配置好的指令模板把“发命令0x6B → 发24位地址 → 插8个dummy周期 → 四线输出数据”整套时序固化了下来。之后每次访问0x90000000地址QSPI外设都会自动执行这套时序。注意一点进入内存映射模式之前必须保证Flash处于空闲状态没有正在进行的擦除或编程操作。另外内存映射模式只适合读如果还想继续用间接模式写Flash需要先退出内存映射模式重新配置QSPI为间接模式。这块我在实验初期反复切换后来总结出一个规则系统启动流程里先把Flash的初始化、擦除、编程全部在间接模式阶段做完最后统一切到内存映射保持模式单一能省掉很多状态管理的麻烦。3. MDMA工作机制与配置3.1 H7的DMA家族与MDMA定位STM32H7和F4系列有一个很大的不同片上不止一个DMA控制器而是有DMA1、DMA2、BDMA、MDMA四个各自挂在不同的总线域上。很多人在H7上做DMA的时候沿用F4的经验结果发现DMA2访问不了某些地址原因是没搞清楚几个控制器的职责范围。控制器挂载总线主要访问范围特点DMA1/DMA2AHB/APB片上外设、内部SRAM需要外设请求信号触发适合外设与内存搬运BDMAD3域APB备份SRAM、低速外设挂在低功耗域主要服务低速场景MDMAAXI内部SRAM、外部存储器、QSPI映射区软件触发或DMA事件级联触发突发能力强支持二维传输本实验里QSPI映射区挂在AHB总线上内部SRAM也在AHB/AXI访问范围内理论上DMA2也可以做搬运。但DMA2的请求源是从外设触发的对于QSPI映射区这种“地址空间型”存储来说没有现成的请求事件可用。MDMA则可以直接用软件触发启动传输相当于一个“内存到内存”的高速搬运工这是选择MDMA最直接的理由。3.2 MDMA关键参数有哪些MDMA的寄存器结构比DMA复杂不少但HAL库把大部分细节封装好了。初始化参数里最需要理解的是数据宽度、突发大小、地址递增方式、传输触发模式这四个。MDMA_HandleTypeDef hmdma {0}; hmdma.Instance MDMA_Channel0; hmdma.Init.Request MDMA_REQUEST_SW; // 软件触发 hmdma.Init.TransferTriggerMode MDMA_BLOCK_TRANSFER; // 块传输 hmdma.Init.Priority MDMA_PRIORITY_HIGH; hmdma.Init.Endianness MDMA_LITTLE_ENDIANNESS_PRESERVE; hmdma.Init.SourceInc MDMA_SRC_INC_WORD; // 源地址递增 hmdma.Init.DestinationInc MDMA_DEST_INC_WORD; // 目的地址递增 hmdma.Init.SourceDataSize MDMA_SRC_DATASIZE_WORD; // 源数据宽度32位 hmdma.Init.DestDataSize MDMA_DEST_DATASIZE_WORD; // 目的数据宽度32位 hmdma.Init.DataAlignment MDMA_DATAALIGN_PACKEN; hmdma.Init.BufferTransferLength 0; hmdma.Init.SourceBurst MDMA_SOURCE_BURST_4BEATS; // 源突发4次 hmdma.Init.DestBurst MDMA_DEST_BURST_4BEATS; // 目的突发4次 hmdma.Init.SourceBlockAddressOffset 0; hmdma.Init.DestBlockAddressOffset 0; HAL_MDMA_Init(hmdma);数据宽度选择32位的原因是QSPI映射区支持32位总线访问一次读4字节效率远高于逐字节读。突发大小选择4次意思是MDMA每次突发连续读4个32位数据合计16字节这正好和QSPI外设的FIFO深度匹配不会把FIFO压爆。3.3 软件触发怎么就能搬运数据MDMA和DMA最大的不同在于触发方式。DMA必须等外设发出DMA请求信号比如串口接收到一个字节、定时器更新事件它才开始搬运。而MDMA可以在没有任何外设事件的情况下由软件直接置位触发位立刻启动一次传输。这就解锁了一个非常有用的场景当源地址和目标地址都是存储空间时数据搬运根本不需要“外设请求”只要告诉MDMA“从哪搬到哪、搬多长”就行。本实验里源地址是0x90000000QSPI映射区目标地址是内部SRAM数组所以直接软件触发没问题。启动传输的HAL代码如下HAL_MDMA_Start_IT(hmdma, (uint32_t)(QSPI_BASE offset), // 源地址Flash偏移 (uint32_t)rx_buf, // 目标地址SRAM缓冲区 length); // 传输长度字节启动之后MDMA会自己产生AXI总线事务连续访问QSPI映射区。QSPI外设每收到一次总线读请求就自动从Flash串行取回数据并填入FIFOMDMA再从FIFO里拿走。这个过程中CPU完全空闲可以在主循环里干别的事传输完成后再在中断回调里处理数据。3.4 D-Cache在MDMA读取场景里的坑H7开启D-Cache之后MDMA读取会带来一个隐蔽问题。MDMA是总线主设备它把数据从QSPI映射区搬到SRAM缓冲区时写入的是物理SRAM地址不经过CPU的D-Cache。如果CPU之前访问过这个SRAM缓冲区D-Cache里可能有这份数据的旧副本搬运完成后CPU去读缓冲区命中的是Cache里的旧数据读不到MDMA刚写入的新内容。解决方法是传输完成后对缓冲区地址做一次Cache无效化操作SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, length);Invalidate操作会把Cache里对应地址的行标记为无效下次CPU访问时强制从SRAM物理地址重新读取。这个坑非常典型H7工程里只要同时用了DMA/MDMA和D-Cache基本都会遇到。我的建议是所有的MDMA中断回调里第一件事就是无效化目标缓冲区不要等数据使用前本文还有配套的精品资源点击获取
返回列表