【江科大STM32学习笔记-11】SPI通信协议 - 11.2 硬件SPI读写W25Q64

1 硬件 SPI 外设的引入

在上一篇博客《【江科大STM32学习笔记-11】SPI通信协议 - 11.1 软件SPI读写W25Q64》中,SPI 通信通过 CPU 软件逐位控制 GPIO 电平的方式进行模拟实现。该实现方式结构直观、易于理解,但由于通信时序完全依赖软件指令执行,其本质为"软件时序仿真",在高频通信场景下存在明显的效率瓶颈,难以满足较高数据吞吐需求。

为提升通信效率与时序稳定性,本章改用 STM32 内部集成的 SPI 硬件外设实现数据传输。硬件 SPI 通过内部移位寄存器与时钟发生机制自动完成数据的发送与接收过程,CPU 不再参与逐位控制,仅需通过寄存器或库函数完成字节级读写操作,从而显著降低软件开销并提升通信速率与实时性。

基于上述机制变化,本章的驱动层改动主要集中在两个方面:

  • 首先,将 SPI 相关引脚从通用 GPIO 控制模式切换为 SPI 外设复用功能模式,使 SCK、MOSI 等信号由硬件外设直接驱动;
  • 其次,将原有基于 GPIO 翻转的模拟收发逻辑,替换为基于 SPI 数据寄存器的硬件收发接口实现。

在保持上层 W25Q64 通信协议接口不变的前提下,底层驱动完成了从"软件模拟 SPI"到"硬件 SPI 外设驱动"的平滑迁移,实现了接口层的无感替换。最终测试结果与软件 SPI 完全一致,验证了硬件迁移的正确性与协议兼容性。


2 STM32 的 SPI 外设架构与工作机制

STM32F103C8T6(中容量产品)内部集成了两个 SPI 通信外设(SPI1 与 SPI2)。根据器件资源划分,中容量产品不支持 I2S 音频功能,因此本芯片的 SPI 外设仅用于标准 SPI 通信。

SPI 通信外设在硬件层面自动完成时钟生成、数据移位以及收发缓冲管理的完整流程,无需 CPU 参与逐位控制。由于硬件 SPI 的内部电路结构在芯片设计阶段已固化,外设的工作模式、数据帧格式和通信速率等特性,均通过配置对应的控制寄存器来设定。

2.1 SPI 外设架构

STM32F1 的 SPI 外设结构框图如图 2-1 所示,整体可划分为四个核心子模块:

① 通信引脚: 外设与芯片外部的物理接口,包含 MOSI、MISO、SCK 和 NSS 四个功能引脚。

② 波特率发生器: 负责按照配置的分频系数生成 SCK 串行时钟信号。

③ 数据收发逻辑: 包含发送缓冲区(TDR)、接收缓冲区(RDR)和移位寄存器,是执行数据帧收发的核心电路。

④ 整体控制逻辑: 负责协调外设各部分的工作状态,实时维护状态寄存器(SR)中的标志位,并提供 DMA 及 CRC 等附加功能的使能控制。
图 2-1 STM32F1 的 SPI 外设结构框图

以下各节依次对四个子模块的结构与功能展开介绍。

2.1.1 通信引脚映射

SPI 外设支持双线全双工、双线单向及单线三种通信模式,本实验统一配置为双线全双工模式。由于 SPI 外设的内部电路与特定 GPIO 端口之间存在固定的物理连线关系,在该模式下,外设的四个通信引脚(MOSI、MISO、SCK、NSS)只能连接至芯片规定的复用功能引脚,而无法任意指定。SPI1 外设在 STM32F103C8T6 上的默认复用映射关系如表 2-1 所示。

SPI1功能引脚 GPIO端口 信号方向(主机模式) 说明
NSS PA4 输出(可选) 片选信号,本实验使用 GPIO 软件模拟
SCK PA5 输出 串行时钟,由主机产生
MISO PA6 输入 主机输入、从机输出
MOSI PA7 输出 主机输出、从机输入
[表2-1 SPI1引脚复用映射表]

提示: SPI2 外设的默认复用引脚位于 GPIOB 端口:NSS→PB12,SCK→PB13,MISO→PB14,MOSI→PB15。

当 SPI1 的默认引脚与其他外设发生资源冲突时,可通过引脚重映射功能将其转移至 PA15、PB3、PB4 和 PB5。需要注意的是,这四个引脚在芯片上电后默认作为 JTAG 调试接口使用,启用重映射前必须在代码中显式关闭 JTAG 功能,将相关引脚释放为普通 GPIO 或外设复用功能,方可正常使用。

确定引脚映射后,还需为各引脚配置正确的 GPIO 工作模式,以将引脚的电气控制权由通用 GPIO 逻辑转交至 SPI 外设。涉及硬件输出的引脚(SCK 与 MOSI)必须配置为 复用推挽输出(AF_PP)模式 ;作为外设数据输入的引脚(MISO)通常配置为上拉输入(IPU)浮空输入模式。本实验的硬件物理接线与上述默认复用引脚完全一致,无需调整外部连线即可完成底层驱动迁移。

2.1.2 时钟控制逻辑

外设内部的波特率发生器负责在 SCK 引脚上输出串行时钟脉冲,其基准输入为 SPI 外设所挂载总线的时钟频率 。通过配置控制寄存器 CR1 中的波特率预分频器位(BR[2:0]),可对基准时钟进行 2 至 256 的 2 次幂分频,SCK 频率的计算关系如式(2-1)所示:

其中,SPI1 外设挂载于 APB2 总线(=72 MHz),SPI2 外设挂载于 APB1 总线(=36 MHz)。各分频系数与对应 SCK 频率的完整对应关系如表 2-2 所示。

BR[2:0] 分频系数 SCK频率 标准库宏常量
000 2 36 MHz SPI_BaudRatePrescaler_2
001 4 18 MHz SPI_BaudRatePrescaler_4
010 8 9 MHz SPI_BaudRatePrescaler_8
011 16 4.5 MHz SPI_BaudRatePrescaler_16
100 32 2.25 MHz SPI_BaudRatePrescaler_32
101 64 1.125 MHz SPI_BaudRatePrescaler_64
110 128 562.5 kHz SPI_BaudRatePrescaler_128
111 256 281.25 kHz SPI_BaudRatePrescaler_256
[表 2-2 SPI1 波特率分频系数对应关系]

分频系数的选择需在通信速率与信号完整性之间取得平衡。SCK 频率过高时,时钟边沿在 PCB 走线上容易产生反射和振铃,从而导致从机采样错误。本实验选用 128 分频,SCK 频率约为 562.5 kHz,波形稳定可靠,满足实验需求。

除分频系数外,控制寄存器 CR1 还包含时钟极性(CPOL)与时钟相位(CPHA)配置位,分别用于设定 SCK 在空闲状态下的电平以及数据的采样与触发边沿,以兼容 SPI 的四种标准工作模式。

本实验采用模式 0(CPOL = 0,CPHA = 0),即 SCK 空闲时保持低电平,在第一个时钟边沿(上升沿)进行数据采样。W25Q64 采用 SPI Mode 0 时序规范,若配置错误将导致数据采样错位,典型表现为读取 ID 异常(如返回 0xFF 或数据错位)。

2.1.3 数据控制逻辑

