SPI应用——W25Q128串行FLASH

一.FLASH存储器介绍

FLASH存储器又称闪存,它与EEPROM都是掉电后数据不丢失的存储器,但FLASH存储器容量普遍大于EEPROM,现在基本取代了它的地位。在存储控制上,最主要的区别是FLASH芯片只能一大片一大片地擦写,而EEPROM可以单个字节擦写。
W25Q128是一种使用SPI通讯协议的NOR FLASH存储器,它的CS/CLK/DIO/DO引脚分别连接到了 STM32对应的SDI引脚NSS/SCK/MOSI/MISO上,其中STM32的NSS引脚是一个普通的GPIO,不是SPI的专用NSS引脚,所以程序要使用软件控制片选的方式。

二.编程要点

1.初始化通讯使用的目标引脚、使能端口时钟及SPI外设的时钟

(1)使用GPIO_InitTypeDef定义GPIO初始化结构体变量,以便下面用于存储GPIO配置;

(2)调用库函数SPIx_SCK_GPIO_CLK_ENABLE(),SPIx_MISO_GPIO_CLK_ENA BLE()等完成SPI相关引脚的时钟使能。调用库函数SPIx_CLK_ENABLE()完成SPI外设的使能。

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

(4)使用以上初始化结构体的配置,调用HAL_GPIO_Init函数向分别寄存器写入参数,完成GPIO的初始化。

 /**
   * @brief SPI MSP 初始化
   *       此函数配置此示例中使用的硬件资源:
   *           - 外设时钟使能
   *           - 外设引脚配置
   * @param hspi: SPI句柄指针
   * @retval 无
   */
 void HAL_SPI_MspInit(SPI_HandleTypeDef *hspi)
 {
     GPIO_InitTypeDef  GPIO_InitStruct;

     /*##-1- Enable peripherals and GPIO Clocks */
     /* Enable GPIO TX/RX clock */
     SPIx_SCK_GPIO_CLK_ENABLE();
     SPIx_MISO_GPIO_CLK_ENABLE();
     SPIx_MOSI_GPIO_CLK_ENABLE();
     SPIx_CS_GPIO_CLK_ENABLE();
     
     /* Enable SPI clock */
     SPIx_CLK_ENABLE();

     /*##-2- Configure peripheral GPIO */
     /* SPI SCK GPIO pin configuration  */
     GPIO_InitStruct.Pin       = SPIx_SCK_PIN;
     GPIO_InitStruct.Mode      = GPIO_MODE_AF_PP;//复用推挽输出
     GPIO_InitStruct.Pull      = GPIO_PULLUP;
     GPIO_InitStruct.Speed     = GPIO_SPEED_FAST;
     GPIO_InitStruct.Alternate = SPIx_SCK_AF;
     HAL_GPIO_Init(SPIx_SCK_GPIO_PORT, &GPIO_InitStruct);

     /* SPI MISO GPIO pin configuration  */
     GPIO_InitStruct.Pin = SPIx_MISO_PIN;
     GPIO_InitStruct.Alternate = SPIx_MISO_AF;
     HAL_GPIO_Init(SPIx_MISO_GPIO_PORT, &GPIO_InitStruct);

     /* SPI MOSI GPIO pin configuration  */
     GPIO_InitStruct.Pin = SPIx_MOSI_PIN;
     GPIO_InitStruct.Alternate = SPIx_MOSI_AF;
     HAL_GPIO_Init(SPIx_MOSI_GPIO_PORT, &GPIO_InitStruct);

     GPIO_InitStruct.Pin = FLASH_CS_PIN ;
     GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
     HAL_GPIO_Init(FLASH_CS_GPIO_PORT, &GPIO_InitStruct);
 }

2.配置SPI外设的模式、地址、速率等参数并使能SPI外设

在配置STM32的SPI模式前,要先了解从机端的SPI模式。通过查阅FLASH数据手册《W25Q128》可知,W25Q128支持SPI模式0及模式3,支持双线全双工, 使用MSB先行模式,支持最高通讯时钟为104MHz,数据帧长度为8位。STM32的SPI外设中的这些参数配置需要与从机端一致。

