STM32H743 QSPI读写W25Q64:从初始化到校验的完整实现
2026/9/16 13:07:26 网站建设 项目流程

简介:一套用于学习 STM32H743IIT6 通过 QSPI 接口直接读写 W25Q64 闪存的实验例程源码,面向嵌入式入门开发者以及对硬件抽象层驱动感兴趣的技术人员;W25Q64 是一款 64Mbit 的 SPI 闪存,支持四线 QSPI 模式,本例程即演示在 HAL 库下完成该模式的高速数据传输。压缩包共含 95 个文件,以 C 源文件与 H 头文件为主,配合 Keil 工程文件、启动文件、链接脚本、Hex 固件与批处理脚本,整体约 952KB,目录划分清晰,便于直接编译和二次修改;该例程目前已有 514 人学习。代码覆盖 QSPI 外设初始化、Flash 片选与写使能命令、数据读写、错误状态获取等完整流程,均调用 ST 官方 HAL 库 API 实现,可移植性好。通过阅读和调试源码,可以理解 QSPI 通信时序与 Flash 操作机制,掌握 MDK 环境下的驱动调试方法,也为后续引入 RTOS、文件系统或扩展其他 STM32 的 QSPI 应用提供了可复用的参考基础,能有效减少同类项目的重复配置工作。

1. 为什么 STM32H743IIT6 的 QSPI 读写 W25Q64 比 SPI 更值得折腾

STM32H743IIT6 外接 W25Q64 时,普通 SPI 的瓶颈在每拍只能传 1 位数据。QSPI 四线模式用同样时钟把带宽放大 4 倍,H7 的 QUADSPI 控制器还支持把 Flash 映射到 0x90000000 地址空间,调试和运行代码都很方便。这个实验例程是 FK743M1 开发板的 MDK-ARM 工程,基于 HAL 库实现 STM32H743IIT6 通过 QSPI 直接读写 W25Q64。HAL 把 QSPI 封装成 Command 加 Transmit/Receive 模型,比 SPI 多一层命令序列,刚上手容易卡在初始化参数和等待忙状态。这里拆解 QSPI 初始化、间接模式读写和读回校验三个环节,适合正在把 SPI Flash 方案改造成 QSPI,或第一次在 H7 上接 W25Q64 的开发者。

2. QSPI 控制器与 W25Q64 的映射机制

2.1 QUADSPI 与 SPI/DSPI 的本质差异

STM32H743 的 QUADSPI 不是普通 SPI 加了几个 IO,而是一个专门管理外部串行 Flash 的独立外设。普通 SPI 一条时钟线上,每一拍只能传 1 位数据;DSPI 双线模式用 IO0/IO1 并行传 2 位;QSPI 四线模式用 IO0~IO3 并行传 4 位。数据线越宽,同样时钟频率下吞吐越高,这是选择 QSPI 的核心理由。对于 W25Q64 这类 SPI NOR Flash,单线读命令 0x03 在 H7 上即使把时钟推到 100MHz,实测带宽也远低于四线快速读。

除了数据线变宽,QUADSPI 还提供了两个普通 SPI 没有的机制:AHB 映射地址和双 Bank 支持。映射模式下,控制器把 Flash 内容映射到 0x90000000 开始的地址,CPU 不用经过 HAL 函数就能直接读取数据;双 Bank 则可以在同一控制器下接两颗 Flash,扩展容量或做并行读取。这个例程只接了一颗 W25Q64,使用 Bank1,核心路径是间接模式直接读写。

在 H7 的 HAL 库实现里,QSPI 底层寄存器包括 CR、DCR、FCR、DLR、AR、DR 等。初始化时设置通信参数,之后每次发指令都会写几遍这些寄存器。理解不了寄存器也没关系,HAL_QSPI_Command 已经把这些操作收拢成一个函数,只需要关注 Command 结构体里的各个字段。

2.2 W25Q64 的指令集与四线命令

W25Q64 容量是 64Mbit,也就是 8MB,地址需要 23 条。它的命令码分成单线和四线两套,不能混用。比如单线读 0x03 只占一根数据线,而四线快速读 0x6B 会使用四根数据线同时传输地址和数据;单线页编程 0x02 与四线页编程 0x32 也一样。如果初始化时把 DataMode 配成 4 线,却发送 0x03,控制器会按四线时序发送单线命令数据,对端 Flash 解析出来的是完全不同的东西,这是很多 QSPI 读写异常的第一来源。