数据收发逻辑是 SPI 外设执行数据帧收发的核心电路,由数据寄存器(DR)与移位寄存器协同构成。

  • 数据寄存器(DR): 在物理层面,DR 对应两个独立的硬件存储区,分别是发送缓冲区(TDR)与接收缓冲区(RDR),二者在寄存器寻址层面共用同一地址。CPU 对 DR 执行写操作时,数据由硬件路由存入 TDR;对 DR 执行读操作时,数据实际从 RDR 取出。
  • 移位寄存器: 移位寄存器直接连接外部数据引脚(MOSI 与 MISO),负责执行并行数据与串行数据之间的转换。发送时,移位寄存器从 TDR 获取并行数据,在 SCK 时钟的驱动下将其逐位串行推至 MOSI 引脚;接收时,移位寄存器在 SCK 采样沿对 MISO 引脚电平逐位锁存,完整接收一帧后将其并行转存至 RDR。每帧数据的位宽可配置为 8 位或 16 位,移位顺序可配置为高位先行(MSB First)或低位先行(LSB First)。为兼容 W25Q64 的指令规范,本实验统一采用 8 位数据帧、高位先行的配置。

由于移位寄存器的发送与接收是同步进行的,完成一个字节的发送必然同步完成一个字节的接收。这带来了以下必须遵守的规则:

  • 强制读取: 每次发送完成后,必须同步读取 DR 寄存器。即便不需要接收数据,也需通过读取操作来清除 RXNE 标志并清空 RDR。若软件未能在下一帧接收完成前读走当前数据,硬件将触发上溢标志(OVR)。一旦发生上溢,新接收的数据将被丢弃(不会写入 RDR),接收缓冲区中仍保留此前未被读取的数据。此时必须按照"先读状态寄存器(SR)再读数据寄存器(DR)"的特定硬件序列,方可清除 OVR 标志并恢复正常接收。
  • 主动触发: 若当前仅需读取从机数据而无实际发送内容,主机也必须主动向 TDR 写入一个占位字节(Dummy Byte,通常为 0xFF)。这是为了利用"虚假发送"驱动硬件产生 SCK 时钟,从而完成完整的数据交换时序,将从机数据"交换"进主机的 RDR 中。

2.1.4 整体控制逻辑

整体控制逻辑负责协调外设各部分的工作时序,通过读写控制寄存器(CR)执行参数配置,通过读取状态寄存器(SR)实时监控通信状态。除基础通信功能外,该模块还提供 DMA 传输与 CRC 循环冗余校验等附加功能的使能控制(本实验暂不启用)。

在轮询模式下,软件通过检测 SR 中的以下两个标志位来判断数据读写的时机:

  • TXE(发送缓冲空标志,SR Bit 1): 当 TDR 中的数据完成向移位寄存器的整体转移、TDR 处于空置状态时,硬件自动将 TXE 置 1,表明 CPU 可以写入下一个待发送字节。CPU 执行写 DR 操作时,硬件自动将 TXE 清零。

  • RXNE(接收缓冲非空标志,SR Bit 0): 当移位寄存器完成一帧数据的接收并将其整体转存至 RDR 后,硬件自动将 RXNE 置 1,表明 RDR 中存有待读取的有效数据。CPU 执行读 DR 操作时,硬件自动将 RXNE 清零。

TXE 在写入数据寄存器 DR 时由硬件自动清零,RXNE 在读取 DR 时由硬件自动清零,无需程序插入额外的清除指令,从而简化了轮询模式下的代码逻辑。需要注意的是,若 RXNE 置 1 后 CPU 未及时读取 DR,新接收的数据将被丢弃,并触发 OVR(Overrun)溢出标志,RDR 中原有数据保持不变。因此程序必须保证发送与读取操作的连贯性,并在必要时按"先读 SR 再读 DR"的顺序清除 OVR 标志。

除上述状态标志外,NSS 引脚的管理方式也通过控制寄存器进行配置。外设支持硬件自动管控与软件手动管控两种模式:

  • 硬件模式下,NSS 引脚的电平由 SPI 外设在通信开始和结束时自动拉低与拉高,适用于单从机场景;
  • 软件模式下(SSM = 1,选用 SPI_NSS_Soft),NSS 引脚的电平状态完全由 GPIO 软件指令直接驱动,与 SPI 外设内部状态无关。本实验选用软件模式,以便在一主多从架构中灵活控制各从机的片选时序。

2.2 SPI 全双工通信时序

在主模式双线全双工配置下(BIDIMODE = 0,RXONLY = 0),SPI 外设的数据传输根据帧间是否存在间隙,可分为 帧间存在间隙的传输(SCK 非连续) 与 **无间隙连续时钟传输(Back-to-back transfer)**两种情况。

两者的区别在于软件写入下一字节的时机:

  • 若软件能够在当前字节移位尚未完成时提前写入下一字节,移位寄存器在完成当前帧后可立即装填下一帧数据,SCK 时钟保持连续输出,BSY 标志在整个传输期间维持为 1,形成无间隙连续时钟传输;
  • 若软件写入速度较慢,未能及时填充发送数据,移位寄存器在帧结束后出现空档,SCK 时钟随之暂停,BSY 标志在帧间隙降为 0,此时表现为帧间存在间隙的传输。

本章节代码基于**帧间存在间隙的传输过程(非连续传输模式)**实现;在 2.2.2 节中,将对无间隙连续时钟传输过程作补充说明,供参考。


2.2.1 帧间存在间隙的传输过程(非连续传输模式)

图 2-2 引用自 STM32 参考手册,展示了非连续传输模式下连续发送三字节时,TXE、BSY 标志位及发送缓冲区的变化时序。图中示例采用 CPOL = 1、CPHA = 1(模式 3)的配置,与本实验所用的模式 0 在时钟极性上有所差异,但各标志位的变化规律在两种模式下一致。
图 2-2 主模式非连续传输时序图(双线全双工模式)

阶段一:拉低片选,产生起始信号。 通信开始前,主机通过 GPIO 将片选信号线拉低,从机检测到 /CS 引脚出现下降沿后进入通信就绪状态。由于本实验采用软件 NSS 管理模式(SSM = 1),该片选信号由独立的 GPIO 软件指令直接驱动,与 SPI 外设内部的寄存器状态无关。

阶段二:写入发送数据,启动传输。 主机将待发送字节写入 DR(数据实际存入 TDR),TXE 标志随即被硬件清零。由于移位寄存器处于空闲状态,TDR 中的数据立即并行装载至移位寄存器,TDR 恢复空置,TXE 再次置 1。数据进入移位寄存器的同时,SCK 开始输出时钟脉冲,BSY 标志同步置 1。

阶段三:SCK 时钟驱动,移位寄存器执行逐位收发。 移位寄存器在 SCK 时钟的驱动下,同步执行 MOSI 引脚的数据输出与 MISO 引脚的数据输入。在模式 0(CPOL = 0,CPHA = 0)下,硬件在每个上升沿对 MISO 引脚电平进行采样并移入寄存器,在每个下降沿将寄存器最高位推至 MOSI 引脚输出。经过 8 个时钟周期后,双方移位寄存器完成一次完整的字节置换,整个过程无需任何软件干预。