void SPI_FLASH_Init(void)
 {
     /* Set the SPI parameters */
     SpiHandle.Instance               = SPIx;
     SpiHandle.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_2;
     SpiHandle.Init.Direction         = SPI_DIRECTION_2LINES;
     SpiHandle.Init.CLKPhase          = SPI_PHASE_2EDGE;
     SpiHandle.Init.CLKPolarity       = SPI_POLARITY_HIGH;
     SpiHandle.Init.CRCCalculation    = SPI_CRCCALCULATION_DISABLE;
     SpiHandle.Init.CRCPolynomial     = 7;
     SpiHandle.Init.DataSize          = SPI_DATASIZE_8BIT;
     SpiHandle.Init.FirstBit          = SPI_FIRSTBIT_MSB;
     SpiHandle.Init.NSS               = SPI_NSS_SOFT;
     SpiHandle.Init.TIMode            = SPI_TIMODE_DISABLE;

     SpiHandle.Init.Mode = SPI_MODE_MASTER;

     HAL_SPI_Init(&SpiHandle);

     __HAL_SPI_ENABLE(&SpiHandle);
 }

STM32的SPI外设配置为主机端,双线全双工模式,数据帧长度为8位,使用SPI模式3(CLKPolarity =1,CLKPhase =1), NSS引脚由软件控制以及MSB先行模式。由于与FLASH芯片通信不需要CRC校验,并没有使能SPI的CRC功能, 这时CRC计算式的成员值是无效的。 赋值结束后调用库函数HAL_SPI_Init把这些配置写入寄存器,并调用__HAL_SPI_ENABLE函数使能外设。

3.编写基本SPI按字节收发的函数

 /**
  * @brief  使用SPI发送一个字节的数据
  * @param  byte:要发送的数据
  * @retval 返回接收到的数据
  */
uint8_t SPI_FLASH_SendByte(uint8_t byte)
{
  SPITimeout = SPIT_FLAG_TIMEOUT;

  /* 等待发送缓冲区为空,TXE事件 置1为非空*/
  while (__HAL_SPI_GET_FLAG( &SpiHandle, SPI_FLAG_TXE ) == RESET)
   {
    if((SPITimeout--) == 0) return SPI_TIMEOUT_UserCallback(0);
   }

  /* 写入数据寄存器,把要写入的数据写入发送缓冲区 */
  WRITE_REG(SpiHandle.Instance->DR, byte);

  SPITimeout = SPIT_FLAG_TIMEOUT;

  /* 等待接收缓冲区非空,RXNE事件 */
  while (__HAL_SPI_GET_FLAG( &SpiHandle, SPI_FLAG_RXNE ) == RESET)
   {
    if((SPITimeout--) == 0) return SPI_TIMEOUT_UserCallback(1);
   }

  /* 读取数据寄存器,获取接收缓冲区数据 */
  return READ_REG(SpiHandle.Instance->DR);
}

 /**
  * @brief  使用SPI读取一个字节的数据
  * @param  无
  * @retval 返回接收到的数据
  */
uint8_t SPI_FLASH_ReadByte(void)
{
  return (SPI_FLASH_SendByte(Dummy_Byte));
}

(1)该函数中不包含SPI起始和停止信号,只是收发的主要过程,所以在调用本函数前后要做好起始和停止信号的操作;

(2)对SPITimeout变量赋值为宏SPIT_FLAG_TIMEOUT。这个SPITimeout变量在下面的while中每次循环减1,该循环通过调用库函数SPI_I2S_GetFlagStatus检测事件,若检测到事件,则进入通讯的下一阶段,若未检测到事件则停留在此处一直检测,当检测SPIT_FLAG_TIMEOUT次都还没等待到事件则认为通讯失败,调用的SPI_TIMEOUT_UserCallback输出调试信息,并退出通讯;

(3)通过检测TXE标志,获取发送缓冲区的状态,若发送缓冲区为空,则表示可能存在的上一个数据已经发送完毕;

(4)等待至发送缓冲区为空后,调用函数

