拓冰建站拓冰建站
首页 / 资讯中心 / 正文

基于HAL库的STM32H743 QSPI读写W25Q64完整例程

简介面向STM32H743IIT6单片机与W25Q64闪存之间的QSPI高速通信需求这套实验例程基于ST官方HAL库给出了可直接运行的读写示例面向嵌入式开发者和学生适用于学习外部存储扩展、驱动移植以及性能调优。压缩包共收录95个文件主体为C源文件与H头文件同时包含Keil工程文件、启动汇编文件、HEX烧录文件、调试配置和清理脚本等整体大小约952KB目录遵循HAL库标准工程结构便于用户快速定位驱动和用户代码。目前已有514人学习下载对于正在使用STM32H743或接触QSPI协议的开发者来说是一份不错的参考。例程具体实现了QSPI引脚初始化、时钟分频配置、W25Q64使能与参数设置、直接读写操作以及错误状态查询并预留了中断和DMA传输的扩展思路通过阅读和修改这些代码开发者能够掌握HAL库的调用要点理解命令、地址和数据在四线SPI模式下的发送流程同时可将该工程作为后续集成文件系统、BootLoader或RTOS的基础模板减少重复开发工作。整体代码简洁、依赖少适合快速验证硬件和二次开发。1. 为什么拿 STM32H743IIT6 做 QSPI 实验第一个要搞懂的是“直接读写”STM32H743IIT6 的 QUADSPI 外设外接一颗 W25Q64是嵌入式里很常见的低成本存储组合8MB 容量、接口只需要 6 根线、掉电不丢失。很多人把 QSPI 理解成“四根线同时发的 SPI”实际 H743 的 QUADSPI 更像一个小型协议控制器它能自动组织 W25Q64 的读、写、擦指令也能在内存映射模式下把 Flash 映射到 0x90000000让 CPU 像读内部 Flash 一样直接取指。这个实验例程的核心动作很简单用 STM32 HAL 库的HAL_QSPI_Command配合HAL_QSPI_Transmit/Receive先跑通间接模式的页写、扇区擦除和读回校验再切到 MemoryMapped 模式验证 XIP 读取。适合刚点亮过 SPI屏、准备做 Bootloader 或远程升级、想给设备加一块掉电不丢参数存储区的工程师。2. QSPI 与 W25Q64 的底层关系从 SPI 到 DSPI 再到 QPI2.1 W25Q64 为什么能跑 QSPI命令、地址、数据的线宽是可配置的W25Q64 内部是一颗 NOR Flash容量 64Mbit也就是 8MB地址宽度 24 位。它的串行接口不只有经典 SPI 的“一条命令线、一条数据输入线、一条数据输出线”还可以把命令、地址、数据分别配置成 1 线、2 线或 4 线。行业里习惯用三个数字描述这种模式比如 1-1-1 表示全部单线1-4-4 表示命令单线、地址四线、数据四线4-4-4 表示全部四线后者就是 QPI。DSPI 是 1-2-2 的过渡形态部分器件支持双线模式但实际项目里很少有人用因为 W25Q64 的封装已经把 IO0 到 IO3 全部引出来了既然四根数据线都在就没有必要在固件里为省一根线牺牲吞吐。四线模式带来的收益不是“四倍频率”而是同样的时钟频率下传输 256 字节数据需要的数据时钟周期少了一大截。例如用 0x03 单线读地址和数据一共要 8 24 2048 个串行时钟改 0xEB 的 1-4-4 模式地址变成 12 个时钟数据变成 512 个时钟总周期数大约降为原来的四分之一。正因为可以在较低时钟频率下拿到更高吞吐w25q64 的四线模式对 PCB 走线的压力也小很多这对 H743 这种高性能 MCU 很实用。2.2 W25Q64 指令集HAL 库命令结构体里每个字段怎么填W25Q64 的指令表不长但每个指令对“是否有地址、是否需要 dummy 周期、数据线数”的要求都不一样。HAL 库的QSPI_CommandTypeDef在填第一个字节之前必须先想清楚指令属性。命令指令码地址Dummy数据典型用途写使能0x06无0无写/擦前置操作读状态寄存器10x05无01 字节检查 BUSY 位页编程0x0224bit01 线写 256 字节扇区擦除0x2024bit0无擦除 4KB读数据0x0324bit01 线最基础的读Fast Read Quad I/O0xEB24bit64 线四线高性能读这六个命令基本能覆盖一个“直接读写”实验。填QSPI_CommandTypeDef时Instruction对应指令码AddressSize对应 24 位地址DummyCycles对应 Fast Read 需要的 dummyDataMode决定数据阶段用几根线。最容易出错的场景是把 0x03 和 0xEB 的 dummy 周期写出不导致连续读出来的数据从第二位开始错位或者明明用的是四线读却忘了把DataMode改成QSPI_DATA_4_LINE控制器只发一根线Flash 则按四线输出读回来自然全是 0xFF。2.3 HAL 库的 QSPI API 顺序Command 在前Transmit/Receive 在后且不带长度HAL 库对 QUADSPI 的封装和 SPI 有一个明显区别HAL_QSPI_Command并不是把数据发出去的函数它只是把指令、地址、dummy 和长度装载进外设真正让数据流动起来的是后面的HAL_QSPI_Transmit或HAL_QSPI_Receive。也就是说绝大多数操作都要“先命令后传输”顺序不能反过来。uint8_t id[3] {0}; QSPI_CommandTypeDef cmd {0}; cmd.Instruction 0x9F; /* JEDEC ID 读命令 */ cmd.InstructionMode QSPI_INSTRUCTION_1_LINE; /* 命令走单线 */ cmd.AddressMode QSPI_ADDRESS_NONE; /* 本条命令无地址 */ cmd.DataMode QSPI_DATA_1_LINE; /* 数据走单线 */ cmd.NbData 3; /* 期望读 3 字节 */ cmd.DummyCycles 0; HAL_QSPI_Command(hqspi, cmd, HAL_QSPI_TIMEOUT_DEFAULT_VALUE); HAL_QSPI_Receive(hqspi, id, HAL_QSPI_TIMEOUT_DEFAULT_VALUE);注意HAL_QSPI_Receive的第三个参数是超时时间不是长度。长度已经由cmd.NbData决定。这个设计很容易被从 SPI HAL 转过来的人忽略一旦把长度写进Receive函数轻则编译报警重则把下一个变量的地址当数据指针。W25Q64 的 JEDEC ID 固定为0xEF 0x40 0x17读出来和这个值一致说明 CubeMX 生成的 QSPI 底层配置基本没有大问题。2.4 什么时候走间接模式什么时候走 MemoryMapped间接模式适合写参数、存日志、做数据采集先发命令再通过 FIFO 搬运数据代码直观随时可以调用。MemoryMapped 模式适合执行代码H743 把 Flash 地址映射到0x90000000CPU 可以在这个地址直接取指或查表不用先把全部数据搬到 RAM。进入内存映射后写操作仍然要走间接模式所以实验例程要保留两种模式的切换接口。完整方案里这两者是互补的不是二选一。提示H743 有 QUADSPI1 和 QUADSPI2 两个独立控制器实验只用其中一个CubeMX 里选 Single Flash 即可不要默认选 Dual Flash双 Bank 配置会让首次读 ID 时把数据拆成两路排查起来更麻烦。3. 用 CubeMX 生成 H743 的 QSPI 工程引脚复用、FCLK 分频和 FlashSize 换算3.1 硬件接线W25Q64 与 QUADSPI_BK1 的六根线W25Q64 的封装引脚和普通 SPI Flash 一样是 8 脚CS、CLK、DI、DO、WP、HOLD 加上电源地。四线模式下 WP 和 HOLD 不再作为写保护与保持输入而是复用成 IO2 和 IO3这一点在硬件设计时就要注意如果板子上这两个引脚被外部上拉到固定电平四线读模式的第二根数据线会被钳住性能反而更差。W25Q64 引脚STM32H743 QUADSPI 功能说明CS#QUADSPI_BK1_NCS片选外设自动控制CLKQUADSPI_BK1_CLK时钟输出DIQUADSPI_BK1_IO0命令与数据发送DOQUADSPI_BK1_IO1数据接收WP#QUADSPI_BK1_IO2四线模式下的数据线HOLD#QUADSPI_BK1_IO3四线模式下的数据线在 CubeMX 的工具菜单里搜索QUADSPI1选择 Single Flash 后软件会列出当前封装可复用的引脚。H743IIT6 封装面积大引脚选择余地多常见做法是让 CubeMX 自动分配再对照原理图手工确认。最稳妥的检查方法是看生成的GPIO_AF配置QUADSPI 系列引脚一般复用为AF10。3.2 H743 时钟配置与 QUADSPI 分频H743 主频可以跑到 480MHz但实验没必要顶着上限跑。典型 CubeMX 工程会配置 HCLK 为 200MHz 或 240MHzQUADSPI 的输入时钟来自内核总线再通过Init.ClockPrescaler做一级分频。实际外设频率约等于“QSPI 输入时钟 / (Prescaler 1)”。W25Q64 的 fast read 最高支持 104MHz四线模式下更看重时序余量。我一般会把 QUADSPI 频率控制在 66MHz 附近例如 HCLK 200MHz 时设ClockPrescaler 2得到约 66.7MHz。如果 PCB 走线长、没有完整地平面再降到 33MHz也就是Prescaler 5。初次调试时宁可慢不要追求跑满先把读写逻辑验证稳定再提高频率。3.2.1 CubeMX 里 QSPI 参数建议表CubeMX 参数建议值理由Clock Prescaler2200MHz 下得到 66.7MHz留出余量FlashSize228MB2^23 字节HAL 需要填对数减一SampleShiftingHalf Clock高频读时序更稳定Chip Select High Time1 CycleCS 释放时间满足 W25Q64 要求Fifo Threshold4间接模式下较均衡读大块可改 8FlashSize是个容易错的参数。W25Q64 容量是 64Mbit也就是 8MB、2^23 字节QUADSPI 寄存器的 FlashSize 字段要求填“地址宽度减一”所以填 22。有些人把 64Mbit 直接当成 64MB填成 25地址空间超过实际容量读尾部数据时行为会变得不可预测。3.3 初始化后先做一次状态寄存器自检CubeMX 生成代码后不要急着写页编程先写一段读状态寄存器 1 的最小函数。上电后状态寄存器 1 如果读出0x00说明 QSPI 控制器至少能与 W25Q64 完成握手如果总是0xFF先怀疑接线和 GPIO 复用再去怀疑时钟频率。QSPI_CommandTypeDef cmd {0}; uint8_t sr1 0xFF; cmd.Instruction 0x05; cmd.InstructionMode QSPI_INSTRUCTION_1_LINE; cmd.AddressMode QSPI_ADDRESS_NONE; cmd.DataMode QSPI_DATA_1_LINE; cmd.NbData 1; cmd.DummyCycles 0; HAL_QSPI_Command(hqspi, cmd, HAL_QSPI_TIMEOUT_DEFAULT_VALUE); HAL_QSPI_Receive(hqspi, sr1, HAL_QSPI_TIMEOUT_DEFAULT_VALUE); printf(SR1 0x%02X\n, sr1);读回0xFF时逻辑分析仪抓 CS、CLK、IO0 三根线如果 CS 没有拉低多半是引脚复用没生效如果 CLK 没有翻转看时钟使能如果 IO0 有波形但 IO1 恒高检查数据线和上拉电阻。这个自检步骤几乎能省下一半的排错时间值得放在实验例程的最前面。4. HAL_QSPI_Command Transmit/Receive 直接读写 W25Q64 的完整例程4.1 写使能、读状态、等待 BUSY 三个基础函数W25Q64 在页编程和扇区擦除之前必须发送 0x06 写使能命令否则写操作会被忽略。擦除或写入完成后状态寄存器 1 的 bit0 会保持 1 一段时间直到内部写操作结束。这三件事每个读写任务都会用到先封装成最小工具函数。static void w25q64_write_enable(void) { QSPI_CommandTypeDef cmd {0}; cmd.Instruction 0x06; cmd.InstructionMode QSPI_INSTRUCTION_1_LINE; cmd.AddressMode QSPI_ADDRESS_NONE; cmd.AddressSize QSPI_ADDRESS_8_BITS; cmd.DataMode QSPI_DATA_NONE; cmd.DummyCycles 0; HAL_QSPI_Command(hqspi, cmd, HAL_QSPI_TIMEOUT_DEFAULT_VALUE); } static uint8_t w25q64_read_status1(void) { QSPI_CommandTypeDef cmd {0}; uint8_t status 0xFF; cmd.Instruction 0x05; cmd.InstructionMode QSPI_INSTRUCTION_1_LINE; cmd.AddressMode QSPI_ADDRESS_NONE; cmd.DataMode QSPI_DATA_1_LINE; cmd.NbData 1; cmd.DummyCycles 0; HAL_QSPI_Command(hqspi, cmd, HAL_QSPI_TIMEOUT_DEFAULT_VALUE); HAL_QSPI_Receive(hqspi, status, HAL_QSPI_TIMEOUT_DEFAULT_VALUE); return status; } static int w25q64_wait_busy(uint32_t timeout_ms) { uint32_t tick HAL_GetTick(); while (w25q64_read_status1() 0x01) { if ((HAL_GetTick() - tick) timeout_ms) { return -1; } } return 0; }w25q64_wait_busy的超时不能省。扇区擦除最长可能需要几百毫秒如果 Flash 处于异常状态没有超时保护的循环会让整个任务卡死。H743 的 HAL 固件库里HAL_GetTick默认由 SysTick 驱动延时精度足够用于这种毫秒级判断。4.2 扇区擦除和页编程地址和对齐都不能想当然W25Q64 的扇区大小是 4KB擦除地址必须是 4096 的整数倍。页编程一次最多写 256 字节而且不能跨页。如果写入长度跨过页边界数据会从页尾绕回该页开头这不是 H743 的问题而是 W25Q64 内部页缓冲区决定的。void w25q64_sector_erase(uint32_t sector_addr) { QSPI_CommandTypeDef cmd {0}; w25q64_write_enable(); cmd.Instruction 0x20; cmd.InstructionMode QSPI_INSTRUCTION_1_LINE; cmd.AddressMode QSPI_ADDRESS_1_LINE; cmd.AddressSize QSPI_ADDRESS_24_BITS; cmd.Address sector_addr; cmd.DataMode QSPI_DATA_NONE; cmd.DummyCycles 0; HAL_QSPI_Command(hqspi, cmd, HAL_QSPI_TIMEOUT_DEFAULT_VALUE); w25q64_wait_busy(1000); } void w25q64_page_program(uint32_t addr, uint8_t *pData, uint16_t len) { QSPI_CommandTypeDef cmd {0}; uint16_t page_left 256 - (addr 0xFF); if (len page_left) { len page_left; } w25q64_write_enable(); cmd.Instruction 0x02; cmd.InstructionMode QSPI_INSTRUCTION_1_LINE; cmd.AddressMode QSPI_ADDRESS_1_LINE; cmd.AddressSize QSPI_ADDRESS_24_BITS; cmd.Address addr; cmd.DataMode QSPI_DATA_1_LINE; cmd.NbData len; cmd.DummyCycles 0; HAL_QSPI_Command(hqspi, cmd, HAL_QSPI_TIMEOUT_DEFAULT_VALUE); HAL_QSPI_Transmit(hqspi, pData, HAL_QSPI_TIMEOUT_DEFAULT_VALUE); w25q64_wait_busy(100); }这两个函数已经能完成“直接写”。调用前要自己做跨页拆分比如写 300 字节时第一次写 256 字节第二次写剩下的 44 字节且第二次地址偏移 256。不要指望在 HAL_QSPI 层自动处理跨页它只负责把NbData个字节送到 Flash不关心页边界。4.3 四线读之前先把 QE 位置 1W25Q64 的 IO2 和 IO3 默认功能是 WP# 和 HOLD#。要把它们切换成数据线必须把状态寄存器 2 的 bit1也就是 QE 位置 1。这一步不做1-4-4 模式读出来的数据会随机出错。void w25q64_quad_enable(void) { QSPI_CommandTypeDef cmd {0}; uint8_t sr2 0; cmd.Instruction 0x35; cmd.InstructionMode QSPI_INSTRUCTION_1_LINE; cmd.AddressMode QSPI_ADDRESS_NONE; cmd.DataMode QSPI_DATA_1_LINE; cmd.NbData 1; cmd.DummyCycles 0; HAL_QSPI_Command(hqspi, cmd, HAL_QSPI_TIMEOUT_DEFAULT_VALUE); HAL_QSPI_Receive(hqspi, sr2, HAL_QSPI_TIMEOUT_DEFAULT_VALUE); if ((sr2 0x02) 0) { w25q64_write_enable(); sr2 | 0x02; cmd.Instruction 0x31; cmd.InstructionMode QSPI_INSTRUCTION_1_LINE; cmd.AddressMode QSPI_ADDRESS_NONE; cmd.DataMode QSPI_DATA_1_LINE; cmd.NbData 1; cmd.DummyCycles 0; HAL_QSPI_Command(hqspi, cmd, HAL_QSPI_TIMEOUT_DEFAULT_VALUE); HAL_QSPI_Transmit(hqspi, sr2, HAL_QSPI_TIMEOUT_DEFAULT_VALUE); w25q64_wait_busy(100); } }这个过程本身也是“直接读写”的一部分读状态寄存器 2 用 0x35写状态寄存器 2 用 0x31。注意写完状态寄存器后同样要等 BUSY 结束否则立即进入四线读模式时内部状态可能还没稳定。4.4 四线读与内存映射的衔接四线读可以直接用间接模式完成命令改为 0xEB地址和数据阶段都走四线并带 6 个 dummy 周期。这个模式下的读性能已经和内存映射接近。void w25q64_read_fast_quad(uint32_t addr, uint8_t *pBuf, uint32_t len) { QSPI_CommandTypeDef cmd {0}; cmd.Instruction 0xEB; cmd.InstructionMode QSPI_INSTRUCTION_1_LINE; cmd.AddressMode QSPI_ADDRESS_4_LINE; cmd.AddressSize QSPI_ADDRESS_24_BITS; cmd.Address addr; cmd.DataMode QSPI_DATA_4_LINE; cmd.NbData len; cmd.DummyCycles 6; HAL_QSPI_Command(hqspi, cmd, HAL_QSPI_TIMEOUT_DEFAULT_VALUE); HAL_QSPI_Receive(hqspi, pBuf, HAL_QSPI_TIMEOUT_DEFAULT_VALUE); }真正要切到 MemoryMapped 时调用HAL_QSPI_MemoryMapped把 0xEB 命令和超时配置交给外设。之后访问0x90000000 偏移量就是读 Flash。这个模式下不能直接调用HAL_QSPI_Transmit写数据要先调用HAL_QSPI_Abort退出映射再走页编程函数。5. 掉电不丢失的验证套路一次擦写、复位回读加上 Cache 失效处理实验例程做到“能读能写”只算完成一半真正有价值的是验证“掉电不丢失”。W25Q64 的部分扇区可以在出厂时写保护但常规操作下写入的数据在断电后不会消失只是等待时间长短的问题。这里有一个完整的验证流程擦除一个扇区写入一组带标志的数据读回比对再让系统复位或切断电源重新上电后从同一地址读取。内存映射模式下最容易出现的坑是 H743 的 D-Cache。H743 内核带有数据缓存第一次从0x90000000读数据后缓存里可能留着旧值。此时哪怕 Flash 内容已经被改掉CPU 再次读同一地址时仍然可能拿到缓存里的历史数据。所以在写入后或断电复位后读取之前要做一次 Cache 失效。uint32_t map_addr 0x90000000 test_addr; SCB_InvalidateDCache_by_Addr((uint32_t *)map_addr, 64); memcpy(rbuf, (uint8_t *)map_addr, 64);SCB_InvalidateDCache_by_Addr的第二个参数是字节数建议和实际读取长度保持一致。如果开启了 I-Cache并且准备直接从 QSPI 执行代码也要在跳转前对代码段做同样处理否则首条指令可能取到旧缓存内容。写数据阶段不要越过映射地址去写映射模式只负责读。验证时建议把擦除、写入、读回三个阶段分开打日志擦除完成后打印ERASE OK页编程完成后打印PROGRAM OK读回比对通过再打印VERIFY OK。出现异常时首先看打印停在哪一步。如果停在擦除重点查写使能是否执行停在编程重点查跨页拆分停在回读重点查 QE 位和 dummy 周期。连续掉电十次数据都不变才算完成掉电不丢失的确认。这一套流程跑顺后还可以把实验代码复用到 Bootloader 的下载区校验、设备标定参数存储、启动日志循环写入等场景。关键是把FlashSize、ClockPrescaler、QE 位和 dummy 周期写进配置文件注释里掉电重新上电后你只需要看 ID 和状态寄存器就能回忆起整条链路。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门