阶段四:移位完成,读取接收数据,写入下一字节。 8 次移位完成后,接收数据从移位寄存器并行转存至 RDR,RXNE 置 1。由于软件未能在本帧结束前及时写入下一字节,移位寄存器出现空窗,SCK 时钟随之暂停,BSY 降为 0。软件读取 DR 获取接收数据,RXNE 随即清零;若还需发送后续字节,再轮询等待 TXE = 1 后写入,外设重新启动新一轮时序。

阶段五:等待 BSY 降为 0,拉高片选,产生停止信号。 所有字节交换完毕后,软件须等待 BSY 降为 0,确认最后一个字节的移位时序已物理结束,再将片选信号线拉高。从机检测到 /CS 上升沿后终止通信,MISO 引脚恢复高阻态。对于 W25Q64 而言,擦除与页编程指令在 /CS 上升沿到来之前,芯片仅将数据暂存于内部缓冲区,/CS 上升沿才是触发实际物理写入的条件,因此停止信号不仅是时序边界,也是触发从机执行相关操作的关键信号。此外,在最后一个字节发送完成后,软件应先等待 RXNE = 1 并完成数据读取,确保接收缓冲区已清空;随后再等待 BSY 标志清零,确认移位寄存器与总线时序已完全空闲后,方可拉高 NSS。否则可能在最后一位数据尚未完成物理移位时提前终止通信,导致数据帧截断或从机状态异常。


2.2.2 无间隙连续时钟传输过程(连续传输模式)

图 2-3 引用自 STM32 参考手册,展示了连续传输模式下主模式全双工通信时,TXE、RXNE、BSY 标志位及收发缓冲区的完整变化时序。图中示例同样采用 CPOL = 1、CPHA = 1(模式 3)的配置,各标志位的变化规律与模式 0 一致。
图 2-3 主模式连续传输时序图(双线全双工模式)

连续传输的前提是软件在当前字节移位结束前完成对 DR 的写入,使 TDR 中始终存有待发数据。满足该条件后,移位寄存器完成当前帧后可立即装填下一帧,SCK 时钟在字节间保持连续输出,BSY 标志在整个传输期间维持为 1。具体通信流程如下。

步骤一:拉低片选,产生起始信号。 通信开始前,主机通过 GPIO 将片选信号线拉低,从机检测到 /CS 引脚出现下降沿后进入通信就绪状态。

步骤二:写入第一个待发字节,启动传输。 主机将第一个待发字节写入 DR(数据实际存入 TDR),TXE 随即被硬件清零。由于移位寄存器处于空闲状态,数据立即整体并行转移至移位寄存器,TDR 恢复空置,TXE 再次置 1,SCK 开始输出时钟脉冲,BSY 同步置 1,通信正式启动。

步骤三:TXE 置 1 后立即写入下一字节。 软件轮询到 TXE = 1 后,在当前字节仍在移位过程中立即将下一个待发字节写入 DR,TXE 随即清零。该字节暂存于 TDR,待当前帧移位完成后整体转移至移位寄存器,SCK 时钟因此保持连续输出不中断。

步骤四:RXNE 置 1 后读取接收数据,交替推进收发。 每完成一帧移位,接收数据从移位寄存器并行转存至 RDR,RXNE 置 1。软件读取 DR 后 RXNE 由硬件自动清零。对于连续发送 n 个字节的完整流程,步骤三与步骤四交替执行------每次检测到 TXE = 1 时写入下一个待发字节,每次检测到 RXNE = 1 时读取已接收字节,直至最后一个字节写入完毕。

步骤五:等待最后一个字节接收完成,等待 BSY 降为 0,拉高片选,产生停止信号。 最后一个字节写入后,软件等待 RXNE = 1 并读取最后一个接收字节,随后轮询等待 BSY 降为 0,确认移位寄存器已完全空闲后,通过 GPIO 将片选信号线拉高,从机检测到 /CS 上升沿后终止通信并恢复至待机状态。

除轮询模式外,TXE 与 RXNE 标志位均可配置为中断源------置 1 时触发 SPI 中断,在中断服务函数中通过检查对应标志位来区分事件并分别处理。对于大批量数据传输场景,也可启用 DMA 方式自动完成 DR 的读写,彻底释放 CPU,同时从根本上保证数据填充的实时性,避免因软件响应延迟导致传输退化为非连续模式。


3 硬件 SPI 驱动实现(MySPI.c)

在前文完成 SPI 外设架构与关键机制分析的基础上,本章直接进入底层驱动实现。基于 STM32 标准外设库,对原有的软件模拟 SPI 驱动(MySPI.c)进行重构,实现一个阻塞式的硬件 SPI 字节交换接口。

驱动的软件分层架构及核心函数内部的硬件交互逻辑如图 3-1 所示。
图 3-1 硬件 SPI 驱动分层架构图

如图所示,MySPI.c 作为硬件抽象层(HAL),向上层 W25Q64.c 提供统一的同步收发接口(MySPI_SwapByte),屏蔽底层寄存器操作细节。

本次重构主要涉及三个方面:

  • **GPIO 复用模式配置:**PA4(NSS)保持通用推挽输出用于软件片选控制;PA5(SCK)与 PA7(MOSI)配置为复用推挽输出(AF_PP),由 SPI 外设直接驱动;PA6(MISO)配置为输入模式。
  • **SPI 外设参数配置:**通过初始化结构体配置主模式、全双工、8 位数据帧、MSB 先行、波特率分频以及 CPOL/CPHA(模式 0),以匹配 W25Q64 的通信时序要求。
  • **字节交换函数重构:**基于 TXE(发送缓冲空)与 RXNE(接收缓冲非空)标志位实现阻塞式轮询机制,由硬件自动完成一帧数据的收发过程。

3.1 片选控制(NSS)

片选信号(SS)采用软件 NSS 管理模式,由 PA4 引脚通过 GPIO 软件指令直接驱动,与 SPI 外设内部的寄存器状态无关。MySPI_W_SS 函数作为片选信号的底层写入接口,供起始函数与终止函数调用。

cpp 复制代码
/**
  * 函    数:SPI 写 SS 引脚电平(软件模拟 NSS)
  * 参    数:BitValue 需要写入 SS 引脚的电平,0 为低电平,1 为高电平
  * 返 回 值:无
  */
void MySPI_W_SS(uint8_t BitValue)
{
    /* 根据 BitValue 设置 PA4 引脚的输出电平。
       BitValue = 0:拉低 SS,产生片选有效信号,通信开始;
       BitValue = 1:拉高 SS,释放片选信号,通信结束。
    */
    GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue);
}

3.2 SPI 外设初始化

MySPI_Init 函数完成三项初始化工作:开启外设时钟、配置 GPIO 引脚模式、配置 SPI 外设参数并使能。

3.2.1 时钟使能

SPI1 外设及其所使用的 GPIO 引脚均挂载于 APB2 总线,初始化前须分别使能对应的外设时钟。

cpp 复制代码
/* 开启 GPIOA 的时钟,用于 PA4~PA7 引脚的 GPIO 功能配置 */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);

/* 开启 SPI1 外设的时钟,SPI1 挂载于 APB2 总线(72 MHz) */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);

3.2.2 GPIO 复用配置

四个功能引脚根据其信号方向分别配置为不同的 GPIO 工作模式。PA4(SS)由软件控制,配置为通用推挽输出;PA5(SCK)与 PA7(MOSI)的输出控制权需移交至 SPI 外设,配置为复用推挽输出;PA6(MISO)作为外设数据输入引脚,配置为上拉输入以防止总线空闲时引脚悬空。

