目录

一、SPI简介

二、SPI协议层

 2.1  起始条件 

2.2  终止条件

2.3  模式0

2.4  模式1

2.5  模式2

2.6  模式3

三、SPI外设

3.1  通讯引脚

3.2  时钟控制逻辑

3.3  数据控制逻辑

3.4  整体控制逻辑

3.5  通讯过程

四、SPI相关库函数

4.1  初始化结构体

4.1.1  SPI_Direction

4.1.2  SPI_Mode

4.1.3  SPI_DataSize

4.1.4  SPI_CPOL

4.1.5  SPI_CPHA

4.1.6  SPI_NSS

4.1.7  SPI_BaudRatePrescaler

4.1.8  SPI_FirstBit

4.1.9  SPI_CRCPolynomial

4.2  控制和状态管理函数

4.3  中断和DMA控制函数

​4.4  数据收发函

4.5  标志位和中断处理函数

五、SPI代码编写

5.1  引脚初始化

5.2  工作模式配置

5.3  起始条件

5.4  终止条件

5.5  发送并接收一个字节

六、W25Q64

6.1  简介

6.2  引脚定义

6.3  设计框图

6.3.1  控制逻辑

6.3.2  存储逻辑

6.4  指令集

6.4.1  写使能指令(06h)

6.4.2  写禁止指令(04h)

6.4.3  读状态寄存器指令(05h)

6.4.4  写状态寄存器指令(01h)

6.4.5  读数据指令(03h)

七、W25Q64代码编写

7.1  写使能

7.2  读标准ID

7.3  读状态寄存器

7.4  整片擦除

7.5  扇区擦除

7.6  读数据

7.7  写一页数据

7.8  连续写数据

7.9  主函数


一、SPI简介

        SPI(Serial Peripheral Interface)是由Motorola公司开发的一种通用数据总线。同步,全双工。支持总线挂载多设备(一主多从)。

四根通信线:

  • SCK(Serial Clock)串行时钟线;
  • MOSI(Master Output Slave Input)主机输出从机输入;
  • MISO(Master Input Slave Output)主机输入从机输出;
  • SS(Slave Select)从机选择(若是有多个从机,有几个从机就有几条SS线,可见硬件电路中的连接图)。

  • 所有SPI设备的SCK、MOSI、MISO分别连在一起;
  • 主机另外引出多条SS控制线,分别接到各从机的SS引脚;
  • 输出引脚配置为推挽输出,输入引脚配置为浮空或上拉输入。

        每个从设备都有独立的这一条SS信号线,本信号线独占主机的一个引脚,即有多少个从设备,就有多少条片选信号线。12C协议中通过设备地址来寻址、选中总线上的某个设备并与其进行通讯:而SPI协议中没有设备地址,它使用SS信号线来寻址,当主机要选择从设备时,把该从设备的SS信号线设置为低电平,该从设备即被选中,即片选有效,接着主机开始与被选中的从设备进行SPI通讯。所以SPI通讯以SS线置低电平为开始信号,以SS线被拉高作为结束信号。

二、SPI协议层

        基本时序图,这里之列举出了一种,对于①和⑥好理解就是开始和结束,但是对于②③④⑤的触发和采样起始SPI没有硬性的要求必须是上升沿触发还是下降沿触发,或者是上升沿采样还是下降沿采样,这个需要我们自己进行规定,其中需要引出两个概念:

时钟极性(CPOL):是指SPI设备处于空闲状态时,SCK信号线的电平信号(即SPI通讯开始前,NSS线为高电平的SCK的状态)。CPOL=0时,SCK在空闲状态为低电平;COPL=1时,SCK在空闲状态为高电平。

时钟相位(CPHA):是指数据的采样的时刻,当CPHA=0时,MOSI或者MISO数据线上的信号将会在SCK时钟线的“奇数边沿”被采样;当CPHA=1时,数据线将会在SCK的“偶数边沿”采样。

         由于CPOL有0和1两种状态,CPHA有0和1两种状态,那么我们组合一下就是SPI的四种工作模式:

SPI模式CPOLCPHA空闲时SCK时钟采样时刻
模式000低电平奇数边沿
模式101低电平偶数边沿
模式210高电平奇数边沿
模式311高电平偶数边沿

 2.1  起始条件 

        对应的序号①,SS从高电平切换到低电平:

2.2  终止条件

        对应序号⑥,SS从低电平切换到高电平:

2.3  模式0

        CPOL=0:空闲状态时,SCK为低电平

        CPHA=0:SCK第一个边沿移入数据,第二个边沿移出数据

2.4  模式1

        CPOL=0:空闲状态时,SCK为低电平

        CPHA=1:SCK第一个边沿移出数据,第二个边沿移入数据

2.5  模式2

        CPOL=1:空闲状态时,SCK为高电平

        CPHA=0:SCK第一个边沿移入数据,第二个边沿移出数据

2.6  模式3

        CPOL=1:空闲状态时,SCK为高电平

        CPHA=1:SCK第一个边沿移出数据,第二个边沿移入数据

三、SPI外设

         STM32的SPI外设可用作通讯的主机及从机,支持最高的SCK时钟频率为fpaa/2 (STM32F10x型号的芯片默认fpclkt为72MHz,fpclk2为36MHz),完全支持SPI协议的4种模式,数据帧长度可设置为8位或16位,可设置数据MSB先行或LSB先行。它还支持双线全双工、双线单向以及单线模式。

3.1  通讯引脚

        STM32芯片有多个SPI外设,它们的SPI通讯信号引出到不同的GPIO引脚上,使用时必须配置到这些指定的引脚:

        对于具体芯片引脚所在位置根据所使用的芯片手册进行查看(这里以ZET6为例):

引脚SPI编号功能
SPI1SPI2SPI3
NSSPA4PB12PA15下载口的TDI从设备选择。这是一个可选的引脚,用来选择主/从设备。它的功能是用来作为“片选引脚”,让主设备可以单独地与特定从设备通讯,避免数据线上的冲突。从设备的NSS引脚可以由主设备的一个标准I/O引脚来驱动。一旦被使能(SSOE位),NSS引脚也可以作为输出引脚,并在SPI处于主模式时拉低;此时,所有的SPI设备,如果它们的NSS引脚连接到主设备的NSS引脚,则会检测到低电平,如果它们被设置为NSS硬件模式,就会自动进入从设备状态。当配置为主设备、NSS配置为输入引脚(MSTR=1,SSOE=0)时,如果NSS被拉低,则这个SPI设备进入主模式失败状态:即MSTR位被自动清除,此设备进入从模式
CLKPA5PB13PB3下载口的TDO串口时钟,作为主设备的输出,从设备的输入
MISOPA6PB14PB4下载口的NTRST主设备输入/从设备输出引脚。该引脚在从模式下发送数据,在主模式下接收数据。
MOSIPA7PB15PB5主设备输出/从设备输入引脚。该引脚在主模式下发送数据,在从模式下接收数据。

        其中SPI1是APB2上的设备,最高通信速率达36Mbtis/s,SPI2、SPI3是APB1上的设备,最高通信速率为18Mbits/s。除了通讯速率,在其它功能上没有差异:

