一、STM32 的 SPI 特性及架构:

1、引脚简介:

(1)   (Slave Select):从设备选择信号线,常称为片选信号线,也称为 NSS、 CS,以下用 NSS 表示。

(2) SCK (Serial Clock): 时钟信号线,用于通讯数据同步。两个设备之间通讯时,通讯速率受限于低速设备。

(3) MOSI (Master Output, Slave Input): 主设备输出/从设备输入引脚。

(4) MISO(Master Input,, Slave Output): 主设备输入/从设备输出引脚。

2、STM32 的 SPI 通讯引脚:

       其中 SPI1SPI4SPI5SPI6 APB2 上的设备,最高通信速率达 45Mbtis/s, SPI2SPI3 APB1 上的设备,最高通信速率为 22.5Mbits/s。

3、通讯过程:

4、SPI 的四种模式:

       由 CPOL 及 CPHA 的不同状态, SPI 分成了四种模式,主机与从机需要工作在相同的模式下才可以正常通讯,实际中采用较多的是“模式 0”与“模式 3”。

  • CPOL=0,表示当SCLK=0时处于空闲态,所以有效状态就是SCLK处于高电平时
  • CPOL=1,表示当SCLK=1时处于空闲态,所以有效状态就是SCLK处于低电平时
  • CPHA=0,表示数据采样是在第1个边沿,数据发送在第2个边沿
  • CPHA=1,表示数据采样是在第2个边沿,数据发送在第1个边沿

5、 W25Q256内存划分情况:

W25Q256芯片:

每个芯片 0-511个块   --- 每个块64KB

每个块有 0-15个扇区 --- 每个扇区4KB

512块 X 64KB = 32768‬KB = 32M ( 32768‬KB / 1024 )

二、SPI 初始化结构体详解:

       STM32标准库提供了SPI初始化结构体初始化函数来配置SPI外设。初始化结构体及函数定义在库文件“stm32f4xx_spi.h”及“stm32f4xx_spi.c”中。

typedef struct 
{ 
 
uint16_t SPI_Direction;       /*设置 SPI 的单双向模式 */ 
uint16_t SPI_Mode;               /*设置 SPI 的主/从机端模式 */ 
uint16_t SPI_DataSize;           /*设置 SPI 的数据帧长度,可选 8/16 位 */ 
uint16_t SPI_CPOL;                /*设置时钟极性 CPOL,可选高/低电平*/ 
uint16_t SPI_CPHA;                /*设置时钟相位,可选奇/偶数边沿采样 */ 
uint16_t SPI_NSS;         /*设置 NSS 引脚由 SPI 硬件控制还是软件控制*/ 
uint16_t SPI_BaudRatePrescaler; /*设置时钟分频因子,fpclk/分频数=fSCK */ 
uint16_t SPI_FirstBit;           /*设置 MSB/LSB 先行 */ 
uint16_t SPI_CRCPolynomial;      /*设置 CRC 校验的表达式 */ 
} SPI_InitTypeDef;

(1) SPI_Direction

       本成员设置 SPI 的通讯方向,可设置为双线全双工(SPI_Direction_2Lines_FullDuplex),双线只接收(SPI_Direction_2Lines_RxOnly),单线只接收(SPI_Direction_1Line_Rx)、单线只发送模式(SPI_Direction_1Line_Tx)。

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

(3) SPI_DataSize
       本成员可以选择 SPI 通讯的数据帧大小是为 8 位(SPI_DataSize_8b)或16 位(SPI_DataSize_16b)。

(4) SPI_CPOLSPI_CPHA


       时钟极性 CPOL 成员,可设置为高电平(SPI_CPOL_High)或低电平(SPI_CPOL_Low )。
       时钟相位 CPHA 则可以设置为 SPI_CPHA_1Edge(在 SCK 的奇数边沿采集数据) 或SPI_CPHA_2Edge (在 SCK 的偶数边沿采集数据) 。

(5) SPI_NSS
       本成员配置 NSS 引脚的使用模式,可以选择为硬件模式(SPI_NSS_Hard )与软件模式(SPI_NSS_Soft ),在硬件模式中的 SPI 片选信号由 SPI 硬件自动产生,而软件模式则需要我们亲自把相应的 GPIO 端口拉高或置低产生非片选和片选信号。实际中软件模式应用比较多。

(6) SPI_BaudRatePrescaler

       本成员设置波特率分频因子,分频后的时钟即为 SPI 的 SCK 信号线的时钟频率。这个成员参数可设置为 fpclk 的 2、 4、 6、 8、 16、 32、 64、 128、 256 分频。