cpp 复制代码
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;

/* 配置 PA4 为通用推挽输出(GPIO_Mode_Out_PP)
   SS 引脚由软件手动控制电平,不受 SPI 外设管理,
   因此保持通用推挽输出模式,通过 MySPI_W_SS 函数驱动 */
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin  = GPIO_Pin_4;
GPIO_Init(GPIOA, &GPIO_InitStructure);

/* 配置 PA5(SCK)与 PA7(MOSI)为复用推挽输出(GPIO_Mode_AF_PP)
   将引脚的输出控制权由通用 GPIO 逻辑转交至 SPI1 外设,
   使 SPI 外设能够直接驱动这两个引脚产生时钟脉冲和串行数据 */
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin  = GPIO_Pin_5 | GPIO_Pin_7;
GPIO_Init(GPIOA, &GPIO_InitStructure);

/* 配置 PA6(MISO)为输入模式(GPIO_Mode_IPU)
   MISO 为外设数据输入引脚,可配置为上拉输入或浮空输入模式;
   在从机未驱动总线(高阻态)时,上拉输入有助于避免引脚悬空,提高信号稳定性 */
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin  = GPIO_Pin_6;
GPIO_Init(GPIOA, &GPIO_InitStructure);

3.2.3 SPI 外设参数配置与使能

通过 SPI_InitTypeDef 结构体配置 SPI1 的工作模式、数据帧格式、时钟参数及 NSS 管理方式,完成后调用 SPI_Init 将参数写入控制寄存器,再通过 SPI_Cmd 使能外设。

cpp 复制代码
SPI_InitTypeDef SPI_InitStructure;

/* 工作角色:配置为主机模式(MSTR = 1)
   由 STM32 产生 SCK 时钟,主动发起通信 */
SPI_InitStructure.SPI_Mode = SPI_Mode_Master;

/* 通信方向:双线全双工模式(BIDIMODE = 0,RXONLY = 0)
   MOSI 与 MISO 同时工作,每次字节交换同步完成发送与接收 */
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;

/* 数据帧宽度:8 位(DFF = 0)
   每次移位寄存器完成 8 次移位后产生一次 TXE/RXNE 标志位事件 */
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;

/* 移位顺序:高位先行(LSBFIRST = 0)
   符合 W25Q64 指令规范,MSB 最先被推至 MOSI 引脚输出 */
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;

/* 波特率分频:128 分频(BR[2:0] = 110)
   SCK 频率 = 72 MHz / 128 = 562.5 kHz
   该速率稳定可靠,满足实验阶段的信号完整性要求 */
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_128;

/* 时钟极性:低极性(CPOL = 0)
   SCK 在总线空闲时保持低电平 */
SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low;

/* 时钟相位:第一个边沿采样(CPHA = 0)
   在 SCK 第一个上升沿对 MISO 引脚电平进行采样
   CPOL = 0,CPHA = 0 共同决定采用 SPI 模式 0 */
SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;

/* NSS 管理:软件模式(SSM = 1)
   NSS 引脚的电平由 GPIO 软件指令直接驱动,
   便于在一主多从架构中灵活控制各从机的片选时序 */
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;

/* CRC 校验多项式:默认值 7,本实验不启用硬件 CRC 功能 */
SPI_InitStructure.SPI_CRCPolynomial = 7;

/* 将结构体中的配置参数写入 SPI1 的控制寄存器(CR1/CR2) */
SPI_Init(SPI1, &SPI_InitStructure);

/* 使能 SPI1 外设(SPE = 1),外设进入就绪状态,等待数据写入 DR 后启动传输 */
SPI_Cmd(SPI1, ENABLE);

/* 初始化完成后将 SS 引脚拉高,使总线处于默认空闲状态 */
MySPI_W_SS(1);

3.3 通信起始与终止函数

MySPI_Start 与 MySPI_Stop 通过调用 MySPI_W_SS 控制片选引脚的电平,分别产生通信的起始信号与停止信号。

cpp 复制代码
/**
  * 函    数:SPI 起始
  * 参    数:无
  * 返 回 值:无
  */
void MySPI_Start(void)
{
    /* 拉低 SS 引脚,产生下降沿,通知从机进入通信就绪状态 */
    MySPI_W_SS(0);
}

/**
  * 函    数:SPI 终止
  * 参    数:无
  * 返 回 值:无
  */
void MySPI_Stop(void)
{
    /* 拉高 SS 引脚,产生上升沿,通知从机结束本次通信
       对于 W25Q64,该上升沿同时触发擦除与页编程等指令的实际执行 */
    MySPI_W_SS(1);
}

3.4 字节交换函数(核心功能)

MySPI_SwapByte 是底层驱动的核心接口。该函数基于 SPI 状态标志位(TXE / RXNE)实现阻塞式轮询,由软件触发数据传输过程,硬件完成移位与收发,从而实现一个字节的全双工交换。

cpp 复制代码
/**
  * 函    数:SPI 交换传输一个字节(硬件 SPI 模式 0)
  * 参    数:ByteSend 要发送给从机的 8 位数据
  * 返 回 值:从机返回给主机的 8 位数据
  */
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{
    /* 第一步:等待 TXE 标志位置 1
       TXE = 1 表示发送缓冲区(TDR)为空,可以写入新的待发数据。
       首次调用时外设刚完成初始化,TDR 为空,TXE 已为 1,此步骤直接通过;
       连续调用时则需等待上一次数据从 TDR 转移至移位寄存器后,TXE 才再次置 1 */
    while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) != SET);

    /* 第二步:将待发字节写入数据寄存器 DR(实际写入 TDR)
       写入操作发生的同时,硬件自动将 TXE 清零;
       由于移位寄存器处于空闲状态,TDR 中的数据立即并行装载至移位寄存器,
       波特率发生器随即启动,SCK 开始输出时钟脉冲,数据从 MOSI 引脚逐位移出 */
    SPI_I2S_SendData(SPI1, ByteSend);

    /* 第三步:等待 RXNE 标志位置 1
       RXNE = 1 表示接收缓冲区(RDR)非空,即移位寄存器已完成 8 次移位,
       MISO 引脚逐位采集到的完整字节已并行转存至 RDR,可供 CPU 读取 */
    while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) != SET);

    /* 第四步:读取数据寄存器 DR(实际读取 RDR)并返回接收到的字节
       读取操作发生的同时,硬件自动将 RXNE 清零,
       至此一个字节的全双工交换周期完整结束 */
    return SPI_I2S_ReceiveData(SPI1);
}

4 W25Q64 驱动封装(W25Q64.c)

得益于前期严谨的分层解耦设计,底层驱动从软件模拟迁移至硬件 SPI 后,W25Q64.c 中的所有函数无需修改任何逻辑,即可直接运行。协议层通过调用 MySPI_Start、MySPI_Stop 和 MySPI_SwapByte 三个标准化接口与底层通信,完全不感知底层实现方式的变化。

4.1 W25Q64 初始化

W25Q64_Init 作为设备初始化的逻辑入口,其唯一职责是调用底层驱动的初始化函数,完成硬件 SPI 外设的配置与引脚初始化。

cpp 复制代码
/**
  * 函    数:W25Q64 初始化
  * 参    数:无
  * 返 回 值:无
  */