WRITE_REG(SpiHandle.Instance->DR, byte);

把要发送的数据“byte”写入到SPI的数据寄存器DR,写入SPI数据寄存器的数据会存储到发送缓冲区,由SPI外设发送出去;

(5)写入完毕后等待RXNE事件,即接收缓冲区非空事件。由于SPI双线全双工模式下MOSI与MISO数据传输是同步的,当接收缓冲区非空时,表示上面的数据发送完毕,且接收缓冲区也收到新的数据;

(6)等待至接收缓冲区非空时,通过调用函数

READ_REG(SpiHandle.Instance->DR);

读取SPI的数据寄存器DR,就可以获取接收缓冲区中的新数据。代码中使用关键字“return”把接收到的这个数据作为SPI_FLASH_SendByte函数的返回值,所以可以看到在下面定义的SPI接收数据函数SPI_FLASH_ReadByte,它只是简单地调用了SPI_FLASH_SendByte函数发送数据“Dummy_Byte”,然后获取其返回值(因为接收字节函数不关注发送的数据而关注接收的数据,所以此时的输入参数“Dummy_Byte”可以为任意值)。可以这样做的原因是SPI的接收过程和发送过程实质是一样的,收发同步进行,关键在于上层应用中关注的是发送还是接收的数据。

4.编写对FLASH擦除及读写操作的函数

(1)指令说明

FLASH芯片自定义了很多指令,通过控制STM32利用SPI总线向FLASH芯片发送指令,FLASH芯片收到后就会执行相应的操作。
这些指令对主机端(STM32)来说,只是它遵守最基本的SPI通讯协议发送出的数据,但在设备端(FLASH芯片)把这些数据解释成不同的意义,所以才成为指令。查看FLASH芯片的数据手册《W25Q128》,可了解各种指令的功能及指令格式。
在这里插入图片描述
该表中的第一列为指令名,第二列为指令编码,第三至第N列的具体内容根据指令的不同而有不同的含义。其中带括号的字节参数,方向为FLASH向主机传输,即命令响应;不带括号的为主机向FLASH传输。
“ A0~A23” 指FLASH芯片 内部存储器组织的地址;
“ M0~M7” 为厂商号(MANUFACTURER ID);
“ID0-ID15”为FLASH芯片的ID;
“dummy”指该处可为任意数据;
“ D0~D7” 为FLASH内部存储矩阵的内容。
在FLASH芯片内部,存储有固定的厂商编号(M7-M0)和不同类型FLASH芯片独有的编号(ID15-ID0)

FLASH型号厂商号FLASH型号(ID15-ID0)
W25Q64EF h4017 h
W25Q128EF h4018 h

通过指令表中的读ID指令“JEDEC ID”可以获取这两个编号,该指令编码为“9F h”,其中“9F h”是指16进制数“9F” (相当于C语言中的0x9F)。紧跟指令编码的三个字节分别为FLASH芯片输出的“生产厂商(M7-M0)”、“存储器类型(ID15-ID8)”及“容量(ID7-ID0)” 。常见的应用是主机端通过读取设备ID来测试硬件是否连接正常,或用于识别设备。
在这里插入图片描述

(2)定义FLASH指令编码表

手册中的指令编码使用宏进行封装,方便调用。

/* Private typedef -----------------------------------------------------------*/
//#define  sFLASH_ID                       0xEF3015     //W25X16
//#define  sFLASH_ID                       0xEF4015      //W25Q16
//#define  sFLASH_ID                        0XEF4017     //W25Q64
//#define  sFLASH_ID                       0XEF4018     //W25Q128
#define  sFLASH_ID                       0XEF4019     //W25Q256


//#define SPI_FLASH_PageSize               4096
#define SPI_FLASH_PageSize               256
#define SPI_FLASH_PerWritePageSize       256

/* Private define ------------------------------------------------------------*/
/*命令定义-开头*******************************/
#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  /* Write In Progress (WIP) flag */
#define Dummy_Byte                0xFF
/*命令定义-结尾*******************************/
(3)读取FLASH芯片ID

根据“JEDEC”指令的时序把读取FLASH ID的过程编写成一个函数