(7) SPI_FirstBit

       所有串行的通讯协议都会有 MSB 先行(高位数据在前)还是 LSB 先行(低位数据在前)的问题,而 STM32 的 SPI 模块可以通过这个结构体成员,对这个特性编程控制。

(8) SPI_CRCPolynomial
       这是 SPI 的 CRC 校验中的多项式,若我们使用 CRC 校验时,就使用这个成员的参数(多项式),来计算 CRC 的值。

三、硬件SPI—读写串行 FLASH 实验

       FLSAH 存储器又称闪存,它与 EEPROM 都是掉电后数据不丢失的存储器,但 FLASH存储器容量普遍大于 EEPROM,现在基本取代了它的地位。

       我们生活中常用的 U 盘、 SD卡、 SSD 固态硬盘以及我们 STM32 芯片内部用于存储程序的设备,都是 FLASH 类型的存储器。在存储控制上,最主要的区别是 FLASH 芯片只能一大片一大片地擦写,而 EEPROM 可以单个字节擦写。

       一种使用 SPI 通讯的串行 FLASH 存储芯片的读写实验,实验中 STM32 的 SPI 外设采用主模式, 通过查询事件的方式来确保正常通讯。
1、硬件设计

       本实验中的 FLASH 芯片(型号: W25Q256)是一种使用 SPI 通讯协议的 NOR FLASH存储器,它的 CS/CLK/DIO/DO 引脚分别连接到了 STM32 对应的 SDI 引脚NSS/SCK/MOSI/MISO 上,其中 STM32 的 NSS 引脚是一个普通的 GPIO,不是 SPI 的专用NSS 引脚,所以程序中我们要使用软件控制的方式。
       FLASH 芯片中还有 WP 和 HOLD 引脚。 WP 引脚可控制写保护功能,当该引脚为低电平时,禁止写入数据。我们直接接电源,不使用写保护功能。 HOLD 引脚可用于暂停通讯,该引脚为低电平时,通讯暂停,数据输出引脚输出高阻抗状态,时钟和数据输入引脚无效。
我们直接接电源,不使用通讯暂停功能。

2、软件设计

(1)编程要点

  1. 初始化通讯使用的目标引脚及端口时钟;
  2. 使能 SPI 外设的时钟;
  3. 配置 SPI 外设的模式、地址、速率等参数并使能 SPI 外设;
  4. 编写基本 SPI 按字节收发的函数;
  5. 编写对 FLASH 擦除及读写操作的的函数;
  6. 编写测试程序,对读写数据进行校验。
     

(2)代码分析
       ① SPI 硬件相关宏定义

//SPI 号及时钟初始化函数 
#define FLASH_SPI                          SPI5 
#define FLASH_SPI_CLK                      RCC_APB2Periph_SPI5 
#define FLASH_SPI_CLK_INIT                 RCC_APB2PeriphClockCmd 
//SCK 引脚 
#define FLASH_SPI_SCK_PIN                  GPIO_Pin_7 
#define FLASH_SPI_SCK_GPIO_PORT            GPIOF 
#define FLASH_SPI_SCK_GPIO_CLK             RCC_AHB1Periph_GPIOF 
#define FLASH_SPI_SCK_PINSOURCE            GPIO_PinSource7 
#define FLASH_SPI_SCK_AF                   GPIO_AF_SPI5 
//MISO 引脚 
#define FLASH_SPI_MISO_PIN                 GPIO_Pin_8 
#define FLASH_SPI_MISO_GPIO_PORT           GPIOF 
#define FLASH_SPI_MISO_GPIO_CLK            RCC_AHB1Periph_GPIOF 
#define FLASH_SPI_MISO_PINSOURCE           GPIO_PinSource8 
#define FLASH_SPI_MISO_AF                  GPIO_AF_SPI5 
//MOSI 引脚 
#define FLASH_SPI_MOSI_PIN                 GPIO_Pin_9 
#define FLASH_SPI_MOSI_GPIO_PORT           GPIOF 
#define FLASH_SPI_MOSI_GPIO_CLK            RCC_AHB1Periph_GPIOF 
#define FLASH_SPI_MOSI_PINSOURCE           GPIO_PinSource9 
#define FLASH_SPI_MOSI_AF                   GPIO_AF_SPI5 
//CS(NSS)引脚 
#define FLASH_CS_PIN                        GPIO_Pin_6 
#define FLASH_CS_GPIO_PORT                  GPIOF 
#define FLASH_CS_GPIO_CLK                   RCC_AHB1Periph_GPIOF 
 