void W25Q64_Init(void)
{
    /* 调用底层 SPI 驱动初始化函数,完成以下工作:
       1. 开启 GPIOA 与 SPI1 的外设时钟;
       2. 配置 PA4 为推挽输出(SS),PA5/PA7 为复用推挽输出(SCK/MOSI),PA6 为上拉输入(MISO);
       3. 配置 SPI1 工作参数(主模式、全双工、8 位帧、模式 0、128 分频、软件 NSS)并使能外设;
       4. 将 SS 引脚初始化为高电平,总线处于空闲状态 */
    MySPI_Init();
}

4.2 读取器件 ID

W25Q64_ReadID 通过发送 JEDEC ID 指令(0x9F)读取芯片的厂商 ID(MID)与设备 ID(DID)。读取 ID 是验证 SPI 链路连通性与模式匹配正确性的首要操作,实验结果应返回 MID = 0xEF,DID = 0x4017。

在读取过程中,我们需要发送 0xFF 作为占位字节(Dummy Byte)。SPI 本质为"移位交换机制",读取操作必须伴随发送操作才能产生 SCK 时钟驱动数据移位。发送 0xFF 既能提供时钟,又不会被 W25Q64 识别为有效指令。

cpp 复制代码
/**
  * 函    数:W25Q64 读取 ID 号
  * 参    数:MID 厂商 ID,通过输出参数返回,W25Q64 返回值为 0xEF
  * 参    数:DID 设备 ID,通过输出参数返回,W25Q64 返回值为 0x4017
  * 返 回 值:无
  */
void W25Q64_ReadID(uint8_t *MID, uint16_t *DID)
{
    MySPI_Start();                                      /* 拉低 SS,产生起始信号 */

    MySPI_SwapByte(W25Q64_JEDEC_ID);                   /* 发送 JEDEC ID 指令(0x9F),通知 W25Q64 准备返回 ID 数据
                                                          发送的同时接收一个无效字节(忽略) */

    *MID = MySPI_SwapByte(W25Q64_DUMMY_BYTE);          /* 发送占位字节(0xFF)以产生 SCK 时钟,
                                                          同步接收 W25Q64 返回的厂商 ID(MID) */

    *DID = MySPI_SwapByte(W25Q64_DUMMY_BYTE);          /* 继续发送占位字节,接收设备 ID 的高 8 位 */
    *DID <<= 8;                                        /* 将高 8 位移至变量的高字节位置 */
    *DID |= MySPI_SwapByte(W25Q64_DUMMY_BYTE);         /* 继续发送占位字节,接收设备 ID 的低 8 位,
                                                          与高 8 位拼接,组成完整的 16 位设备 ID */

    MySPI_Stop();                                       /* 拉高 SS,产生停止信号,结束本次通信 */
}

4.3 写使能

W25Q64 的页编程与擦除操作均需在指令发送前执行一次写使能操作,将芯片内部的 WEL(Write Enable Latch)标志位置 1,否则写操作将被芯片忽略。

cpp 复制代码
/**
  * 函    数:W25Q64 写使能
  * 参    数:无
  * 返 回 值:无
  */
void W25Q64_WriteEnable(void)
{
    MySPI_Start();                        /* 拉低 SS,产生起始信号 */

    MySPI_SwapByte(W25Q64_WRITE_ENABLE);  /* 发送写使能指令(0x06),
                                             置位 W25Q64 内部状态寄存器的 WEL 标志位,
                                             允许后续的页编程或擦除操作执行 */

    MySPI_Stop();                         /* 拉高 SS,产生停止信号
                                             SS 上升沿使写使能指令在芯片内部生效 */
}

4.4 忙状态检测

Flash 存储器的擦除与页编程操作涉及物理电荷转移,需要消耗毫秒级的时间。在此期间,W25Q64 内部状态寄存器 1 的 BUSY 位(Bit 0)保持为 1,主机必须通过轮询该位等待操作完成后,方可发送下一条指令。

cpp 复制代码
/**
  * 函    数:W25Q64 等待忙
  * 参    数:无
  * 返 回 值:无
  */
void W25Q64_WaitBusy(void)
{
    uint32_t Timeout;

    MySPI_Start();                                      /* 拉低 SS,产生起始信号,进入持续读取状态寄存器的通信帧 */

    MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1);     /* 发送读状态寄存器 1 的指令(0x05),
                                                          此后可持续发送占位字节读取状态字节 */

    Timeout = 100000;                                   /* 设置超时计数初值,防止芯片异常时程序陷入死循环 */

    while ((MySPI_SwapByte(W25Q64_DUMMY_BYTE) & 0x01) == 0x01) /* 持续发送占位字节以读取状态寄存器 1,
                                                          通过位掩码 0x01 提取 BUSY 位(Bit 0):
                                                          BUSY = 1 表示芯片内部仍在执行擦写操作,继续等待;
                                                          BUSY = 0 表示操作完成,退出循环 */
    {
        Timeout--;                                      /* 每次循环计数值自减 */
        if (Timeout == 0)                               /* 计数值减至 0,判定为超时 */
        {
            /* 超时错误处理逻辑可在此处添加,例如记录错误码或触发复位 */
            break;                                      /* 跳出等待循环,避免程序永久阻塞 */
        }
    }

    MySPI_Stop();                                       /* 拉高 SS,产生停止信号,结束状态寄存器读取帧 */
}

4.5 扇区擦除

W25Q64 以扇区(4 KB)为最小擦除单位,擦除后该扇区内所有字节恢复为 0xFF。执行擦除前须先调用写使能,擦除指令发送完成后须调用等待忙函数,待芯片完成物理擦除后方可进行后续操作。

cpp 复制代码
/**
  * 函    数:W25Q64 扇区擦除(4 KB)
  * 参    数:Address 目标扇区内的任意地址,范围:0x000000~0x7FFFFF
  *           芯片内部会自动对齐至该地址所在扇区的起始位置
  * 返 回 值:无
  */
void W25Q64_SectorErase(uint32_t Address)
{
    W25Q64_WriteEnable();                               /* 发送写使能指令,置位 WEL 标志位,
                                                          授权后续擦除操作的执行权限 */

    MySPI_Start();                                      /* 拉低 SS,产生起始信号 */

    MySPI_SwapByte(W25Q64_SECTOR_ERASE_4KB);           /* 发送扇区擦除指令(0x20) */

    MySPI_SwapByte(Address >> 16);                     /* 发送 24 位地址的高 8 位(A23~A16) */
    MySPI_SwapByte(Address >> 8);                      /* 发送 24 位地址的中 8 位(A15~A8) */
    MySPI_SwapByte(Address);                           /* 发送 24 位地址的低 8 位(A7~A0) */

    MySPI_Stop();                                       /* 拉高 SS,产生停止信号
                                                          SS 上升沿触发 W25Q64 启动内部实际擦除操作 */

    W25Q64_WaitBusy();                                  /* 轮询等待芯片完成物理擦除,
                                                          期间 W25Q64 内部状态寄存器的 BUSY 位持续为 1 */
}

4.6 页编程

W25Q64 以页(256 B)为单位进行编程,单次写入的数据不能跨越页边界。执行页编程前须先确保目标区域已完成擦除(即数据为 0xFF),并调用写使能。