下表整理了本例程中会用到的 W25Q64 命令:

功能命令码地址宽度数据/说明
WREN 写使能0x060每次擦除/编程前必须发
RDSR 读状态寄存器0x050第 0 位 BUSY,第 1 位 WEL
单线读0x033兼容模式,速度慢
四线快速读0x6B3四线读,本工程使用
四线页编程0x323单页最大 256 字节
四线扇区擦除0x213擦除 4KB 扇区
块擦除0xD83擦除 64KB 块

注意四线命令发送时,命令阶段仍然用单线或双线,因为 QUADSPI 控制器需要先让 Flash 进入正确的指令状态。设置 SIOOMode 为 QSPI_SIOO_INST_EVERY_CMD,可以确保每一条命令都重新发送指令字节,避免上一次残余状态影响后续传输。

2.3 间接模式与 Memory-Mapped 模式怎么选

HAL 库对 QUADSPI 的操作分为两种模式。间接模式下,需要自己构造 QSPI_CommandTypeDef 结构体,指定指令、地址、数据长度和线宽,然后调用 HAL_QSPI_Transmit 或 HAL_QSPI_Receive 完成数据传输。Memory-Mapped 模式下,控制器把 Flash 映射到 0x90000000,CPU 读地址就像读 RAM,不需要显式调用 Receive 函数,但写入和擦除仍然只能走间接模式。

这个实验例程的标题是“直接读写实验例程”,实际使用的是间接模式。原因是间接模式每一步都是显式的命令序列,从写使能到忙状态检查再到数据收发,出错时能定位到具体步骤;而内存映射模式读起来虽然舒服,但一旦配置了 DummyCycles 或采样沿不对,会莫名读到 0xFF,调试起来反而不直观。建议先把间接模式跑通,确认 Flash 读写正常后,再尝试映射模式来做字库或代码存储。后面的章节都会基于间接模式展开。

3. HAL 库配置 QSPI 初始化与引脚复用

3.1 时钟分频与 FlashSize 的计算

STM32H743 的 QUADSPI 时钟来自 AHB1,在 480MHz 主频下默认 AHB1 分频后约 240MHz,实际要参考 RCC 配置。W25Q64 数据手册标称四线快速读最高支持 133MHz,普通读 104MHz,考虑到 PCB 走线和上拉电阻,把 QSPI 时钟分到 50~70MHz 比较稳妥。HAL 库的 ClockDiv 是对 QSPI 内核时钟继续分频,时钟频率等于 AHB 时钟除以 ClockDiv+1。我一般先用逻辑分析仪看波形,再逐步提高频率,不要一开始就压极限。

FlashSize 字段比较绕,它定义为地址宽度减 1,单位是 bit。W25Q64 是 8MB 容量,需要 23 根地址线,所以 FlashSize 设为 22,对应 2^23 = 8M 字节。很多教程把 W25Q128 设成 23 也是这个规律:16MB 需要 24 根地址线,减一就是 23。这个值如果配小,读写大地址会偏移;配大,芯片内部访问会越界到不存在的区域,结果一样是数据不对。

3.2 GPIO 复用初始化完整代码

在 FK743M1 工程里,QSPI 引脚接在 GPIOF 和 GPIOG 上。我这里按常见接法写:CLK=PF10,BK1_IO0=PF8,BK1_IO1=PF9,BK1_IO2=PF7,BK1_IO3=PF6,片选=PG6。实际接线要以板子原理图为准,H7 的 QSPI 引脚可以映射到多组 GPIO。