//控制 CS(NSS)引脚输出低电平 
#define SPI_FLASH_CS_LOW()     {FLASH_CS_GPIO_PORT->BSRRH=FLASH_CS_PIN;} 
//控制 CS(NSS)引脚输出高电平 
#define SPI_FLASH_CS_HIGH()    {FLASH_CS_GPIO_PORT->BSRRL=FLASH_CS_PIN;}

      与 FLASH 通讯使用的 SPI 号 、引脚号、引脚源以及复用功能映射都以宏封装起来,并且定义了控制 CS(NSS)引脚输出电平的宏,以便配置产生起始和停止信号时使用。

       ② 初始化 SPI 的 GPIO

/**
* @brief  SPI_FLASH 初始化 
* @param 无 
* @retval 无 
*/ 
void SPI_FLASH_Init(void) 
{ 
GPIO_InitTypeDef GPIO_InitStructure; 
/* 使能 FLASH_SPI 及 GPIO 时钟 */ 
/*!< SPI_FLASH_SPI_CS_GPIO, SPI_FLASH_SPI_MOSI_GPIO, 
SPI_FLASH_SPI_MISO_GPIO 和 SPI_FLASH_SPI_SCK_GPIO 时钟使能 */ 
 
RCC_AHB1PeriphClockCmd (FLASH_SPI_SCK_GPIO_CLK | FLASH_SPI_MISO_GPIO_CLK| 
FLASH_SPI_MOSI_GPIO_CLK|FLASH_CS_GPIO_CLK, ENABLE); 
/*!< SPI_FLASH_SPI 时钟使能 */ 
FLASH_SPI_CLK_INIT(FLASH_SPI_CLK, ENABLE); 
//设置引脚复用 GPIO_PinAFConfig(FLASH_SPI_SCK_GPIO_PORT,FLASH_SPI_SCK_PINSOURCE, 
FLASH_SPI_SCK_AF); GPIO_PinAFConfig(FLASH_SPI_MISO_GPIO_PORT,FLASH_SPI_MISO_PINSOURCE, 
FLASH_SPI_MISO_AF); GPIO_PinAFConfig(FLASH_SPI_MOSI_GPIO_PORT,FLASH_SPI_MOSI_PINSOURCE, 
FLASH_SPI_MOSI_AF); 
/*!< 配置 SPI_FLASH_SPI 引脚: SCK */ 
GPIO_InitStructure.GPIO_Pin = FLASH_SPI_SCK_PIN; 
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; 
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL; 
GPIO_Init(FLASH_SPI_SCK_GPIO_PORT, &GPIO_InitStructure); 
/*!< 配置 SPI_FLASH_SPI 引脚: MISO */ 
GPIO_InitStructure.GPIO_Pin = FLASH_SPI_MISO_PIN; 
GPIO_Init(FLASH_SPI_MISO_GPIO_PORT, &GPIO_InitStructure); 
/*!< 配置 SPI_FLASH_SPI 引脚: MOSI */ 
GPIO_InitStructure.GPIO_Pin = FLASH_SPI_MOSI_PIN; 
GPIO_Init(FLASH_SPI_MOSI_GPIO_PORT, &GPIO_InitStructure); 
/*!< 配置 SPI_FLASH_SPI 引脚: CS */ 
GPIO_InitStructure.GPIO_Pin = FLASH_CS_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT; 
GPIO_Init(FLASH_CS_GPIO_PORT, &GPIO_InitStructure); 
/* 停止信号 FLASH: CS 引脚高电平*/ 
SPI_FLASH_CS_HIGH(); 
}

       向 GPIO 初始化结构体赋值,把 SCK/MOSI/MISO 引脚初始化成复用推挽模式。而CS(NSS)引脚由于使用软件控制,我们把它配置为普通的推挽输出模式

       ③ 配置 SPI 的模式