cpp 复制代码
/**
  * 函    数:W25Q64 页编程
  * 参    数:Address 页编程的起始地址,范围:0x000000~0x7FFFFF
  * 参    数:DataArray 指向待写入数据数组的指针
  * 参    数:Count 写入的字节数量,范围:1~256
  * 返 回 值:无
  * 注意事项:写入地址范围不得跨越页边界(256 B 对齐),否则写入地址将回绕至页首
  */
void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count)
{
    uint16_t i;

    W25Q64_WriteEnable();                               /* 发送写使能指令,置位 WEL 标志位,
                                                          授权后续页编程操作的执行权限 */

    MySPI_Start();                                      /* 拉低 SS,产生起始信号 */

    MySPI_SwapByte(W25Q64_PAGE_PROGRAM);               /* 发送页编程指令(0x02) */

    MySPI_SwapByte(Address >> 16);                     /* 发送 24 位起始地址的高 8 位(A23~A16) */
    MySPI_SwapByte(Address >> 8);                      /* 发送 24 位起始地址的中 8 位(A15~A8) */
    MySPI_SwapByte(Address);                           /* 发送 24 位起始地址的低 8 位(A7~A0) */

    for (i = 0; i < Count; i++)                        /* 循环发送 Count 个待写入字节 */
    {
        MySPI_SwapByte(DataArray[i]);                  /* 依次发送数组中的数据,
                                                          W25Q64 在接收每个字节后自动将地址指针递增 */
    }

    MySPI_Stop();                                       /* 拉高 SS,产生停止信号
                                                          SS 上升沿触发 W25Q64 启动内部实际编程操作 */

    W25Q64_WaitBusy();                                  /* 轮询等待芯片完成物理编程,
                                                          页编程通常需要 0.4~3 ms */
}

4.7 读取数据

读取操作无需写使能,也无需等待忙,发送指令与起始地址后,可连续读取任意数量的字节,读取期间地址指针自动递增,直至片选拉高为止。

cpp 复制代码
/**
  * 函    数:W25Q64 读取数据
  * 参    数:Address 读取起始地址,范围:0x000000~0x7FFFFF
  * 参    数:DataArray 指向用于存储读取数据的数组的指针,通过输出参数返回
  * 参    数:Count 读取的字节数量,范围:1~0x800000
  * 返 回 值:无
  */
void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count)
{
    uint32_t i;

    MySPI_Start();                                      /* 拉低 SS,产生起始信号 */

    MySPI_SwapByte(W25Q64_READ_DATA);                  /* 发送读数据指令(0x03) */

    MySPI_SwapByte(Address >> 16);                     /* 发送 24 位起始地址的高 8 位(A23~A16) */
    MySPI_SwapByte(Address >> 8);                      /* 发送 24 位起始地址的中 8 位(A15~A8) */
    MySPI_SwapByte(Address);                           /* 发送 24 位起始地址的低 8 位(A7~A0) */

    for (i = 0; i < Count; i++)                        /* 循环读取 Count 个字节 */
    {
        DataArray[i] = MySPI_SwapByte(W25Q64_DUMMY_BYTE); /* 发送占位字节(0xFF)以产生 SCK 时钟,
                                                              同步接收 W25Q64 在当前地址返回的数据字节,
                                                              芯片内部地址指针在每次接收后自动加 1 */
    }

    MySPI_Stop();                                       /* 拉高 SS,产生停止信号,结束读取操作 */
}

5. 本章节实验

5.1 硬件 SPI 读写 W25Q64

5.1.1 实验目标

  • 掌握硬件 SPI 外设配置: 理解如何使用 STM32 标准库配置 SPI 控制器(SPI1)的引脚复用模式、工作角色、预分频系数及数据帧格式。

  • 理解全双工数据交换机制: 掌握利用发送缓冲空标志位(TXE)和接收缓冲非空标志位(RXNE)进行底层数据收发的硬件状态机逻辑。

  • 验证驱动分层与解耦设计: 通过保持与软件模拟 SPI 相同的数据交换接口函数签名,验证底层通信机制由软件替换为硬件对上层 W25Q64 协议驱动的透明性。

  • 验证 Flash 存储器的非易失特性: 通过写入测试数组并在掉电重启后进行数据回读,验证 Flash 的物理擦写操作及数据持久化能力。

5.1.2 硬件设计

5.1.3 软件设计

本实验采用驱动分层设计的软件架构,将 STM32 底层硬件外设控制与 W25Q64 应用层协议逻辑解耦,具体流程如下:

(1)底层 SPI 硬件驱动模块(基于 MySPI.c)

  • 引脚复用与初始化: 配置 PA5(SCK)与 PA7(MOSI)为复用推挽输出模式(GPIO_Mode_AF_PP),将引脚的电平跳变控制权交由 SPI 外设;配置 PA6(MISO)为上拉输入模式(GPIO_Mode_IPU);配置 PA4 为通用推挽输出,用于通过软件独立控制 NSS 片选信号。

  • 外设核心参数配置: 实例化 SPI_InitTypeDef 结构体,配置 SPI1 为主机模式、双线全双工、8 位数据宽度、高位先行。设定波特率预分频系数为 128(即通信时钟为 = 562.5 kHz),并结合 SPI_CPOL_Low 与 SPI_CPHA_1Edge 锁定 SPI 模式 0 通信规范。

  • 硬件字节交换逻辑: 重写 MySPI_SwapByte 函数。利用 SPI_I2S_GetFlagStatus 轮询 TXE 与 RXNE 标志位,结合 SPI_I2S_SendData 与 SPI_I2S_ReceiveData 库函数,利用外设内部的移位寄存器自动完成串行时序生成与数据的同步置换。

(2)W25Q64 协议驱动模块(基于 W25Q64.c 与 W25Q64_Ins.h)

  • 接口无缝继承: 该模块无需修改基础业务代码,直接调用底层的 MySPI_Start、MySPI_SwapByte 与 MySPI_Stop 接口,完成指令码、24位物理地址与数据流的拼接发送。

  • 状态同步封装: 继承 W25Q64_WaitBusy 的轮询检测机制,通过持续读取状态寄存器 1 的最低位,确保在上一次物理擦除或页编程操作结束前,主控不发起新的存储阵列修改指令。

  • 操作接口封装: 提供 W25Q64_SectorErase(扇区擦除)、W25Q64_PageProgram(页编程)与 W25Q64_ReadData(连续读取)等高级应用接口,并在擦除和写入函数内部自动前置执行写使能(Write Enable)。

(3)应用层读写测试逻辑(基于 main.c)

  • 设备身份校验: 主程序上电后首先调用 W25Q64_ReadID 获取 Flash 的厂商 ID(MID)与设备 ID(DID),并在 OLED 屏幕显示验证 SPI 物理链路的连通状态。

  • 擦写规范测试: 严格遵循 Flash 必须先擦除后写入的物理特性,先调用 W25Q64_SectorErase 对起始地址 0x000000 所在的扇区执行擦除,随后调用 W25Q64_PageProgram 将由四个独立字节(0x01, 0x02, 0x03, 0x04)组成的测试数组写入该物理地址。

  • 数据回读验证: 调用 W25Q64_ReadData 从 0x000000 地址开始连续读取 4 个字节至接收数组,并将写入数组与读取数组的数据同步输出至 OLED 屏幕进行比对。