3.2  时钟控制逻辑

        SCK线的时钟信号,由波特率发生器根据“控制寄存器CR1”中的BR[0:2]位控制,该位是对fpclk时钟的分频因子,对fpclk的分频结果就是SCK引脚的输出时钟频率(图在数据手册23.5.1):

        举个例子,对于SPI1来说,其挂载在APB2总线上,其时钟频率为72MHz,假如我们将DR位写为000,那么SCK时钟信号就会72MHz/2=36MHz,我们可以通过BR位来动态调整时钟频率。

3.3  数据控制逻辑

        SPI的MOSI及MISO都连接到数据移位寄存器上,数据移位寄存器的数据来源来源于接收缓冲区及发送缓冲区:

  • 通过写SPI的“数据寄存器DR”把数据填充到发送缓冲区中。
  • 通过读“数据寄存器DR”,可以获取接收缓冲区中的内容。

3.4  整体控制逻辑

        整体控制逻辑负责协调整个SPI外设,控制逻辑的工作模式根据“控制寄存器(CR1/CR2)”的参数而改变,基本的控制参数包括前面提到的SPI模式、波特率、LSB先行、主从模式、单双向模式等等,具体可以通过数据手册进行查看:

3.5  通讯过程

        简单来说想要发送数据,首先将数据放到发送缓冲区,然后发送;想要接收数据,将数据放到接收缓冲区,然后接收:

四、SPI相关库函数

         这里只是介绍一些常用的,更多函数可以产看固件库手册:

链接: https://pan.baidu.com/s/16L6NxIPSKreqdnLR5kH0Ow?pwd=txjy

提取码: txjy 

4.1  初始化结构体

typedef struct
{
  uint16_t SPI_Direction;           /*!< 指定SPI单向或双向数据模式。
                                         该参数可以是 @ref SPI_data_direction 的值 */
  uint16_t SPI_Mode;                /*!< 指定SPI工作模式。
                                         该参数可以是 @ref SPI_mode 的值 */
  uint16_t SPI_DataSize;            /*!< 指定SPI数据大小。
                                         该参数可以是 @ref SPI_data_size 的值 */
  uint16_t SPI_CPOL;                /*!< 指定串行时钟稳态。
                                         该参数可以是 @ref SPI_Clock_Polarity 的值 */
  uint16_t SPI_CPHA;                /*!< 指定位捕获的时钟有效边沿。
                                         该参数可以是 @ref SPI_Clock_Phase 的值 */
  uint16_t SPI_NSS;                 /*!< 指定NSS信号是由硬件(NSS引脚)管理
                                         还是通过使用SSI位的软件管理。
                                         该参数可以是 @ref SPI_Slave_Select_management 的值 */
  uint16_t SPI_BaudRatePrescaler;   /*!< 指定用于配置发送和接收SCK时钟的
                                         波特率预分频值。
                                         该参数可以是 @ref SPI_BaudRate_Prescaler 的值。
                                         @注意 通信时钟源自主时钟。从设备时钟不需要设置。*/
  uint16_t SPI_FirstBit;            /*!< 指定数据传输从最高有效位(MSB)还是最低有效位(LSB)开始。
                                         该参数可以是 @ref SPI_MSB_LSB_transmission 的值 */
  uint16_t SPI_CRCPolynomial;       /*!< 指定用于CRC计算的多项式。*/
}SPI_InitTypeDef;

4.1.1  SPI_Direction

        指定SPI的数据传输方式:

/** @defgroup SPI_data_direction
  * @{
  */
#define SPI_Direction_2Lines_FullDuplex ((uint16_t)0x0000)  /*!< 双线全双工模式 */
#define SPI_Direction_2Lines_RxOnly     ((uint16_t)0x0400)  /*!< 双线只接收模式 */
#define SPI_Direction_1Line_Rx          ((uint16_t)0x8000)  /*!< 单线只接收模式 */
#define SPI_Direction_1Line_Tx          ((uint16_t)0xC000)  /*!< 单线只发送模式 */
#define IS_SPI_DIRECTION_MODE(MODE) (((MODE) == SPI_Direction_2Lines_FullDuplex) || \
                                     ((MODE) == SPI_Direction_2Lines_RxOnly) || \
                                     ((MODE) == SPI_Direction_1Line_Rx) || \
                                     ((MODE) == SPI_Direction_1Line_Tx))  /*!< 检查SPI方向模式是否有效 */

4.1.2  SPI_Mode

        指定SPI的工作模式:

/** @defgroup SPI_mode
  * @{
  */
#define SPI_Mode_Master                 ((uint16_t)0x0104)  /*!< 主模式 */
#define SPI_Mode_Slave                  ((uint16_t)0x0000)  /*!< 从模式 */
#define IS_SPI_MODE(MODE) (((MODE) == SPI_Mode_Master) || \
                           ((MODE) == SPI_Mode_Slave))      /*!< 检查SPI主从模式是否有效 */

        本成员设置SPI工作在主机模式(SPI_Mode_Master)或从机模式(SPI_Mode_Slave  ),这两个模式的最大区别为SPI的SCK信号线的时序,SCK的时序是由通讯中的主机产生的。若被配置为从机模式,STM32的SPI外设将接受外来的SCK信号。

4.1.3  SPI_DataSize

        指定SPI数据大小:

/** @defgroup SPI_data_size
  * @{
  */
#define SPI_DataSize_16b                ((uint16_t)0x0800)  /*!< 16位数据帧 */
#define SPI_DataSize_8b                 ((uint16_t)0x0000)  /*!< 8位数据帧 */
#define IS_SPI_DATASIZE(DATASIZE) (((DATASIZE) == SPI_DataSize_16b) || \
                                   ((DATASIZE) == SPI_DataSize_8b))  /*!< 检查SPI数据大小是否有效 */

4.1.4  SPI_CPOL

        指定串行时钟稳态:

/** @defgroup SPI_Clock_Polarity
  * @{
  */
#define SPI_CPOL_Low                    ((uint16_t)0x0000)  /*!< 时钟空闲状态为低电平 */
#define SPI_CPOL_High                   ((uint16_t)0x0002)  /*!< 时钟空闲状态为高电平 */
#define IS_SPI_CPOL(CPOL) (((CPOL) == SPI_CPOL_Low) || \
                           ((CPOL) == SPI_CPOL_High))       /*!< 检查SPI时钟极性是否有效 */

4.1.5  SPI_CPHA

        指定位捕获的时钟有效边沿:

/** @defgroup SPI_Clock_Phase
  * @{
  */
#define SPI_CPHA_1Edge                  ((uint16_t)0x0000)  /*!< 在第一个时钟边沿进行数据采样 */
#define SPI_CPHA_2Edge                  ((uint16_t)0x0001)  /*!< 在第二个时钟边沿进行数据采样 */
#define IS_SPI_CPHA(CPHA) (((CPHA) == SPI_CPHA_1Edge) || \
                           ((CPHA) == SPI_CPHA_2Edge))      /*!< 检查SPI时钟相位是否有效 */

4.1.6  SPI_NSS

        指定NSS信号是由硬件(NSS引脚)管理,还是通过使用SSI位的软件管理:

/** @defgroup SPI_Slave_Select_management
  * @{
  */