/*
 * @brief 读取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;
 }

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

(4)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();
 }

与EEPROM一样,FLASH芯片向内部存储矩阵写入数据需要消耗一定的时间,并不是在总线通讯结束的一瞬间完成的, 所以在写操作后需要确认FLASH芯片“空闲”时才能进行再次写入。为了表示工作状态, FLASH芯片定义了一个状态寄存器:
在这里插入图片描述
状态寄存器的第0位“BUSY”,当这个位为“1”时,表明FLASH芯片处于忙碌状态,它可能正在对内部的存储矩阵进行“擦除”或“数据写入”的操作。
向FLASH芯片发送“Read Status Register”指令05 h,FLASH芯片就会持续向主机返回最新的状态寄存器内容, 直到收到SPI通讯的停止信号。
在这里插入图片描述

 /**
  * @brief  等待WIP(BUSY)标志被置0,即等待到FLASH内部数据写入完毕
  * @param  none
  * @retval none
  */
void SPI_FLASH_WaitForWriteEnd(void)
{
  uint8_t 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();
}
(5)FLASH扇区擦除

由于FLASH存储器的特性决定其只能把原来为1的数据位改写成0,而原来为0的数据位不能直接改写成1。这里涉及数据”擦除“的概念:在写入前必须对目标存储矩阵进行擦除操作,把矩阵中的数据位擦除为1,在数据写入时如果要存储数据1就不修改存储矩阵;要存储数据0时,才更改该位。
通常对存储矩阵擦除的基本操作单位都是多个字节进行,W25Q128 FLASH芯片支持“扇区擦除”、“块擦除”以及“整片擦除”。FLASH芯片的最小擦除单位为扇区(Sector),而一个块(Block)包含16个扇区,其内部存储矩阵分布如下。

擦除单位大小
扇区擦除Sector Erase4KB
块擦除Block Erase64KB
整片擦除Chip Erase整个芯片完全擦除

在这里插入图片描述
扇区擦除指令的第一个字节为指令编码,紧接着发送的3个字节用于表示要擦除的24位存储矩阵地址。 要注意的是在扇区擦除指令前,还需要先发送“写使能”指令,发送扇区擦除指令后, 通过读取寄存器状态等待扇区擦除操作完毕,扇区擦除时序、实现代码如下。
在这里插入图片描述

 /**
  * @brief  擦除FLASH扇区
  * @param  SectorAddr:要擦除的扇区地址
  * @retval 无
  */
void SPI_FLASH_SectorErase(uint32_t 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();
}

注意发送擦除地址时高位在前,调用扇区擦除指令时输入的地址要对齐到4KB。

(6)FLASH的页写入

目标扇区被擦除完毕后,就可以写入数据了。与EEPROM类似,FLASH芯片也有页写入命令, 使用页写入命令最多可以一次向FLASH传输256个字节的数据,称这个单位为页大小,FLASH页写入时序如下。若发送的数据超出256个,则会覆盖前面发送的数据。
在这里插入图片描述
与擦除指令不同,页写入指令的地址并不要求按256字节对齐,只要确认目标存储单元是擦除状态即可(即被擦除后没有被写入过)。所以,若对“地址x”执行页写入指令后,发送了200个字节数据后终止通讯,下一次再执行页写入指令,从“地址(x+200)”开始写入200个字节也是没有问题的(小于256均可)。只是在实际应用中由于基本擦除单元是4KB,一般都以扇区为单位进行读写。

 /**
 * @brief  对FLASH按页写入数据,调用本函数写入数据前需要先擦除扇区
 * @param  pBuffer,要写入数据的指针
 * @param WriteAddr,写入地址
 * @param  NumByteToWrite,写入数据长度,必须小于等于页大小
 * @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);
     /*发送写地址的高位*/
     SPI_FLASH_SendByte((WriteAddr & 0xFF0000) >> 16);
     /*发送写地址的中位*/
     SPI_FLASH_SendByte((WriteAddr & 0xFF00) >> 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();
 }
(7)不定量数据写入