具体代码如下:

main.c文件:

cpp 复制代码
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "W25Q64.h"

uint8_t MID;							//定义用于存放MID号的变量
uint16_t DID;							//定义用于存放DID号的变量

uint8_t ArrayWrite[] = {0x01, 0x02, 0x03, 0x04};	//定义要写入数据的测试数组
uint8_t ArrayRead[4];								//定义要读取数据的测试数组

int main(void)
{
	/*模块初始化*/
	OLED_Init();						//OLED初始化
	W25Q64_Init();						//W25Q64初始化
	
	/*显示静态字符串*/
	OLED_ShowString(1, 1, "MID:   DID:");
	OLED_ShowString(2, 1, "W:");
	OLED_ShowString(3, 1, "R:");
	
	/*显示ID号*/
	W25Q64_ReadID(&MID, &DID);			//获取W25Q64的ID号
	OLED_ShowHexNum(1, 5, MID, 2);		//显示MID
	OLED_ShowHexNum(1, 12, DID, 4);		//显示DID
	
	/*W25Q64功能函数测试*/
	W25Q64_SectorErase(0x000000);					//扇区擦除
	W25Q64_PageProgram(0x000000, ArrayWrite, 4);	//将写入数据的测试数组写入到W25Q64中
	
	W25Q64_ReadData(0x000000, ArrayRead, 4);		//读取刚写入的测试数据到读取数据的测试数组中
	
	/*显示数据*/
	OLED_ShowHexNum(2, 3, ArrayWrite[0], 2);		//显示写入数据的测试数组
	OLED_ShowHexNum(2, 6, ArrayWrite[1], 2);
	OLED_ShowHexNum(2, 9, ArrayWrite[2], 2);
	OLED_ShowHexNum(2, 12, ArrayWrite[3], 2);
	
	OLED_ShowHexNum(3, 3, ArrayRead[0], 2);			//显示读取数据的测试数组
	OLED_ShowHexNum(3, 6, ArrayRead[1], 2);
	OLED_ShowHexNum(3, 9, ArrayRead[2], 2);
	OLED_ShowHexNum(3, 12, ArrayRead[3], 2);
	
	while (1)
	{
		
	}
}

MySPI.c文件:

cpp 复制代码
#include "stm32f10x.h"                  // Device header

/**
  * 函    数:SPI写SS引脚电平,SS仍由软件模拟
  * 参    数:BitValue 协议层传入的当前需要写入SS的电平,范围0~1
  * 返 回 值:无
  * 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SS为低电平,当BitValue为1时,需要置SS为高电平
  */
void MySPI_W_SS(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue);		//根据BitValue,设置SS引脚的电平
}

/**
  * 函    数:SPI初始化
  * 参    数:无
  * 返 回 值:无
  */
void MySPI_Init(void)
{
	/*开启时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);	//开启GPIOA的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);	//开启SPI1的时钟
	
	/*GPIO初始化*/
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);					//将PA4引脚初始化为推挽输出
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);					//将PA5和PA7引脚初始化为复用推挽输出
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);					//将PA6引脚初始化为上拉输入
	
	/*SPI初始化*/
	SPI_InitTypeDef SPI_InitStructure;						//定义结构体变量
	SPI_InitStructure.SPI_Mode = SPI_Mode_Master;			//模式,选择为SPI主模式
	SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;	//方向,选择2线全双工
	SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;		//数据宽度,选择为8位
	SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;		//先行位,选择高位先行
	SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_128;	//波特率分频,选择128分频
	SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low;				//SPI极性,选择低极性
	SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;			//SPI相位,选择第一个时钟边沿采样,极性和相位决定选择SPI模式0
	SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;				//NSS,选择由软件控制
	SPI_InitStructure.SPI_CRCPolynomial = 7;				//CRC多项式,暂时用不到,给默认值7
	SPI_Init(SPI1, &SPI_InitStructure);						//将结构体变量交给SPI_Init,配置SPI1
	
	/*SPI使能*/
	SPI_Cmd(SPI1, ENABLE);									//使能SPI1,开始运行
	
	/*设置默认电平*/
	MySPI_W_SS(1);											//SS默认高电平
}

/**
  * 函    数:SPI起始
  * 参    数:无
  * 返 回 值:无
  */
void MySPI_Start(void)
{
	MySPI_W_SS(0);				//拉低SS,开始时序
}

/**
  * 函    数:SPI终止
  * 参    数:无
  * 返 回 值:无
  */
void MySPI_Stop(void)
{
	MySPI_W_SS(1);				//拉高SS,终止时序
}

/**
  * 函    数:SPI交换传输一个字节,使用SPI模式0
  * 参    数:ByteSend 要发送的一个字节
  * 返 回 值:接收的一个字节
  */
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{
	while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) != SET);	//等待发送数据寄存器空
	
	SPI_I2S_SendData(SPI1, ByteSend);								//写入数据到发送数据寄存器,开始产生时序
	
	while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) != SET);	//等待接收数据寄存器非空
	
	return SPI_I2S_ReceiveData(SPI1);								//读取接收到的数据并返回
}

W25Q64.c文件:

cpp 复制代码
#include "stm32f10x.h"                  // Device header
#include "MySPI.h"
#include "W25Q64_Ins.h"

/**
  * 函    数:W25Q64初始化
  * 参    数:无
  * 返 回 值:无
  */
void W25Q64_Init(void)
{
	MySPI_Init();					//先初始化底层的SPI
}

/**
  * 函    数:W25Q64读取ID号
  * 参    数:MID 工厂ID,使用输出参数的形式返回
  * 参    数:DID 设备ID,使用输出参数的形式返回
  * 返 回 值:无
  */
void W25Q64_ReadID(uint8_t *MID, uint16_t *DID)
{
	MySPI_Start();								//SPI起始
	MySPI_SwapByte(W25Q64_JEDEC_ID);			//交换发送读取ID的指令
	*MID = MySPI_SwapByte(W25Q64_DUMMY_BYTE);	//交换接收MID,通过输出参数返回
	*DID = MySPI_SwapByte(W25Q64_DUMMY_BYTE);	//交换接收DID高8位
	*DID <<= 8;									//高8位移到高位
	*DID |= MySPI_SwapByte(W25Q64_DUMMY_BYTE);	//或上交换接收DID的低8位,通过输出参数返回
	MySPI_Stop();								//SPI终止
}

/**
  * 函    数:W25Q64写使能
  * 参    数:无
  * 返 回 值:无
  */
void W25Q64_WriteEnable(void)
{
	MySPI_Start();								//SPI起始
	MySPI_SwapByte(W25Q64_WRITE_ENABLE);		//交换发送写使能的指令
	MySPI_Stop();								//SPI终止
}

/**
  * 函    数:W25Q64等待忙
  * 参    数:无
  * 返 回 值:无
  */
void W25Q64_WaitBusy(void)
{
	uint32_t Timeout;
	MySPI_Start();								//SPI起始
	MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1);				//交换发送读状态寄存器1的指令
	Timeout = 100000;							//给定超时计数时间
	while ((MySPI_SwapByte(W25Q64_DUMMY_BYTE) & 0x01) == 0x01)	//循环等待忙标志位
	{
		Timeout --;								//等待时,计数值自减
		if (Timeout == 0)						//自减到0后,等待超时
		{
			/*超时的错误处理代码,可以添加到此处*/
			break;								//跳出等待,不等了
		}
	}
	MySPI_Stop();								//SPI终止
}