/** 
* @brief  SPI_FLASH 初始化 
* @param 无 
* @retval 无 
*/ 
void SPI_FLASH_Init(void) 
{ 
SPI_InitTypeDef SPI_InitStructure; GPIO_InitTypeDef GPIO_InitStructure; 
/* 使能 FLASH_SPI 及 GPIO 时钟 */ 
/*!< SPI_FLASH_SPI_CS_GPIO, SPI_FLASH_SPI_MOSI_GPIO, 
SPI_FLASH_SPI_MISO_GPIO,SPI_FLASH_SPI_SCK_GPIO 时钟使能 */ 
RCC_AHB1PeriphClockCmd (FLASH_SPI_SCK_GPIO_CLK |FLASH_SPI_MISO_GPIO_CLK|FLASH_SPI_MOSI_GPIO_CLK|FLASH_CS_GPIO_CLK, ENABLE); 
/*!< SPI_FLASH_SPI 时钟使能 */ 
//设置引脚复用
GPIO_PinAFConfig(FLASH_SPI_SCK_GPIO_PORT,FLASH_SPI_SCK_PINSOURCE,FLASH_SPI_SCK_AF); 
GPIO_PinAFConfig(FLASH_SPI_MISO_GPIO_PORT,FLASH_SPI_MISO_PINSOURCE,FLASH_SPI_MISO_AF); 
GPIO_PinAFConfig(FLASH_SPI_MOSI_GPIO_PORT,FLASH_SPI_MOSI_PINSOURCE,FLASH_SPI_MOSI_AF); 
 
/*!< 配置 SPI_FLASH_SPI 引脚: SCK */ 
GPIO_InitStructure.GPIO_Pin = FLASH_SPI_SCK_PIN; 
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; 
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; 
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL; 
 
GPIO_Init(FLASH_SPI_SCK_GPIO_PORT, &GPIO_InitStructure); 
 
/*!< 配置 SPI_FLASH_SPI 引脚: MISO */ 
GPIO_InitStructure.GPIO_Pin = FLASH_SPI_MISO_PIN; 
GPIO_Init(FLASH_SPI_MISO_GPIO_PORT, &GPIO_InitStructure); 
 
/*!< 配置 SPI_FLASH_SPI 引脚: MOSI */ 
GPIO_InitStructure.GPIO_Pin = FLASH_SPI_MOSI_PIN; 
GPIO_Init(FLASH_SPI_MOSI_GPIO_PORT, 
&GPIO_InitStructure); 
 
/*!< 配置 SPI_FLASH_SPI 引脚: CS */ 
GPIO_InitStructure.GPIO_Pin = FLASH_CS_PIN; 
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT; 
GPIO_Init(FLASH_CS_GPIO_PORT, &GPIO_InitStructure); 
 
/* 停止信号 FLASH: CS 引脚高电平*/ 
SPI_FLASH_CS_HIGH(); 
 
/* FLASH_SPI 模式配置 */ 
// FLASH 芯片 支持 SPI 模式 0 及模式 3,据此设置 CPOL CPHA 
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; SPI_InitStructure.SPI_Mode = 
SPI_Mode_Master; SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; SPI_InitStructure.SPI_CPOL = 
SPI_CPOL_High; SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge; 
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; 
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_2; 
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; 
SPI_InitStructure.SPI_CRCPolynomial = 7; 
SPI_Init(FLASH_SPI, &SPI_InitStructure); 
 
/* 使能 FLASH_SPI  */ 
SPI_Cmd(FLASH_SPI, ENABLE); 
 
/* 使 SPI_FLASH 进入 4 字节地址模式 */ 
SPI_FLASH_Mode_Init(); 
}

        这段代码中,把 STM32 的 SPI 外设配置为主机端双线全双工模式,数据帧长度为 8位,使用 SPI 模式 3(CPOL=1, CPHA=1), NSS 引脚由软件控制以及 MSB 先行模式。最后一个成员为 CRC 计算式,由于我们与 FLASH 芯片通讯不需要 CRC 校验,并没有使能 SPI的 CRC 功能,这时 CRC 计算式的成员值是无效的。最后配置 W25Q256 进入 4 字节地址模式,默认是 3 字节地址模式。

       ④ 使用 SPI 发送和接收一个字节的数据

/*
* @brief 使用 SPI 发送一个字节的数据 
* @param byte:要发送的数据 
* @retval 返回接收到的数据 
*/ 
u8 SPI_FLASH_SendByte(u8 byte) 
{ 
SPITimeout = SPIT_FLAG_TIMEOUT; 
/* 等待发送缓冲区为空,TXE 事件 */ 
while (SPI_I2S_GetFlagStatus(FLASH_SPI, SPI_I2S_FLAG_TXE) == RESET) 
{ 
if ((SPITimeout--) == 0) 
return SPI_TIMEOUT_UserCallback(0); 
} 
 
/* 写入数据寄存器,把要写入的数据写入发送缓冲区 */ 
SPI_I2S_SendData(FLASH_SPI, byte); 
SPITimeout = SPIT_FLAG_TIMEOUT; 
/* 等待接收缓冲区非空,RXNE 事件 */ 
while (SPI_I2S_GetFlagStatus(FLASH_SPI, SPI_I2S_FLAG_RXNE) == RESET) 
{ 
if ((SPITimeout--) == 0) 
return SPI_TIMEOUT_UserCallback(1); 
} 
/* 读取数据寄存器,获取接收缓冲区数据 */ 
return SPI_I2S_ReceiveData(FLASH_SPI); 
} 
/** 
* @brief 使用 SPI 读取一个字节的数据 
* @param 无 
* @retval 返回接收到的数据 
*/ 
u8 SPI_FLASH_ReadByte(void) 
{ 
return (SPI_FLASH_SendByte(Dummy_Byte)); 
}

       ⑤ 控制 FLASH 的指令

       ⑥ 定义 FLASH 指令编码表

        FLASH 芯片的常用指令编码使用宏来封装起来,后面需要发送指令编码的时候我们直接使用这些宏即可。