#define SPI_NSS_Soft                    ((uint16_t)0x0200)  /*!< 软件NSS管理 */
#define SPI_NSS_Hard                    ((uint16_t)0x0000)  /*!< 硬件NSS管理 */
#define IS_SPI_NSS(NSS) (((NSS) == SPI_NSS_Soft) || \
                         ((NSS) == SPI_NSS_Hard))          /*!< 检查SPI从设备选择管理方式是否有效 */

4.1.7  SPI_BaudRatePrescaler

        指定用于配置发送和接收SCK时钟的波特率预分频值。通信时钟源自主时钟。从设备时钟不需要设置:

/** @defgroup SPI_BaudRate_Prescaler
  * @{
  */
#define SPI_BaudRatePrescaler_2         ((uint16_t)0x0000)  /*!< 波特率预分频值:2分频 */
#define SPI_BaudRatePrescaler_4         ((uint16_t)0x0008)  /*!< 波特率预分频值:4分频 */
#define SPI_BaudRatePrescaler_8         ((uint16_t)0x0010)  /*!< 波特率预分频值:8分频 */
#define SPI_BaudRatePrescaler_16        ((uint16_t)0x0018)  /*!< 波特率预分频值:16分频 */
#define SPI_BaudRatePrescaler_32        ((uint16_t)0x0020)  /*!< 波特率预分频值:32分频 */
#define SPI_BaudRatePrescaler_64        ((uint16_t)0x0028)  /*!< 波特率预分频值:64分频 */
#define SPI_BaudRatePrescaler_128       ((uint16_t)0x0030)  /*!< 波特率预分频值:128分频 */
#define SPI_BaudRatePrescaler_256       ((uint16_t)0x0038)  /*!< 波特率预分频值:256分频 */
#define IS_SPI_BAUDRATE_PRESCALER(PRESCALER) (((PRESCALER) == SPI_BaudRatePrescaler_2) || \
                                              ((PRESCALER) == SPI_BaudRatePrescaler_4) || \
                                              ((PRESCALER) == SPI_BaudRatePrescaler_8) || \
                                              ((PRESCALER) == SPI_BaudRatePrescaler_16) || \
                                              ((PRESCALER) == SPI_BaudRatePrescaler_32) || \
                                              ((PRESCALer) == SPI_BaudRatePrescaler_64) || \
                                              ((PRESCALER) == SPI_BaudRatePrescaler_128) || \
                                              ((PRESCALER) == SPI_BaudRatePrescaler_256))  /*!< 检查SPI波特率预分频值是否有效 */

4.1.8  SPI_FirstBit

        指定数据传输从最高有效位(MSB)还是最低有效位(LSB)开始:

/** @defgroup SPI_MSB_LSB_transmission
  * @{
  */
#define SPI_FirstBit_MSB                ((uint16_t)0x0000)  /*!< 数据传输从最高有效位(MSB)开始 */
#define SPI_FirstBit_LSB                ((uint16_t)0x0080)  /*!< 数据传输从最低有效位(LSB)开始 */
#define IS_SPI_FIRST_BIT(BIT) (((BIT) == SPI_FirstBit_MSB) || \
                               ((BIT) == SPI_FirstBit_LSB))  /*!< 检查SPI数据传输位顺序是否有效 */

4.1.9  SPI_CRCPolynomial

        指定用于CRC计算的多项式,如果不使用CRC功能,数值写0即可。如果使用举个例子:

// 设置CRC多项式(例如使用CRC-8标准多项式)
SPI_InitStructure.SPI_CRCPolynomial = 0x07;
//常用多项式:0x07 (CRC-8)、0x1021 (CRC-16)、0x4C11DB7 (CRC-32)

对于CRC不熟悉的可以参考:

CRC(循环冗余校验)·CRC校验原理及步骤解析入门教程(C语言)_crc校验 c语言-CSDN博客

4.2  控制和状态管理函数

void SPI_Cmd(SPI_TypeDef* SPIx, FunctionalState NewState);

功能:使能或禁用SPI外设

参数:NewState - ENABLE 或 DISABLE

4.3  中断和DMA控制函数

void SPI_I2S_ITConfig(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT, FunctionalState NewState);

功能:使能或禁用指定的SPI/I2S中断

参数:SPI_I2S_IT - 要使能的中断源


void SPI_I2S_DMACmd(SPI_TypeDef* SPIx, uint16_t SPI_I2S_DMAReq, FunctionalState NewState);

​功能:使能或禁用SPI/I2S的DMA请求

参数:SPI_I2S_DMAReq - 要使能的DMA请求

​4.4  数据收发函

void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data);

功能:通过SPI/I2S外设发送数据


uint16_t SPI_I2S_ReceiveData(SPI_TypeDef* SPIx);

功能:从SPI/I2S外设接收数据

返回值:接收到的数据

4.5  标志位和中断处理函数

FlagStatus SPI_I2S_GetFlagStatus(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG);
//功能:检查指定的SPI/I2S标志位是否被设置
void SPI_I2S_ClearFlag(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG);
//功能:清除SPI/I2S的标志位
ITStatus SPI_I2S_GetITStatus(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT);
//功能:检查指定的SPI/I2S中断是否发生
void SPI_I2S_ClearITPendingBit(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT);
//功能:清除SPI/I2S的中断挂起位

五、SPI代码编写

5.1  引脚初始化

        我们先找到这个表格(这里以ZET6为例):

引脚SPI编号功能
SPI1SPI2SPI3
NSSPA4PB12PA15下载口的TDI从设备选择。这是一个可选的引脚,用来选择主/从设备。它的功能是用来作为“片选引脚”,让主设备可以单独地与特定从设备通讯,避免数据线上的冲突。从设备的NSS引脚可以由主设备的一个标准I/O引脚来驱动。一旦被使能(SSOE位),NSS引脚也可以作为输出引脚,并在SPI处于主模式时拉低;此时,所有的SPI设备,如果它们的NSS引脚连接到主设备的NSS引脚,则会检测到低电平,如果它们被设置为NSS硬件模式,就会自动进入从设备状态。当配置为主设备、NSS配置为输入引脚(MSTR=1,SSOE=0)时,如果NSS被拉低,则这个SPI设备进入主模式失败状态:即MSTR位被自动清除,此设备进入从模式
CLKPA5PB13PB3下载口的TDO串口时钟,作为主设备的输出,从设备的输入
MISOPA6PB14PB4下载口的NTRST主设备输入/从设备输出引脚。该引脚在从模式下发送数据,在主模式下接收数据。
MOSIPA7PB15PB5主设备输出/从设备输入引脚。该引脚在主模式下发送数据,在从模式下接收数据。

        这里我使用SPI1的引脚,为了代码的维护下,我们对其进行宏定义操作:

#define SPIx                         SPI1
#define SPI_APBxClock_FUN            RCC_APB2PeriphClockCmd
#define SPI_CLK                      RCC_APB2Periph_SPI1
#define SPI_GPIO_APBxClock_FUN       RCC_APB2PeriphClockCmd
#define SPI_SCK_PORT                 GPIOA
#define SPI_SCK_PIN                  GPIO_Pin_5
#define SPI_MOSI_PORT                GPIOA
#define SPI_MOSI_PIN                 GPIO_Pin_7
#define SPI_MISO_PORT                GPIOA
#define SPI_MISO_PIN                 GPIO_Pin_6
#define SPI_GPIO_CLK                 RCC_APB2Periph_GPIOA
#define SPI_CS_PORT                  GPIOA
#define SPI_CS_PIN                   GPIO_Pin_4
//CS引脚配置
#define SPI_CS_HIGH  		         GPIO_SetBits(SPI_CS_PORT,SPI_CS_PIN);
#define SPI_CS_LOW		             GPIO_ResetBits(SPI_CS_PORT,SPI_CS_PIN);

        对引脚进行初始化函数:

static void SPI_GPIO_Config(void)
{
  GPIO_InitTypeDef  GPIO_InitStructure;
	/* 使能与SPI 有关的时钟 */
	SPI_APBxClock_FUN ( SPI_CLK, ENABLE );
	SPI_GPIO_APBxClock_FUN ( SPI_GPIO_CLK, ENABLE );
  /* MISO MOSI SCK*/
  GPIO_InitStructure.GPIO_Pin = SPI_SCK_PIN;
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
  GPIO_Init(SPI_SCK_PORT, &GPIO_InitStructure);
	GPIO_InitStructure.GPIO_Pin = SPI_MOSI_PIN;
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
  GPIO_Init(SPI_MOSI_PORT, &GPIO_InitStructure);
	GPIO_InitStructure.GPIO_Pin = SPI_MISO_PIN;
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
  GPIO_Init(SPI_MISO_PORT, &GPIO_InitStructure);
	//初始化CS引脚,使用软件控制,所以直接设置成推挽输出
	GPIO_InitStructure.GPIO_Pin = SPI_CS_PIN;
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
  GPIO_Init(SPI_CS_PORT, &GPIO_InitStructure);
	SPI_CS_HIGH;
}

5.2  工作模式配置

        这里我配置模式3的工作模式:

        SCK空闲时为高电平,在第二个边沿进行采样:

//工作模式配置
static void SPI_Mode_Config(void)
{
  SPI_InitTypeDef  SPI_InitStructure;
	SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_2 ;// SPI 波特率预分频器,设置SPI时钟频率 = 系统时钟/2
	//SPI 使用模式3
	SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;// SPI 时钟相位配置,选择第二个边沿进行数据采样
	SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;// SPI 时钟极性配置,选择高电平空闲
	SPI_InitStructure.SPI_CRCPolynomial = 0;//不使用CRC功能
	SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;// SPI 数据帧格式,设置为8位数据
	SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;//双线全双工
	SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;// SPI 数据传输顺序,设置为高位先行(MSB First)
	SPI_InitStructure.SPI_Mode = SPI_Mode_Master;// SPI 工作模式,设置为主设备模式
	SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;// NSS(片选)信号管理,设置为软件控制
	SPI_Init(SPIx,&SPI_InitStructure);	//写入配置到寄存器
	SPI_Cmd(SPIx,ENABLE);//使能SPI
}

5.3  起始条件

        就是将SS引脚拉低:

void SPI_Start(void)
{
	SPI_CS_LOW;//拉低SS,开始时序
}

5.4  终止条件

        就是将SS引脚拉高:

void SPI_Stop(void)
{
	SPI_CS_HIGH;//拉高SS,终止时序
}

5.5  发送并接收一个字节

        主要用到这两个函数:

​void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data);//功能:通过SPI/I2S外设发送数据
uint16_t SPI_I2S_ReceiveData(SPI_TypeDef* SPIx);//功能:从SPI/I2S外设接收数据

        为了防止程序一直卡死在等待发送或者等待接收,增加超时等待机制:

/*等待超时时间*/
#define SPIT_FLAG_TIMEOUT         ((uint32_t)0x1000)
#define SPIT_LONG_TIMEOUT         ((uint32_t)(10 * SPIT_FLAG_TIMEOUT))
static __IO uint32_t  SPITimeout = SPIT_LONG_TIMEOUT;

        完整函数:

//发送并接收一个字节
uint8_t SPI_FLASH_SendByte(uint8_t data)
{
	SPITimeout = SPIT_FLAG_TIMEOUT;
	//检查并等待至TX缓冲区为空
	while(SPI_I2S_GetFlagStatus(SPIx,SPI_I2S_FLAG_TXE) == RESET)
	{
		if((SPITimeout--) == 0)
		return SPI_TIMEOUT_UserCallback(0);
	}
	//程序执行到此处,TX缓冲区已空
	SPI_I2S_SendData (SPIx,data);
	SPITimeout = SPIT_FLAG_TIMEOUT;
	//检查并等待至RX缓冲区为非空
	while(SPI_I2S_GetFlagStatus(SPIx,SPI_I2S_FLAG_RXNE) == RESET)
	{
		if((SPITimeout--) == 0)
		return SPI_TIMEOUT_UserCallback(1);
	}
	//程序执行到此处,说明数据发送完毕,并接收到一字字节
	return SPI_I2S_ReceiveData(SPIx);
}

六、W25Q64

6.1  简介

        W25Qxx系列是一种低成本、小型化、使用简单的非易失性存储器,常应用于数据存储、字库存储、固件程序存储等场景。

存储介质:Nor Flash(闪存)

时钟频率:80MHz / 160MHz (Dual SPI) / 320MHz (Quad SPI)

芯片型号存储容量(24位地址)
W25Q404Mbit / 512KByte
W25Q808Mbit / 1MByte
W25Q1616Mbit / 2MByte
W25Q3232Mbit / 4MByte
W25Q6464Mbit / 8MByte
W25Q128128Mbit / 16MByte
W25Q256256Mbit / 32MByte

        对于上述数据怎么来的呢?我们可以参考数据手册,对于W25Q64其是 64Mbit 的容量,而1数据位是8字节,那么 64Mbit 就是 8MByte:

6.2  引脚定义

         芯片手册的引脚图如下:

引脚功能补充
VCC、GND电源(2.7~3.6V)
CS(SS)SPI片选,低电平有效

当 /CS 为高电平时,设备处于未选中状态,串行数据输出(DO 或 IO0、IO1、IO2、IO3)引脚处于高阻态。当设备未选中时,除非正在进行内部擦除、编程或状态寄存器操作,否则设备功耗将处于待机水平。

当 /CS 为低电平时,设备将被选中,功耗将增加到活动水平,并且可以向设备写入指令和从设备读取数据。

上电后,在接受新指令之前,/CS 必须从高电平变为低电平。/CS 输入在上电时必须跟踪 VCC 电源电平,如有需要,可在 /CS 上使用上拉电阻来实现这一点。

CLK(SCK)SPI时钟

串行时钟(CLK)SPI 串行时钟输入(CLK)引脚为串行输入和输出操作提供时序。

DI(MOSI)SPI主机输出从机输入W25Q64BV 支持标准 SPI、双 SPI 和四 SPI 操作。标准 SPI 指令使用单向 DI(输入)引脚,在串行时钟(CLK)输入引脚的上升沿将指令、地址或数据串行写入设备。标准 SPI 还使用单向 DO(输出)引脚在时钟(CLK)下降沿从设备读取数据或状态。双线和四线 SPI 指令使用双向 IO 引脚在时钟(CLK)上升沿向设备串行写入指令、地址或数据,并在时钟(CLK)下降沿从设备读取数据或状态。四线 SPI 指令要求状态寄存器 2 中的非易失性四线使能位(QE)被设置。当 QE=1 时,WP 引脚变为 IO2,/HOLD 引脚变为 IO3。
DO(MISO)SPI主机输入从机输出
WP写保护,低电平有效