QSPI_HandleTypeDef hqspi; void MX_QSPI_Init(void) { QSPI_InitTypeDef qspi_init = {0}; hqspi.Instance = QUADSPI; __HAL_RCC_QSPI_CLK_ENABLE(); __HAL_RCC_GPIOF_CLK_ENABLE(); __HAL_RCC_GPIOG_CLK_ENABLE(); GPIO_InitTypeDef gpio_init = {0}; gpio_init.Pin = GPIO_PIN_6 | GPIO_PIN_7 | GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10; gpio_init.Mode = GPIO_MODE_AF_PP; gpio_init.Pull = GPIO_NOPULL; gpio_init.Speed = GPIO_SPEED_FREQ_VERY_HIGH; gpio_init.Alternate = GPIO_AF10_QUADSPI; /* H7 常见 AF10,具体以数据手册为准 */ HAL_GPIO_Init(GPIOF, &gpio_init); gpio_init.Pin = GPIO_PIN_6; /* nCS */ HAL_GPIO_Init(GPIOG, &gpio_init); qspi_init.ClockDiv = 3; /* 240MHz/(3+1)=60MHz */ qspi_init.FlashSize = 22; /* 2^23 = 8MB */ qspi_init.SampleShift = QSPI_SAMPLE_SHIFT_NONE; qspi_init.FlashID = QSPI_FLASH_ID_1; qspi_init.CSHighTime = 1; qspi_init.ClockMode = QSPI_CLOCK_MODE_0; qspi_init.FifoThreshold = 4; qspi_init.DualFlash = QSPI_DUALFLASH_DISABLE; HAL_QSPI_Init(&hqspi, &qspi_init); }

GPIO 速度必须设成 VERY_HIGH,否则 QSPI 高速读写时沿会变得很缓,容易出现随机错误。Pull 一般用 GPIO_NOPULL,如果 PCB 上 Flash 的 WP 和 Hold 引脚没接上拉,需要在外部电路处理,不能光靠内部上下拉。HAL_QSPI_Init 会自己操作控制寄存器,但不会自动控制片选以外的信号线,所以 Flash 的 WP# 和 HOLD# 引脚必须接到高电平,或者复用成普通输出拉高,否则写保护会挡住页编程。

3.3 HAL_QSPI_Init 参数逐项解释

QSPI_InitTypeDef 里的参数比 SPI 多几个,最容易踩坑的是 SampleShift、CSHighTime 和 FifoThreshold。

SampleShift 控制数据采样点相对于输出时钟沿的位置。如果时钟频率低于 60MHz,用默认的 NONE 就行;频率提高后,Flash 端数据返回沿会被 PCB 延迟拉偏,设定一个采样移位可以补偿这部分延迟。我的经验是,出现“第一次读成功、第二次失败”或者高低温测试失败时,优先检查这个字段。

CSHighTime 是命令结束后片选拉高的最少时钟周期数。W25Q64 在快速读命令结尾需要一点时间去释放总线,设成 1 通常够用,但如果你把 DummyCycles 设得很多,这里最好也加 1。FifoThreshold 决定 FIFO 触发阈值,H7 的 QSPI FIFO 深度是 16 字节,这里的 4 表示 FIFO 内剩余数据少于 4 字节时触发传输完成标志。对于大数据块搬运,改这个值能影响 DMA 中断频率,但对功能本身不产生致命影响。

注意:HAL_QSPI_Init 只初始化控制器,并没有真正对 Flash 发送命令。W25Q64 上电后默认处于单线 SPI 状态,只有收到像 0x66/0x99 这样的复位使能序列后才是确定状态。有些板子会在上电时给 CS 脚做特殊处理,这部分直接看时钟波形更可靠。

4. 直接读写实现:从命令序列到数据校验

4.1 写使能与忙状态轮询

W25Q64 的擦除和页编程开始后,Flash 内部会把 CS 拉高当作执行起点,执行期间 BUSY 位置 1,控制器这边不会得到任何中断。因此每次操作后必须轮询状态寄存器,等 BUSY 清零才能继续下一条命令。下面这个函数使用 0x05 读状态寄存器,用单线模式就够了:

static uint8_t W25Q64_ReadStatus(void) { uint8_t status = 0; QSPI_CommandTypeDef cmd = {0}; cmd.Instruction = 0x05; /* RDSR */ cmd.InstructionMode = QSPI_INSTRUCTION_1_LINE; cmd.AddressMode = QSPI_ADDRESS_NONE; cmd.DataMode = QSPI_DATA_1_LINE; cmd.NbData = 1; cmd.DummyCycles = 0; if (HAL_QSPI_Command(&hqspi, &cmd, 1000) == HAL_OK) { HAL_QSPI_Receive(&hqspi, &status, 1000); } return status; } static void W25Q64_WaitBusy(uint32_t timeout_ms) { uint32_t tick = HAL_GetTick(); while ((W25Q64_ReadStatus() & 0x01) != 0) { if ((HAL_GetTick() - tick) > timeout_ms) { break; } } }