/**
  * 函    数:W25Q64页编程
  * 参    数:Address 页编程的起始地址,范围:0x000000~0x7FFFFF
  * 参    数:DataArray	用于写入数据的数组
  * 参    数:Count 要写入数据的数量,范围:0~256
  * 返 回 值:无
  * 注意事项:写入的地址范围不能跨页
  */
void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count)
{
	uint16_t i;
	
	W25Q64_WriteEnable();						//写使能
	
	MySPI_Start();								//SPI起始
	MySPI_SwapByte(W25Q64_PAGE_PROGRAM);		//交换发送页编程的指令
	MySPI_SwapByte(Address >> 16);				//交换发送地址23~16位
	MySPI_SwapByte(Address >> 8);				//交换发送地址15~8位
	MySPI_SwapByte(Address);					//交换发送地址7~0位
	for (i = 0; i < Count; i ++)				//循环Count次
	{
		MySPI_SwapByte(DataArray[i]);			//依次在起始地址后写入数据
	}
	MySPI_Stop();								//SPI终止
	
	W25Q64_WaitBusy();							//等待忙
}

/**
  * 函    数:W25Q64扇区擦除(4KB)
  * 参    数:Address 指定扇区的地址,范围:0x000000~0x7FFFFF
  * 返 回 值:无
  */
void W25Q64_SectorErase(uint32_t Address)
{
	W25Q64_WriteEnable();						//写使能
	
	MySPI_Start();								//SPI起始
	MySPI_SwapByte(W25Q64_SECTOR_ERASE_4KB);	//交换发送扇区擦除的指令
	MySPI_SwapByte(Address >> 16);				//交换发送地址23~16位
	MySPI_SwapByte(Address >> 8);				//交换发送地址15~8位
	MySPI_SwapByte(Address);					//交换发送地址7~0位
	MySPI_Stop();								//SPI终止
	
	W25Q64_WaitBusy();							//等待忙
}

/**
  * 函    数:W25Q64读取数据
  * 参    数:Address 读取数据的起始地址,范围:0x000000~0x7FFFFF
  * 参    数:DataArray 用于接收读取数据的数组,通过输出参数返回
  * 参    数:Count 要读取数据的数量,范围:0~0x800000
  * 返 回 值:无
  */
void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count)
{
	uint32_t i;
	MySPI_Start();								//SPI起始
	MySPI_SwapByte(W25Q64_READ_DATA);			//交换发送读取数据的指令
	MySPI_SwapByte(Address >> 16);				//交换发送地址23~16位
	MySPI_SwapByte(Address >> 8);				//交换发送地址15~8位
	MySPI_SwapByte(Address);					//交换发送地址7~0位
	for (i = 0; i < Count; i ++)				//循环Count次
	{
		DataArray[i] = MySPI_SwapByte(W25Q64_DUMMY_BYTE);	//依次在起始地址后读取数据
	}
	MySPI_Stop();								//SPI终止
}

W25Q64_Ins.h文件:

cpp 复制代码
#ifndef __W25Q64_INS_H
#define __W25Q64_INS_H

#define W25Q64_WRITE_ENABLE							0x06
#define W25Q64_WRITE_DISABLE						0x04
#define W25Q64_READ_STATUS_REGISTER_1				0x05
#define W25Q64_READ_STATUS_REGISTER_2				0x35
#define W25Q64_WRITE_STATUS_REGISTER				0x01
#define W25Q64_PAGE_PROGRAM							0x02
#define W25Q64_QUAD_PAGE_PROGRAM					0x32
#define W25Q64_BLOCK_ERASE_64KB						0xD8
#define W25Q64_BLOCK_ERASE_32KB						0x52
#define W25Q64_SECTOR_ERASE_4KB						0x20
#define W25Q64_CHIP_ERASE							0xC7
#define W25Q64_ERASE_SUSPEND						0x75
#define W25Q64_ERASE_RESUME							0x7A
#define W25Q64_POWER_DOWN							0xB9
#define W25Q64_HIGH_PERFORMANCE_MODE				0xA3
#define W25Q64_CONTINUOUS_READ_MODE_RESET			0xFF
#define W25Q64_RELEASE_POWER_DOWN_HPM_DEVICE_ID		0xAB
#define W25Q64_MANUFACTURER_DEVICE_ID				0x90
#define W25Q64_READ_UNIQUE_ID						0x4B
#define W25Q64_JEDEC_ID								0x9F
#define W25Q64_READ_DATA							0x03
#define W25Q64_FAST_READ							0x0B
#define W25Q64_FAST_READ_DUAL_OUTPUT				0x3B
#define W25Q64_FAST_READ_DUAL_IO					0xBB
#define W25Q64_FAST_READ_QUAD_OUTPUT				0x6B
#define W25Q64_FAST_READ_QUAD_IO					0xEB
#define W25Q64_OCTAL_WORD_READ_QUAD_IO				0xE3

#define W25Q64_DUMMY_BYTE							0xFF

#endif

5.1.4 实验现象

程序下载并运行后,OLED 屏幕将呈现如下状态:

  • 通信链路验证: 屏幕第一行显示读取到的 MID(0xEF)和 DID(0x4017),表明硬件 SPI 通信链路建立成功,W25Q64 芯片响应指令正常。

  • 写入过程显示: 屏幕第二行显示预设的待写入数组内容(即 0x01, 0x02, 0x03, 0x04)。

  • 读取结果显示: 屏幕第三行显示从 W25Q64 内部对应地址回读的数据。在底层驱动逻辑与时序完全正确的情况下,第三行的回读数值与第二行的写入数值完全一致。

  • 掉电保持验证: 若在主程序代码中将扇区擦除与页编程的函数调用注释掉,重新编译下载或对开发板手动断电复位,屏幕第三行仍能正确显示之前存入的数据(0x01, 0x02, 0x03, 0x04),这直观验证了 Flash 存储器数据掉电不丢失的非易失特性。

相关推荐
sulikey2 小时前
个人Linux操作系统学习笔记1 - Linux权限与工具
linux·笔记·学习
艾莉丝努力练剑3 小时前
【Linux网络】计算机网络入门:Socket编程预备,从字节序共识到 Socket 地址结构的“伪多态”设计
linux·服务器·网络·c++·学习·计算机网络
是烟花哈10 小时前
【前端】React框架学习
前端·学习·react.js
[J] 一坚10 小时前
嵌入式高手C
c语言·开发语言·stm32·单片机·mcu·51单片机·iot
檀越剑指大厂10 小时前
32 万星的面试学习计划 + 内网穿透工具,程序员面试准备效率翻倍!
学习·面试·职场和发展
FreakStudio10 小时前
和做工厂系统的印尼老哥,复刻了一套属于 MicroPython 的包管理系统
python·单片机·嵌入式·大学生·面向对象·并行计算·电子diy·电子计算机
Oll Correct11 小时前
实验二十一:验证OSPF可以划分区域
网络·笔记
HIZYUAN12 小时前
AG32 MCU Reference Manual(202401008修订版)使用手册
单片机·嵌入式硬件
YangYang9YangYan12 小时前
2026年工作后学习数据分析的价值与路径
学习·数据挖掘·数据分析