/*命令定义-开头*******************************/ 
#define W25X_WriteEnable         0x06 
#define W25X_WriteDisable        0x04 
#define W25X_ReadStatusReg       0x05 
#define W25X_WriteStatusReg    0x01 
#define W25X_ReadData            0x03 
#define W25X_FastReadData        0x0B 
#define W25X_FastReadDual        0x3B 
#define W25X_PageProgram         0x02 
#define W25X_BlockErase          0xD8 
#define W25X_SectorErase         0x20 
#define W25X_ChipErase           0xC7 
#define W25X_PowerDown           0xB9 
#define W25X_ReleasePowerDown  0xAB 
#define W25X_DeviceID            0xAB 
#define W25X_ManufactDeviceID    0x90 
#define W25X_JedecDeviceID       0x9F 
#define W25X_Enter4ByteMode      0xB7 
#define W25X_ReadStatusRegister3     0x15 
 
#define WIP_Flag                 0x01  
#define Dummy_Byte               0xFF 

       ⑦ 读取 FLASH 芯片 ID

/*
* @param 无 
* @retval FLASH ID 
*/ 
u32 SPI_FLASH_ReadID(void) 
{ 
 
u32 Temp = 0, Temp0 = 0, Temp1 = 0, Temp2 = 0; 
 
/* 开始通讯:CS 低电平 */ 
SPI_FLASH_CS_LOW(); 
 
/* 发送 JEDEC 指令,读取 ID */ 
SPI_FLASH_SendByte(W25X_JedecDeviceID); 
 
/* 读取一个字节数据 */ 
Temp0 = SPI_FLASH_SendByte(Dummy_Byte); 
 
/* 读取一个字节数据 */ 
Temp1 = SPI_FLASH_SendByte(Dummy_Byte); 
 
/* 读取一个字节数据 */ 
Temp2 = SPI_FLASH_SendByte(Dummy_Byte); 
 
/* 停止通讯:CS 高电平 */ 
SPI_FLASH_CS_HIGH(); 
 
/*把数据组合起来,作为函数的返回值*/ 
Temp = (Temp0 << 16) | (Temp1 << 8) | Temp2; 
 
return Temp; 
}

      实现了“ JEDEC ID”指令的时序:发送一个字节的指令编码“ W25X_JedecDeviceID”,然后读取 3 个字节,获取 FLASH 芯片对该指令的响应,最后把读取到的这 3 个数据合并到一个变量 Temp 中,然后作为函数返回值,把该返回值与我们定义的宏“ sFLASH_ID”对比,即可知道 FLASH 芯片是否正常

       ⑧ FLASH 写使能以及读取当前状态
       在向 FLASH 芯片存储矩阵写入数据前,首先要使能写操作,通过“ Write Enable”命令即可写使能。

写使能命令

/** 
* @brief 向 FLASH 发送 写使能 命令 
* @param none 
* @retval none 
*/ 
void SPI_FLASH_WriteEnable(void) 
{ 
/* 通讯开始:CS 低 */ 
SPI_FLASH_CS_LOW(); 
 
/* 发送写使能命令*/ 
SPI_FLASH_SendByte(W25X_WriteEnable); 
 
/*通讯结束:CS 高 */ 
SPI_FLASH_CS_HIGH(); 
}

通过读状态寄存器等待 FLASH 芯片空闲

/*WIP(BUSY)标志:FLASH 内部正在写入*/ 
#define WIP_Flag                 0x01 
 