读取状态时 DataMode 设置为 1 线比较稳妥。因为 0x05 本身是单线命令,如果配置成 4 线,控制器会在发送命令后直接切到四线读数据,而 W25Q64 的状态寄存器读取不支持四线模式,读回来的字节全是 0x00 或 0xFF,会让 WaitBusy 一直卡住。

4.2 间接模式下命令序列的封装

接下来把三类操作封装:写使能、扇区擦除、页编程。内部都是先填充 QSPI_CommandTypeDef,再调用 HAL_QSPI_Command 进入传输阶段:

static void W25Q64_WriteEnable(void) { QSPI_CommandTypeDef cmd = {0}; cmd.Instruction = 0x06; /* WREN */ cmd.InstructionMode = QSPI_INSTRUCTION_1_LINE; cmd.AddressMode = QSPI_ADDRESS_NONE; cmd.DataMode = QSPI_DATA_NONE; cmd.NbData = 0; HAL_QSPI_Command(&hqspi, &cmd, 1000); } static void W25Q64_EraseSector(uint32_t addr) { W25Q64_WriteEnable(); W25Q64_WaitBusy(100); QSPI_CommandTypeDef cmd = {0}; cmd.Instruction = 0x21; /* 四线扇区擦除 */ cmd.InstructionMode = QSPI_INSTRUCTION_1_LINE; cmd.Address = addr; cmd.AddressMode = QSPI_ADDRESS_4_LINES; cmd.DataMode = QSPI_DATA_NONE; cmd.NbData = 0; cmd.DummyCycles = 0; HAL_QSPI_Command(&hqspi, &cmd, 1000); W25Q64_WaitBusy(1000); } static void W25Q64_PageProgram(uint32_t addr, uint8_t *buf, uint32_t len) { W25Q64_WriteEnable(); W25Q64_WaitBusy(100); QSPI_CommandTypeDef cmd = {0}; cmd.Instruction = 0x32; /* 四线页编程 */ cmd.InstructionMode = QSPI_INSTRUCTION_1_LINE; cmd.Address = addr; cmd.AddressMode = QSPI_ADDRESS_4_LINES; cmd.DataMode = QSPI_DATA_4_LINES; cmd.NbData = len; cmd.DummyCycles = 0; HAL_QSPI_Command(&hqspi, &cmd, 1000); HAL_QSPI_Transmit(&hqspi, buf, 1000); W25Q64_WaitBusy(1000); }

这里有几个细节。第一,擦除和页编程发送命令前都先写使能,W25Q64 的 WEL 位在一次写序列完成后会自动清零,下一次写操作前必须重新 WREN。第二,页编程长度不能超过 256 字节,超出后地址会绕回当前页开头,覆盖掉前面的数据。第三,AddressMode 用 4 线,意味着 3 字节地址会通过四根 IO 并行移入,指令阶段因为 SIOOMode 默认是每条命令都重新发送指令,所以 InstructionMode 还是 1 线。

4.3 大数据读写的边界处理

W25Q64 的扇区是 4KB,擦除粒度是扇区。如果只写 100 字节,也需要先擦掉目标地址所在的一个完整扇区,否则原来该扇区其他位置的旧数据会和本次新写的数据叠加出混乱结果。常见的封装是先备份整个扇区、修改、再擦除、再写回,但那样耗时很大。本实验例程的简化做法是把写入长度限制在扇区对齐范围内,写之前直接擦除目标扇区。

对于读取,可以直接用四线快速读 0x6B,数据长度不限制,只要不超过 8MB 地址空间。读函数和页编程类似,区别是不需要 WREN,调用 HAL_QSPI_Command 后走 HAL_QSPI_Receive 接收数据。Fast Read 在命令后需要插入 DummyCycles,W25Q64 的 0x6B 命令默认是 8 个 dummy 周期,配置时把 cmd.DummyCycles 设为 8,否则读回来的数据会整体左移一位或多位,表现为高字节错乱。

void W25Q64_ReadData(uint32_t addr, uint8_t *buf, uint32_t len) { QSPI_CommandTypeDef cmd = {0}; cmd.Instruction = 0x6B; /* 四线快速读 */ cmd.InstructionMode = QSPI_INSTRUCTION_1_LINE; cmd.Address = addr; cmd.AddressMode = QSPI_ADDRESS_4_LINES; cmd.DataMode = QSPI_DATA_4_LINES; cmd.NbData = len; cmd.DummyCycles = 8; /* W25Q64 固定 8 个 dummy */ HAL_QSPI_Command(&hqspi, &cmd, 1000); HAL_QSPI_Receive(&hqspi, buf, 1000); }