实际应用中常常要写入不定量的数据,直接调用“页写入”函数并不方便,所以在其原来基础上编写了“不定量数据写入”的函数。

 /**
 * @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;

     /*差count个数据值,刚好可以对齐到页地址*/
     count = SPI_FLASH_PageSize - Addr;
     /*计算出要写多少整数页*/
     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);
             }
         }
     }
 }
(8)从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);

     /* 发送 读 地址高位 */
     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++;
     }

     /* 停止信号 FLASH: CS 高电平 */
     SPI_FLASH_CS_HIGH();
 }

5.编写测试程序,对读写数据进行校验

 /* 获取缓冲区的长度 */
 #define TxBufferSize1   (countof(TxBuffer1) - 1)
 #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)
 {
     SystemClock_Config();
     LED_GPIO_Config();
     LED_BLUE;
     /* 配置串口1为:115200 8-N-1 */
     Debug_USART_Config();

     printf("\r\n这是一个16M串行flash(W25Q128)实验 \r\n");

     /* 16M串行flash W25Q128初始化 */
     SPI_FLASH_Init();

     Delay( 200 );

     /* 获取 SPI Flash ID */
     FlashID = SPI_FLASH_ReadID();

     /* 检验 SPI Flash ID */
     if (FlashID == sFLASH_ID)
     {
         printf("\r\n检测到SPI FLASH W25Q128 !\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(W25Q128)测试成功!\n\r");
         }
         else
         {
             LED_RED;
             printf("\r\n16M串行flash(W25Q128)测试失败!\n\r");
         }
     }// if (FlashID == sFLASH_ID)
     else
     {
         LED_RED;
         printf("\r\n获取不到 W25Q128 ID!\n\r");
     }

     SPI_Flash_PowerDown();
     while (1);
 }

/*
 * 函数名:Buffercmp
 * 描述  :比较两个缓冲区中的数据是否相等
 * 输入  :-pBuffer1     src缓冲区指针
 *         -pBuffer2     dst缓冲区指针
 *         -BufferLength 缓冲区长度
 * 输出  :无
 * 返回  :-PASSED pBuffer1 等于   pBuffer2
 *         -FAILED pBuffer1 不同于 pBuffer2
 */
TestStatus Buffercmp(uint8_t* pBuffer1, uint8_t* pBuffer2, uint16_t BufferLength)
{
  while(BufferLength--)
  {
    if(*pBuffer1 != *pBuffer2)
    {
      return FAILED;
    }

    pBuffer1++;
    pBuffer2++;
  }
  return PASSED;
}

初始化LED、串口、SPI外设,然后读取FLASH芯片的ID进行校验,若ID校验通过则向FLASH的特定地址写入测试数据,然后再从该地址读取数据,测试读写是否正常。

  • 4
    点赞
  • 39
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
根据引用\[1\]中提供的信息,可以使用STM32的HAL库来模拟SPI方式驱动W25Q128存储芯片。首先需要进行模拟SPI方式的IO配置,然后使用相应的驱动代码来实现功能。 W25Q128是一款SPI接口的NOR Flash芯片,具有128 Mbit的存储空间,相当于16M字节。NOR Flash是一种常用的用于存储数据的半导体器件,具有容量大、可重复擦写、按扇区/块擦除、掉电后数据可继续保存的特性。Flash的物理特性是只能写0,不能直接写1,写1需要进行擦除操作。 根据引用\[3\]中的实验,可以通过硬件接线将W25Q128模块与STM32连接起来,其中VCC接3.3V,CS接PA4,CLK接PA5,DO接PA6,DI接PA7。然后可以使用CubeMX进行相应的配置。 要获取W25Q128的ID,可以使用SPI通信协议来读取芯片的ID寄存器。具体的代码实现可以参考引用\[1\]中提供的驱动代码。 #### 引用[.reference_title] - *1* [STM32CubeMX | STM32使用HAL库模拟SPI方式驱动W25Q128存芯片](https://blog.csdn.net/qq153471503/article/details/106895933)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* *3* [STM32SPIW25Q128](https://blog.csdn.net/weixin_49001476/article/details/130909856)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值