/** 
* @brief 等待 WIP(BUSY)标志被置 0,即等待到 FLASH 内部数据写入完毕 
* @param none 
* @retval none 
*/ 
void SPI_FLASH_WaitForWriteEnd(void) 
{ 
u8 FLASH_Status = 0; 
/* 选择 FLASH: CS 低 */ 
SPI_FLASH_CS_LOW(); 
/* 发送 读状态寄存器 命令 */ 
SPI_FLASH_SendByte(W25X_ReadStatusReg); 
SPITimeout = SPIT_FLAG_TIMEOUT; 
/* 若 FLASH 忙碌,则等待 */ 
do 
{ 
/* 读取 FLASH 芯片的状态寄存器 */ 
FLASH_Status = SPI_FLASH_SendByte(Dummy_Byte); 
if ((SPITimeout--) == 0) 
{ 
SPI_TIMEOUT_UserCallback(4); 
return; 
} 
} 
while ((FLASH_Status & WIP_Flag) == SET); /* 正在写入标志 */ 
 
/* 停止信号 FLASH: CS 高 */ 
SPI_FLASH_CS_HIGH(); 
}

      发送读状态寄存器的指令编码“ W25X_ReadStatusReg”后,在 while 循环里持续获取寄存器的内容并检验它的“ WIP_Flag 标志” (即 BUSY 位),一直等待到该标志表示写入结束时才退出本函数,以便继续后面与 FLASH 芯片的数据通讯。

       ⑨ FLASH 扇区擦除

       由于 FLASH 存储器的特性决定了它只能把原来为“ 1”的数据位改写成“0”,而原来为“ 0”的数据位不能直接改写为“ 1”。所以这里涉及到数据“擦除”的概念。

       扇区擦除指令的第一个字节为指令编码,紧接着发送的 4 个字节用于表示要擦除的存储矩阵地址。要注意的是在扇区擦除指令前,还需要先发送“写使能”指令发送扇区擦除指令后,通过读取寄存器状态等待扇区擦除操作完毕。

/*
* @brief 擦除 FLASH 扇区 
* @param SectorAddr:要擦除的扇区地址 
* @retval 无 
*/ 
void SPI_FLASH_SectorErase(u32 SectorAddr) 
{ 
/* 发送 FLASH 写使能命令 */ 
SPI_FLASH_WriteEnable(); 
SPI_FLASH_WaitForWriteEnd(); 
/* 擦除扇区 */ 
/* 选择 FLASH: CS 低电平 */ 
SPI_FLASH_CS_LOW(); 
/* 发送扇区擦除指令*/ 
SPI_FLASH_SendByte(W25X_SectorErase); 
/*发送擦除扇区地址的高 8 位*/ 
SPI_FLASH_SendByte((SectorAddr & 0xFF000000) >> 24); 
/*发送擦除扇区地址的中前 8 位*/ 
SPI_FLASH_SendByte((SectorAddr & 0xFF0000) >> 16); 
/* 发送擦除扇区地址的中后 8 位 */ 
SPI_FLASH_SendByte((SectorAddr & 0xFF00) >> 8); 
/* 发送擦除扇区地址的低 8 位 */ 
SPI_FLASH_SendByte(SectorAddr & 0xFF); 
/* 停止信号 FLASH: CS 高电平 */ 
SPI_FLASH_CS_HIGH(); 
/* 等待擦除完毕*/ 
SPI_FLASH_WaitForWriteEnd(); 
}                                                                 

       ⑩ FLASH 的页写入

       FLASH 芯片也有页写入命令,使用页写入命令最多可以一次向 FLASH 传输 256 个字节的数据,我们把这个单位为页大小。

/** 
* @brief 对 FLASH 按页写入数据,调用本函数写入数据前需要先擦除扇区 
* @param pBuffer,要写入数据的指针 
* @param WriteAddr,写入地址 
* @param  NumByteToWrite,写入数据长度,必须小于等于 SPI_FLASH_PerWritePageSize 
* @retval 无 
*/ 
void SPI_FLASH_PageWrite(u8* pBuffer, u32 WriteAddr, u16 NumByteToWrite) 
{ 
/* 发送 FLASH 写使能命令 */ 
SPI_FLASH_WriteEnable(); 
/* 选择 FLASH: CS 低电平 */ 
SPI_FLASH_CS_LOW(); 
/* 写页写指令*/ 
SPI_FLASH_SendByte(W25X_PageProgram); 
/*发送写地址的高 8 位*/ 
SPI_FLASH_SendByte((WriteAddr & 0xFF000000) >> 24); 
/*发送写地址的中前 8 位*/ 
SPI_FLASH_SendByte((WriteAddr & 0xFF0000) >> 16); 
/*发送写地址的中后 8 位*/ 
SPI_FLASH_SendByte((WriteAddr & 0xFF00) >> 8); 
/*发送写地址的低 8 位*/ 
SPI_FLASH_SendByte(WriteAddr & 0xFF); 
if (NumByteToWrite > SPI_FLASH_PerWritePageSize) 
{ 
NumByteToWrite = SPI_FLASH_PerWritePageSize; 
FLASH_ERROR("SPI_FLASH_PageWrite too large!"); 
} 
/* 写入数据*/ 
while (NumByteToWrite--) 
{ 
/* 发送当前要写入的字节数据 */ 
SPI_FLASH_SendByte(*pBuffer); 
/* 指向下一字节数据 */ 
pBuffer++; 
} 
/* 停止信号 FLASH: CS 高电平 */ 
SPI_FLASH_CS_HIGH(); 
 
/* 等待写入完毕*/ 
SPI_FLASH_WaitForWriteEnd(); 
}

     先发送“写使能”命令,接着才开始页写入时序, 然后发送指令编码、地址, 再把要写入的数据一个接一个地发送出去,发送完后结束通讯,检查 FLASH状态寄存器,等待 FLASH 内部写入结束。

       ⑪  不定量数据写入