写保护(/WP)引脚可用于防止状态寄存器被写入。与状态寄存器的块保护(SEC、TB、BP2、BP1 和 BPO)位以及状态寄存器保护(SRP)位配合使用时,可以对部分或整个存储阵列进行硬件保护。/WP 引脚为低电平有效。当状态寄存器 2 的 QE 位设置为四线 IO 时,/WP 引脚(硬件写保护)功能不可用,因为此引脚用于 IO2。

HOLD数据保持/HOLD 引脚允许在设备被选中时将其暂停。当 /HOLD 被拉低时。当 /CS 为低电平时,DO 引脚将处于高阻态,DI 和 CLK 引脚上的信号将被忽略(无关紧要)。当 /HOLD 被置为高电平时,设备操作可以恢复。当多个设备共享相同的 SPI 信号时,HOLD 功能可能很有用。/HOLD 引脚为低电平有效。当状态寄存器 2 的 QE 位设置为四线 I/O 时,HOLD 引脚功能不可用,因为此引脚用于 IO3。

        常规外围电路设计:

6.3  设计框图

        我们分上下两部分介绍,完整的设计框图:

6.3.1  控制逻辑

        先说一下控制逻辑部分:

        首先左侧框住部分和芯片引脚相连,可以参考上方电路图,然后 Write Control Logic 配合 WP 引脚进行写保护的操作,其状态会被状态寄存器记录下来:

  • BUSY位:忙位,是只读位,位于状态寄存器中的S0。当执行页编程、扇区擦除、块擦除、芯片擦除、写状态寄存器等指令时,该位将自动置 1。此时,除了读状态寄存器指令,其他指令都忽略;当页编程、扇区擦除、块擦除、芯片擦除和写状态寄存器等指令执行完毕之后,该位将自动清 0,表示芯片可以接收其他指令了。
  • WEL位:写保护位,是只读位,位于状态寄存器中的S1。执行完写使能指令后,该位将置 1。当芯片处于写保护状态下,该位为 0。
  • BP2、BP1、 BP0位:块保护位,是可读可写位,分别位于状态寄存器的S4、S3、S2,可以用 写状态寄存器指令置位 这些块保护位。在默认状态下,这些位都为 0,即 块处于 未保护状态下。可以设置块为没有保护、部分保护或者全部保护等状态。
  • TB位:底部和顶部块的保护位,是可读可写位,位于状态寄存器的 S5。该位默认为 0,表明顶部和底部块 处于未被保护状态下,可以用 写状态寄存器指令置位该位。当 SPR位为 1 或 /WP引脚 为低电平时,这些位不可以被更改。
  • 保留位:位于状态寄存器的 S6,读取状态寄存器值时,该位为 0。
  • SRP位:状态寄存器保护位,是可读可写位,位于状态寄存器的 S7。该位结合 /P引脚 可以禁止写状态寄存器功能。该位默认值为0。当SRP=0时,/WP引脚 不能控制状态寄存器的写禁止;当 SRP=1 且 /P=0时,写状态寄存器指令失效;当SRP=1 且 /P=1 时,可以执行写状态寄存器指令。

        实现掉电不丢失部分,W25Q64 的每一个存储单元(存储一个比特 0 或 1)在物理上都是一个 “浮栅MOSFET晶体管”。

  • 浮栅:这是一个被绝缘体(二氧化硅)完全包围、与外界物理隔离的导电栅极。电荷一旦被注入,就无法轻易逃逸。
  • 电荷的囚笼:你可以把浮栅想象成一个“电子陷阱”。在无外力作用下,被困在里面的电子可以稳定地保存很多年(通常超过20年)。

        状态如何定义?

  • 状态 ‘1’ (已擦除):浮栅内没有电子。此时晶体管的阈值电压较低,容易导通。
  • 状态 ‘0’ (已编程):通过高压,将电子注入到浮栅中。这些电子的存在会改变晶体管的特性,使其阈值电压变高,不易导通。

        接着页地址锁存计数器和字节地址锁存计数器。

  • 页地址锁存计数器:它锁存的是 “页”的编号。对于 8MB 的芯片,总页数为 8,388,608 / 256 = 32,768 页。所以这个计数器可以理解为一个从 0 到 32767 的计数器。这个计数器保存的是地址的 高16位(A23-A8)。它决定了当前读取操作位于 哪一页。
  • 字节地址锁存计数器:它锁存的是在一个页内部的 “字节”位置。由于一页只有 256 字节,所以这个计数器是一个 8 位计数器,范围是 0 到 255。这个计数器保存的是地址的 低8位(A7-A0)。它决定了当前读取操作从该页的 哪个字节 开始。

        简单来说,页地址锁存计数器,管理 “在哪一页”,在字节计数器溢出时递增。字节地址锁存计数器,管理 “在页内的哪个字节”,每读取一个字节就递增一次。

6.3.2  存储逻辑

         首先我们先看一些这个介绍:

        简单翻译一下,W25Q64BV 阵列由 32,768 个可编程页组成,每页 256 字节。每次最多可编程 256 字节。页可以以 16 页为一组(扇区擦除)、128 页为一组(32KB 块擦除)、256 页为一组(64KB 块擦除)或整个芯片(芯片擦除)的方式进行擦除。W25Q64BV 分别具有 2048 个可擦除扇区和 128 个可擦除块。较小的 4KB 扇区在需要数据和参数存储的应用中提供了更大的灵活性:

        每块,每扇区都有自己的地址空间:

6.4  指令集

        这些是我们想要操作W25Q64是需要的一些指令集,下面对常用的一些进行讲述:

指令值中文名称核心作用典型使用场景
0x06写使能开启 Flash 的写入 / 擦除权限(所有写 / 擦除操作前必须先执行此指令页编程、扇区擦除、块擦除、整片擦除、写状态寄存器前调用
0x04写禁用关闭 Flash 的写入 / 擦除权限(防止误操作)写 / 擦除操作完成后,主动禁用写权限,提升安全性
0x05读状态寄存器读取 Flash 的状态寄存器(核心位:BIT0 = 忙标志,BIT1 = 写使能锁存)等待擦除 / 写入完成(查 BIT0)、验证写使能是否生效(查 BIT1)
0x01写状态寄存器配置 Flash 的状态寄存器(如写保护、Quad SPI 模式等)启用 / 禁用 Flash 的硬件写保护(WP 引脚)、切换 SPI 通信模式(单 / 双 / 四线)
0x03普通读数据从指定地址读取任意长度数据(最常用的读指令)常规数据读取(速度适中,时序简单,兼容性最好)
0x0B快速读数据高速读数据(比普通读多 1 个 dummy 字节,支持更高 SPI 时钟)对读取速度有要求的场景(如固件升级、数据批量读取)
0x3B双线快速读用 2 根数据线(MISO/MOSI)同时读数据,速度翻倍高速数据读取(需硬件支持双线模式,Flash 的 DIO 引脚需接 MCU)
0x02页编程(写数据)向指定页写入最多 256 字节数据(写入前必须先擦除单页数据写入(W25Q64 每页 256 字节,地址需页对齐)
0xD864KB 块擦除擦除指定 64KB 块的所有数据(擦除后字节为 0xFF)批量数据更新(擦除效率比扇区擦除高,适合大段数据写入前的擦除)
0x204KB 扇区擦除擦除指定 4KB 扇区的所有数据(最常用的擦除指令)小批量数据更新(W25Q64 每扇区 4KB,地址需扇区对齐)
0xC7整片擦除擦除 Flash 所有数据(8M 字节)初始化 Flash、全量数据更新(慎用!W25Q64 擦除耗时约 10 秒)
0xB9掉电模式进入低功耗模式(电流从 mA 级降至 μA 级)设备休眠时降低功耗(需先执行 ReleasePowerDown 才能恢复通信)
0xAB释放掉电模式退出掉电模式,同时可读取 Flash 的 Device ID从休眠唤醒后恢复 Flash 通信、快速读取 Device ID
0xAB读 Device ID单独读取 Flash 的设备 ID(与 ReleasePowerDown 指令值相同,用途不同)快速识别 Flash 型号(需配合 dummy 字节)
0x90读厂商 + 设备 ID读取厂商 ID(0xEF = 华邦)+ 设备 ID(0x16=W25Q64)验证 Flash 型号(最常用的 ID 读取指令,兼容性最好)
0x9F读 JEDEC ID读取厂商 ID + 内存类型 + 容量 ID(0xEF 0x40 0x17=W25Q64)标准化识别 Flash(JEDEC 标准,支持不同品牌 Flash 的通用识别)

6.4.1  写使能指令(06h)

功能:将状态寄存器中的 WEL (Write Enable Latch) 位置 '1'。

前置条件:在执行页编程、擦除(扇区/块/芯片)或写状态寄存器指令前,必须先执行此指令。

时序:

  • 将 /CS 引脚拉至低电平。
  • 通过 DI 引脚发送 1 字节指令码 06h。数据在 CLK 的上升沿被锁存。
  • 将 /CS 引脚拉至高电平,完成指令。

6.4.2  写禁止指令(04h)

功能:将状态寄存器中的 WEL 位清零 ('0')。

时序:

  • 将 /CS 引脚拉至低电平。
  • 通过 DI 引脚发送 1 字节指令码 04h。
  • 将 /CS 引脚拉至高电平,完成指令。

        在写状态寄存器、页编程、擦除指令执行完成后,WEL 位会自动清零。

6.4.3  读状态寄存器指令(05h)

功能:读取状态寄存器的当前值。

时序:

  • 将 /CS 引脚拉至低电平。
  • 通过 DI 引脚发送 1 字节指令码 05h(在 CLK 上升沿锁存)。
  • 芯片随即通过 DO 引脚输出状态寄存器的值(在 CLK 下降沿输出,MSB first)。

        需要注意的是:

  • 此指令可在任何时候执行,包括芯片忙于编程或擦除操作时。
  • 通过读取 BUSY 位,可判断内部操作是否完成,从而确定芯片是否能接收下一条指令。
  • 只要 /CS 保持为低,状态寄存器数据将持续输出。指令在 /CS 拉高后结束。

6.4.4  写状态寄存器指令(01h)

功能:读取状态寄存器的当前值。

时序:

  • 将 /CS 引脚拉至低电平。
  • 通过 DI 引脚发送 1 字节指令码 05h(在 CLK 上升沿锁存)。
  • 芯片随即通过 DO 引脚输出状态寄存器的值(在 CLK 下降沿输出,MSB first)。

        需要注意的是:

  • 此指令可在任何时候执行,包括芯片忙于编程或擦除操作时。
  • 通过读取 BUSY 位,可判断内部操作是否完成,从而确定芯片是否能接收下一条指令。
  • 只要 /CS 保持为低,状态寄存器数据将持续输出。指令在 /CS 拉高后结束。

6.4.5  读数据指令(03h)

功能:从指定地址开始读取一个或多个字节的数据。

时序:

  • 将 /CS 引脚拉至低电平。
  • 通过 DI 引脚依次发送:指令码 03h、24 位地址(A23-A0)。
  • 芯片从该地址开始,在 CLK 的下降沿通过 DO 引脚输出数据(MSB first)。

        需要注意的是:

  • 地址自动递增:输出当前地址数据后,地址计数器自动加1,并连续输出下一地址的数据,形成数据流。只要时钟持续且 /CS 为低,即可连续读取整个存储器。
  • 指令在 /CS 拉高后结束。
  • 当芯片正在执行编程、擦除或写状态寄存器操作时,此指令无效。

七、W25Q64代码编写

        对于W25Q64来说,其使用主要通过指令集进行操作,我们通过SPI收发数据进行像W25Q64发送相关指令,进行想要的数据操作:

        我们将这些指令进行宏定义操作:

#define W25X_WriteEnable		      0x06 //写使能(写/擦除前必须执行)
#define W25X_WriteDisable		      0x04 //写禁用(防止误操作)
#define W25X_ReadStatusReg		      0x05 //读状态寄存器(查忙标志/写使能)
#define W25X_WriteStatusReg		      0x01 //写状态寄存器(配置写保护/通信模式)
#define W25X_ReadData			      0x03 //普通读数据(常规读取,兼容性好)
#define W25X_FastReadData		      0x0B //快速读数据(高速读取,需dummy字节)
#define W25X_FastReadDual		      0x3B //双线快速读(2线传输,速度翻倍)
#define W25X_PageProgram		      0x02 //页编程(写数据,每页最大256字节)
#define W25X_BlockErase			      0xD8 //64KB块擦除(批量擦除,效率高)
#define W25X_SectorErase		      0x20 //4KB扇区擦除(常用擦除指令)
#define W25X_ChipErase			      0xC7 //整片擦除(擦除所有数据,慎用)
#define W25X_PowerDown			      0xB9 //掉电模式(低功耗)
#define W25X_ReleasePowerDown	      0xAB //释放掉电模式(恢复通信)
#define W25X_DeviceID			      0xAB //读Device ID(仅设备编号)
#define W25X_ManufactDeviceID     	  0x90 //读厂商+设备ID(验证Flash型号)
#define W25X_JedecDeviceID		      0x9F //读JEDEC ID(标准ID,含容量信息)

7.1  写使能

        其实代码非常简单,先拉低SS引脚,开始通讯,然后发送0x06命令给W25Q64,然后在拉低引脚即可:

//向FLASH发送写使能命令
void SPI_FLASH_WriteEnable(void)
{
  SPI_Start();//SS引脚拉低,开始通讯
  SPI_FLASH_SendByte(W25X_WriteEnable);//发送写使能命令
  SPI_Stop();//SS引脚拉高,结束通讯
}

7.2  读标准ID

        同样,首先拉低SS引脚开始SPI通讯,然后使用上面写的收发数据函数进行发送0x9F的命令,读取标准ID,然后我们将读取的ID打印出来:

//读取FLASH ID
uint32_t SPI_FLASH_ReadID(void)
{
	uint32_t FLASH_ID_Group = 0;//完整ID组合
	uint32_t FLASH_ID1 = 0, FLASH_ID2 = 0, FLASH_ID3 = 0;//读取ID数据存放
	SPI_Start();//SS引脚拉低,开始通讯
	SPI_FLASH_SendByte(W25X_JedecDeviceID);//发送JEDEC指令,读取ID
  FLASH_ID1 = SPI_FLASH_SendByte(Dummy_Byte);//读取一个字节数据,读取制造商ID
  FLASH_ID2 = SPI_FLASH_SendByte(Dummy_Byte);//读取一个字节数据,读取存储器类型
  FLASH_ID3 = SPI_FLASH_SendByte(Dummy_Byte);//读取一个字节数据,读取容量
	SPI_Stop();//SS引脚拉高,结束通讯
	FLASH_ID_Group = (FLASH_ID1 << 16) | (FLASH_ID2 << 8) | FLASH_ID3;//把数据组合起来,作为函数的返回值
	return FLASH_ID_Group;
}

        主函数调用一下:

	//读取ID
	id_example = SPI_FLASH_ReadID();
	printf("id = 0x%x \r\n",id_example);

        可以发现正常打印,说明芯片正常连接上了。

7.3  读状态寄存器

        主要调用命令为0x05,读取 Flash 的状态寄存器(核心位:BIT0 = 忙标志,BIT1 = 写使能锁存),直到 “忙标志” 清零,确保 Flash 完成内部的擦除 / 写入操作后,再执行下一条指令,其代码实现,主要靠SPI是全双工,发1字节的同时必收1字节的特性,我们在发送读状态寄存器命令后,不断发送空字节0xFF,那么我们就可以通过MISO引脚接受Flash返回的状态值,直到状态寄存器的BIT0(WIP位)为0,也就是空闲状态才截止:

//等待WIP(BUSY)标志被置0,即等待到FLASH内部数据写入完毕
void SPI_FLASH_WaitForWriteEnd(void)
{
  uint8_t FLASH_Status = 0;
  SPI_Start();//SS引脚拉低,开始通讯
  SPI_FLASH_SendByte(W25X_ReadStatusReg);//发送 读状态寄存器 命令
  /* 若FLASH忙碌,则等待 */
  do
  {
    FLASH_Status = SPI_FLASH_SendByte(Dummy_Byte);//读取FLASH芯片的状态寄存器
  }
  while ((FLASH_Status & WIP_Flag) == SET);//正在写入标志
  SPI_Stop();//SS引脚拉高,结束通讯
}

        这里使用do-while循环的原因是do-while 是 “先执行一次,再判断条件”,确保至少读取一次状态寄存器(避免 Flash 刚进入忙状态就跳过循环)。

7.4  整片擦除

        要进行Flash的擦除操作,需要先向Flash发送写使能命令,这样做的主要原因是 W25X 系列 Flash 的硬件安全机制,擦除 / 写入属于 “修改数据” 的高危操作,Flash 默认禁止此类操作,必须通过 “写使能指令(0x06)” 主动解锁,否则擦除 / 写入指令会被 Flash 直接忽略。

        然后等写使能命令完成后发送整片擦除命令(0xC7):

void SPI_FLASH_BulkErase(void)
{
  SPI_FLASH_WriteEnable();//发送FLASH写使能命令
  SPI_FLASH_WaitForWriteEnd();//等待写使能命令完毕
  SPI_Start();//SS引脚拉低,开始通讯
  SPI_FLASH_SendByte(W25X_ChipErase);//发送整块擦除指令
  SPI_Stop();//SS引脚拉高,结束通讯
  SPI_FLASH_WaitForWriteEnd();//等待擦除完毕
}

7.5  扇区擦除

        其原理和整片擦除一样,只不过整片擦除是全擦除不需要考虑你要擦哪里,但是扇区擦除就需要告诉Flash你要擦哪里(一片扇区为4K大小):

void SPI_FLASH_SectorErase(u32 SectorAddr)
{
  SPI_FLASH_WriteEnable();//发送FLASH写使能命令
  SPI_FLASH_WaitForWriteEnd();//等待写使能命令完毕
  SPI_Start();//SS引脚拉低,开始通讯
  SPI_FLASH_SendByte(W25X_SectorErase);//发送扇区擦除指令,4K擦除
  SPI_FLASH_SendByte((SectorAddr & 0xFF0000) >> 16);//发送擦除扇区地址的高位
  SPI_FLASH_SendByte((SectorAddr & 0xFF00) >> 8);//发送擦除扇区地址的中位
  SPI_FLASH_SendByte(SectorAddr & 0xFF);//发送擦除扇区地址的低位
  SPI_Stop();//SS引脚拉高,结束通讯
  SPI_FLASH_WaitForWriteEnd();//等待擦除完毕
}

7.6  读数据

        向Flash发送读指令,然后告诉Flash你要读哪里,要读多长,读数据也是采用字节替换,向Flash连续发送0xFF,进行数据交换:

//读取FLASH数据
//pBuffer,存储读出数据的指针
//ReadAddr,读取地址
//NumByteToRead,读取数据长度
void SPI_FLASH_BufferRead(u8* pBuffer, u32 ReadAddr, u16 NumByteToRead)
{
  SPI_Start();//SS引脚拉低,开始通讯
  SPI_FLASH_SendByte(W25X_ReadData);//发送读指令
  SPI_FLASH_SendByte((ReadAddr & 0xFF0000) >> 16);//发送读地址高位
  SPI_FLASH_SendByte((ReadAddr& 0xFF00) >> 8);//发送读地址中位
  SPI_FLASH_SendByte(ReadAddr & 0xFF);//发送读地址低位
	/* 读取数据 */
  while (NumByteToRead--)
  {
    *pBuffer = SPI_FLASH_SendByte(Dummy_Byte);//读取一个字节
    pBuffer++;//指向下一个字节缓冲区
  }
  SPI_Stop();//SS引脚拉高,结束通讯
}

7.7  写一页数据

        和上面流程一样,发送写使能,等待写使能,然后发送写指令,发送地址,告诉Flash要写入哪里,然后发送当前要写入的数据:

void SPI_FLASH_PageWrite(u8* pBuffer, u32 WriteAddr, u16 NumByteToWrite)
{
  SPI_FLASH_WriteEnable();//发送FLASH写使能命令
  SPI_FLASH_WaitForWriteEnd();//等待写使能命令完毕
  SPI_Start();//SS引脚拉低,开始通讯
  SPI_FLASH_SendByte(W25X_PageProgram);//写页写指令
  SPI_FLASH_SendByte((WriteAddr & 0xFF0000) >> 16);//发送写地址的高位
  SPI_FLASH_SendByte((WriteAddr & 0xFF00) >> 8);//发送写地址的中位
  SPI_FLASH_SendByte(WriteAddr & 0xFF);//发送写地址的低位
	//数据长度检查,如果超出页大小打印输出错误
	//并且保持数据在256,不进行覆盖之前数据
  if(NumByteToWrite > SPI_FLASH_PerWritePageSize)
  {
     NumByteToWrite = SPI_FLASH_PerWritePageSize;
     printf("SPI_FLASH_PageWrite too large!");
  }
  /* 写入数据*/
  while (NumByteToWrite--)
  {
    SPI_FLASH_SendByte(*pBuffer);//发送当前要写入的字节数据
    pBuffer++;//指向下一字节数据
  }
  SPI_Stop();//SS引脚拉高,结束通讯
  SPI_FLASH_WaitForWriteEnd();//等待写入完毕
}

        这里我们需要知道一件事,上面介绍W25Q64的时候,其数据手册也说了,对于W25Q64来说其每次写操作最多操作256字节,超出的部分要么丢掉,要么将数据放到前面,覆盖掉之前的数据,那么我们如果既想要之前的数据,又不想后面的数据丢失要怎么办呢?我们可以进行跨页写入。

页写入必须从页起始地址开始,且单页最多写 256 字节!!!

7.8  连续写数据

        实际应用中,写入的起始地址可能不对齐数据长度可能跨页(比如从地址 0x0000FF 写 300 字节,会跨 0x000000~0x0000FF 页和 0x000100~0x0001FF 页)。那么我们要如何实现支持任意起始地址、任意长度数据的写入(哪怕数据跨多个页)呢?

        首先创建几个变量用来存放一些基础信息:

uint8_t Addr = 0;//地址在页内的偏移量
uint8_t count = 0;//当前页剩余空间
uint8_t NumOfPage = 0;//完整页数
uint8_t NumOfSingle = 0;//剩余字节数

        首先我们计算起始地址在当前页的偏移,比如:WriteAddr=0x0000FF → Addr=255(WriteAddr,写入地址),若WriteAddr是256的整数倍(如0x000100)→ Addr=0(地址对齐):

Addr = WriteAddr % SPI_FLASH_PageSize;

        然后计算当前页可用剩余空间,通过256(一页空间)减去当前页已用的空间:

count = SPI_FLASH_PageSize - Addr;

        计算完整页数,以及剩余不满一页的字节数:

  NumOfPage =  NumByteToWrite / SPI_FLASH_PageSize;//写入数据长度除以一页大小(256字节)计算出要写多少整数页
  NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize;//对一页运算求余,计算出剩余不满一页的字节数

        首先处理刚好页对齐的数据(起始地址刚好是页起始),也就是addr等于0的数据,此时有两种状态:

        一种数据长度不到一页,此时直接调用上面写一页数据的函数即可:

SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumByteToWrite);

        另一种多页,此时根据上面计算得到的完整页数,逐页写入,没写完一页数据地址后移一页,数据指针后移一页,最后在将剩余不满一页的字节写入:

while (NumOfPage--)//先把整数页都写了
{
	SPI_FLASH_PageWrite(pBuffer, WriteAddr, SPI_FLASH_PageSize);
	WriteAddr +=  SPI_FLASH_PageSize;// 地址后移1页
	pBuffer += SPI_FLASH_PageSize;// 数据指针后移1页
}
SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumOfSingle);//若有多余的不满一页的数据,把它写完

        若地址不对齐,也就是addr不等于0,不是从起始地址开始写入的,这个情况就比较复杂,我们先考虑,要写入的数据长度小于一页数据,不过就算要写入的数据小于一页数据。也可能比剩余空间大或者小。

        举个例子,当前页的起始地址为1,但是其中已经写了200个字节的数据了,还剩56个数据,这时我们在想往当前页写入数据:

① 假如要写10个数据是不是,剩下的空间可以直接写入,不需要在跨别的页写入了

② 但是如果我们要写入70个数据,是不是还需要到下一页进行写入。

③ 如果我们写入的数据大于一页,例如600,我们是不是可以将这600个数据分成56+544,前56个数据写入当前页,后544个数据可以按照从页起始位置开始写。

        我们先看情况一:当前剩余空间足够,这样我们只需要调用写一页数据的函数,告诉Flash我们要写入的数据,地址,以及长度即可:

SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumByteToWrite);

        情况二:当前剩余空间不足,且要写入的数据不够一页,我们将当前空间写满,多余数据从下一页起始位置开始写入:

temp = NumOfSingle - count;  // 剩余要写到下一页的字节数
SPI_FLASH_PageWrite(pBuffer, WriteAddr, count);// 先写满当前页
WriteAddr +=  count;  // 地址后移到下一页起始
pBuffer += count;     // 数据指针后移1字节
SPI_FLASH_PageWrite(pBuffer, WriteAddr, temp);// 写剩余字节到下一页

        情况三:要写入的数据大于一页,我们先扣除当前剩余页的空间,然后重新计算剩余的整页数和不够一页的字节数据:

// 步骤1:先扣掉当前页剩余空间(count=56),重新计算整页/剩余数
NumByteToWrite -= count;       // 600-56=544字节
NumOfPage =  NumByteToWrite / SPI_FLASH_PageSize;  // 544/256=2整页
NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize; // 544%256=32字节
// 步骤2:先写当前页剩余的56字节,让地址对齐到下一页
SPI_FLASH_PageWrite(pBuffer, WriteAddr, count);
// 步骤3:地址/指针后移,接下来按“对齐逻辑”写
WriteAddr +=  count;
pBuffer += count;
// 步骤4:循环写整页(2个256字节)
while (NumOfPage--)
{
 SPI_FLASH_PageWrite(pBuffer, WriteAddr, SPI_FLASH_PageSize);
 WriteAddr +=  SPI_FLASH_PageSize;
 pBuffer += SPI_FLASH_PageSize;
}
// 步骤5:写最后剩余的32字节
if (NumOfSingle != 0)
{
 SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumOfSingle);
}