在批量读写过程中,还有一个常见问题:H7 的 QSPI FIFO 是按 32 位对齐读写的。如果接收缓冲区是 uint8_t 数组,但地址没有 4 字节对齐,HAL_QSPI_Receive 内部会走字节拷贝穿行模式,速度下降一点,并不会出错。但如果你开了 DMA 或者使用内存映射模式,缓冲区对齐就变成硬性要求,否则 DMA 会触发数据总线错误。

4.4 错误处理与 HAL_QSPI_GetState

HAL 库给 QSPI 提供了异步状态机。调用 HAL_QSPI_Command 后,如果有 DMA 或中断传输,驱动会进入 QSPI_STATE_BUSY 状态,直到传输完成或超时。错误排查时,先看函数返回值,再看状态和错误码:

HAL_QSPI_StateTypeDef st = HAL_QSPI_GetState(&hqspi); uint32_t err = HAL_QSPI_GetError(&hqspi);

下面列出我实际遇到过的错误和对应场景:

错误码含义常见原因
QSPI_ERROR_TIMEOUT超时命令配置冲突,Flash 未及时响应
QSPI_ERROR_TRANSFER传输错误FIFO 溢出或缓冲区未对齐
QSPI_ERROR_DMADMA 错误DMA 参数配置错误
QSPI_ERROR_CMD命令错误指令模式/地址模式设置不合法

如果 HAL_QSPI_Command 返回 HAL_ERROR,首先检查 cmd 结构体里的 InstructionMode、AddressMode、DataMode 组合是否被硬件支持。比如 AddressMode 为 4 线而 InstructionMode 为 1 线,这是允许的组合;但如果 NbData 为 0,DataMode 却配成 QSPI_DATA_4_LINES,一些 H7 芯片会直接返回错误。保持 DataMode 为 QSPI_DATA_NONE 是更安全的做法。

5. 验证与排错:掉电不丢数据的检查手段

5.1 读回校验

写完后立即读回是第一步,掉电再上电读回是第二步,这两个步骤都不能少。最简单的方法是固定地址写一段特征数据,再读出来逐字节对比。

uint8_t wbuf[256]; uint8_t rbuf[256]; uint32_t addr = 0x00010000; for (int i = 0; i < 256; i++) wbuf[i] = (uint8_t)(i * 7 + 3); W25Q64_EraseSector(addr); W25Q64_PageProgram(addr, wbuf, 256); W25Q64_ReadData(addr, rbuf, 256); if (memcmp(wbuf, rbuf, 256) != 0) Error_Handler();

注意 PageProgram 前已经擦除扇区,且写入地址按扇区对齐。如果地址不按 4KB 对齐,需要先读回备份整个扇区,再擦除,再写入,否则会破坏扇区内其他数据。

5.2 常见坑检查清单

第一,超时时间不能太短。擦除 4KB 扇区通常要几十毫秒,页编程几毫秒,但颗粒体质差异可能翻倍,WaitBusy 超时建议给 1 秒。第二,DummyCycles 不能漏。0x6B 快速读在地址结束后要 8 个 dummy 周期,配错后数据串位。第三,CSHighTime 设 0 会导致 CS 释放太快,出现间歇性失败。第四,读回校验在擦除后和掉电后各做一次,前者区分写问题,后者区分保持问题。

5.3 用调试器观察数据

FK743M1 工程里带了 JLinkSettings.ini,用 J-Link 调试时可以在 Expressions 窗口直接看 rbuf 数组,比串口打印快,还能实时看 HAL_QSPI_GetState 和 HAL_QSPI_GetError 的返回值。如果是内存映射模式,还可以在 Memory 窗口输入 0x90000000 直接看 Flash 内容。

写测试数据时不要用全 0 或全 1。擦除后的 Flash 是 0xFF,写入 0x00 时是逐位烧录,读回“成功”的假象概率很高。改成 A5/5A 这类交错数据,一旦有字节对不上,可以根据错位规律判断是地址线短路还是某根数据线虚焊。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询