/** 
* @brief 对 FLASH 写入数据,调用本函数写入数据前需要先擦除扇区 
* @param pBuffer,要写入数据的指针 
* @param WriteAddr,写入地址 
* @param NumByteToWrite,写入数据长度 
* @retval 无 
*/ 
void SPI_FLASH_BufferWrite(u8* pBuffer, u32 WriteAddr, u16 NumByteToWrite) 
{ 
u8 NumOfPage = 0, NumOfSingle = 0, Addr = 0, count = 0, temp = 0; 
/*mod 运算求余,若 writeAddr 是 SPI_FLASH_PageSize 整数倍,运算结果 Addr 值为0*/ 
Addr = WriteAddr % SPI_FLASH_PageSize; 
NumOfPage =  NumByteToWrite / SPI_FLASH_PageSize; 
/*mod 运算求余,计算出剩余不满一页的字节数*/ 
NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize; 
/* Addr=0,则 WriteAddr 刚好按页对齐 aligned  */ 
if (Addr == 0) 
{ 
/* NumByteToWrite < SPI_FLASH_PageSize */ 
if (NumOfPage == 0) 
{ 
SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumByteToWrite); 
} 
else /* NumByteToWrite > SPI_FLASH_PageSize */ 
{ 
/*先把整数页都写了*/ 
while (NumOfPage--) 
{ 
SPI_FLASH_PageWrite(pBuffer, WriteAddr, SPI_FLASH_PageSize); 
WriteAddr += SPI_FLASH_PageSize; 
pBuffer += SPI_FLASH_PageSize; 
} 
/*若有多余的不满一页的数据,把它写完*/ 
SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumOfSingle); 
} 
} 
/* 若地址与 SPI_FLASH_PageSize 不对齐 */ 
else 
{ 
/* NumByteToWrite < SPI_FLASH_PageSize */ 
if (NumOfPage == 0) 
{ 
/*当前页剩余的 count 个位置比 NumOfSingle 小,写不完*/ 
if (NumOfSingle > count) 
{ 
temp = NumOfSingle - count; 
/*先写满当前页*/ 
SPI_FLASH_PageWrite(pBuffer, WriteAddr, count);
WriteAddr += count; 
pBuffer += count; 
/*再写剩余的数据*/ 
SPI_FLASH_PageWrite(pBuffer, WriteAddr, temp); 
} 
else /*当前页剩余的 count 个位置能写完 NumOfSingle 个数据*/ 
{ 
SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumByteToWrite); 
} 
} 
else /* NumByteToWrite > SPI_FLASH_PageSize */ 
{ 
/*地址不对齐多出的 count 分开处理,不加入这个运算*/ 
NumByteToWrite -= count; 
NumOfPage = NumByteToWrite / SPI_FLASH_PageSize; 
NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize; 
 
SPI_FLASH_PageWrite(pBuffer, WriteAddr, count); 
WriteAddr += count; 
pBuffer += count; 
/*把整数页都写了*/ 
while (NumOfPage--) 
{ 
SPI_FLASH_PageWrite(pBuffer, WriteAddr, SPI_FLASH_PageSize); 
WriteAddr += SPI_FLASH_PageSize; 
pBuffer += SPI_FLASH_PageSize; 
} 
/*若有多余的不满一页的数据,把它写完*/ 
if (NumOfSingle != 0) 
{ 
SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumOfSingle); 
} 
} 
} 
}

       页的大小以及实际数据写入的时候,使用的是针对 FLASH 芯片的页写入函数,且在实际调用这个“不定量数据写入”函数时,还要注意确保目标扇区处于擦除状态。

       ⑫ 从 FLASH 读取数据

       发送了指令编码及要读的起始地址后, FLASH 芯片就会按地址递增的方式返回存储矩阵的内容,读取的数据量没有限制,只要没有停止通讯, FLASH 芯片就会一直返回数据