7.9  主函数

        我们故意不对齐数据读写一下看看:

#include "stm32f10x.h"
#include "Delay.h"
#include "Bsp_LED_Gpio.h"
#include "Bsp_Usartx.h"
#include "SPI_gpio.h"
#include "W25Qx_Flash.h"
uint8_t write_buf[1000] = {0};  // 写入缓冲区
uint8_t read_buf[1000] = {0};   // 读取缓冲区
int main(void)
{
	LED_GPIO_Config();//LED引脚初始化
	USART_Config();//串口初始化
	SPI_FLASH_Init();//SPI初始化
	uint32_t id_example;
	printf("这是一个SPI读取W25Q64测试\r\n");
	//读取ID
	id_example = SPI_FLASH_ReadID();
	printf("id = 0x%x \r\n",id_example);
	uint16_t i = 0;
	printf("\r\n构造累加测试数据...\r\n");
	for(i=0; i<1000; i++)
	{
			write_buf[i] = i & 0xFF;  // 累加值(0~255循环,便于验证)
	}
	SPI_FLASH_BulkErase();//全片擦除
	printf("\r\n正在写入数据...\r\n");
	SPI_FLASH_BufferWrite(write_buf,0x000001,1000);//写入数据故意不对齐
	printf("\r\n正在读取数据...\r\n");
	SPI_FLASH_BufferRead(read_buf, 0x000000, 1010);
	for(i=0; i<1010; i++)
	{
		printf("%02X ",read_buf[i]);
	}
	while(1)
	{
	}
}

        数据能正常读写:

OTA_时光の尘的博客-CSDN博客

STM32学习笔记_时光の尘的博客-CSDN博客