/** 
* @brief 读取 FLASH 数据 
* @param  pBuffer,存储读出数据的指针 
* @param  ReadAddr,读取地址 
* @param  NumByteToRead,读取数据长度 
* @retval 无 
*/ 
void SPI_FLASH_BufferRead(u8* pBuffer, u32 ReadAddr, u16 NumByteToRead) 
{ 
/* 选择 FLASH: CS 低电平 */ 
SPI_FLASH_CS_LOW(); 
/* 发送 读 指令 */ 
SPI_FLASH_SendByte(W25X_ReadData); 
/* 发送  读  地址高 8 位  */ 
SPI_FLASH_SendByte((ReadAddr & 0xFF000000) >> 24); 
/* 发送 读 地址中前 8 位 */ 
SPI_FLASH_SendByte((ReadAddr & 0xFF0000) >> 16); 
/* 发送 读 地址中后 8 位 */ 
SPI_FLASH_SendByte((ReadAddr& 0xFF00) >> 8); 
/* 发送 读 地址低 8 位 */ 
SPI_FLASH_SendByte(ReadAddr & 0xFF); 
/* 读取数据 */ 
while (NumByteToRead--) { 
/* 读取一个字节*/ 
*pBuffer = SPI_FLASH_SendByte(Dummy_Byte); 
/* 指向下一个字节缓冲区 */ 
pBuffer++; 
} 
/* 停止信号 FLASH: CS 高电平 */ 
SPI_FLASH_CS_HIGH(); 
}

(3)main 函数

#define RxBufferSize1   (countof(TxBuffer1) - 1) 
#define countof(a)      (sizeof(a) / sizeof(*(a))) 
#define  BufferSize (countof(Tx_Buffer)-1) 
 
#define  FLASH_WriteAddress    0x00000 
#define  FLASH_ReadAddress     FLASH_WriteAddress 
#define  FLASH_SectorToErase   FLASH_WriteAddress 
 
 
/* 发送缓冲区初始化 */ 
uint8_t Tx_Buffer[] = " stm32 开发板\r\n"; 
uint8_t Rx_Buffer[BufferSize]; 
 
//读取的 ID 存储位置 
  IO uint32_t DeviceID = 0; 
  IO uint32_t FlashID = 0; 
  IO TestStatus TransferStatus1 = FAILED; 
 
// 函数原型声明 
void Delay(  IO uint32_t nCount); 
 
/* 
* 函数名:main 
* 描述  :主函数 
* 输入  :无 
* 输出  :无 
*/ 
int main(void) 
{ 
LED_GPIO_Config(); 
LED_BLUE; 
/* 配置串口 1 为:115200 8-N-1 */ 
Debug_USART_Config(); 
printf("\r\n 这是一个 16M 串行 flash(W25Q128)实验 \r\n"); 
Delay( 200 ); 
/* 获取 SPI Flash ID */  
FlashID = SPI_FLASH_ReadID(); 
/* 检验 SPI Flash ID */ 
if (FlashID == sFLASH_ID || FlashID2 == sFLASH_ID) 
{ 
printf("\r\n 检测到 SPI FLASH W25Q256 !\r\n"); 
/* 擦除将要写入的 SPI FLASH 扇区,FLASH 写入前要先擦除 */ 
SPI_FLASH_SectorErase(FLASH_SectorToErase); 
/*  将发送缓冲区的数据写到 flash 中  */ 
SPI_FLASH_BufferWrite(Tx_Buffer, FLASH_WriteAddress, BufferSize); 
printf("\r\n 写入的数据为:\r\n%s", Tx_Buffer); 
/* 将刚刚写入的数据读出来放到接收缓冲区中 */ 
SPI_FLASH_BufferRead(Rx_Buffer, FLASH_ReadAddress, BufferSize); 
printf("\r\n 读出的数据为:\r\n%s", Rx_Buffer); 
/* 检查写入的数据与读出的数据是否相等 */ 
TransferStatus1 = Buffercmp(Tx_Buffer, Rx_Buffer, BufferSize); 
if ( PASSED == TransferStatus1 ) 
{ 
LED_GREEN; 
printf("\r\n16M 串行 flash(W25Q256)测试成功!\n\r"); 
} 
else 
{ 
LED_RED; 
printf("\r\n16M 串行 flash(W25Q256)测试失败!\n\r"); 
} 
}// if (FlashID == sFLASH_ID) 
else 
{ 
LED_RED; 
printf("\r\n 获取不到 W25Q256 ID!\n\r"); 
} 
SPI_Flash_PowerDown(); 
while (1); 
}