怎样写SPI NOR FLASH 驱动

SPI 总线相关知识请自行百度。

STM32 SPI 状态寄存器(SPI_SR)中的一些常用标志位

TxE:发送缓冲为空 (Transmit buffer empty) ,为空则可发送数据

RXNE:接收缓冲非空 (Receive buffer not empty) 为非空则可读取数据

BSY:忙标志 (Busy flag)

STM32 SPI读写时序

这里以W25Q64为例介绍SPI接口的NOR flash驱动写法。

先查阅W25Q64数据手册,W25Q64简介如下

W25Q64是华邦公司推出的大容量SPI FLASH产品,其容量为64Mb。该25Q系列的器件在灵活性和性能方面远远超过普通的串行闪存器件。W25Q64将8M字节的容量分为128个块,每个块大小为64K字节,每个块又分为16个扇区,每个扇区4K个字节。W25Q64的最小擦除单位为一个扇区,也就是每次必须擦除4K个字节。所以,这需要给W25Q64开辟一个至少4K的缓存区,这样必须要求芯片有4K以上的SRAM才能有很好的操作。 
W25Q64的擦写周期多达10W次,可将数据保存达20年之久,支持2.7~3.6V的电压,支持标准的SPI,还支持双输出/四输出的SPI,最大SPI时钟可达80Mhz。

W25Q64特征: 
 支持标准、双输出和四输出的SPI 
 高性能串行闪存 
 高达普通串行闪存性能的6倍 
 80Mhz的时钟操作 
 支持160Mhz的双输出SPI 
 支持320Mhz的四输出SPI 
 40MB/S的数据连续传输速率 
 高效的“连续读取模式” 
 低指令开销 
 仅需8个时钟周期处理内存 
 允许XIP操作 
 性能优于X16并行闪存 
 低功耗,温度范围宽 
 单电源2.7V至3.6V 
 4mA有源电流 
 -40°C 至+85°C的正常运行温度范围 
 灵活的4KB扇区构架 
 扇区统一擦除(4KB) 
 块擦除(32KB和64KB) 
 1到256个字节编程 
 超过10万次擦除/写循环 
 超过20年的数据保存 
 高级的安全功能 
 软件和硬件写保护 
 自上至下,扇区或块选择 
 锁定和保护OTP 
 每个设备都有唯一的64位ID 
 有效的空间的包装 
 8-pin SOIC 208-mil 
 8-pin PDIP 300-mil 
 8-pad WSON 8x6-mm 
 16-pin SOIC 300-mil 
W25Q64 
 CS:片选信号输入 
 DO(IO1):数据输出(数据输入输出1) 
 WP(IO2):写保护输入(数据输入输出2) 
 GND:地信号 
 DI(IO0):数据输入(数据输入输出0) 
 CLK:串行时钟输入 
 HOLD(IO3):Hold输入(数据输入输出3) 
 VCC:电源 
注: 
1. IO0和IO1用于标准SPI和双输出SPI操作 
2. IO0-IO3用于四输出SPI操作

片选: 
SPI片选引脚能够使能和失能器件的操作。当片选引脚为高电平时,器件没有被选中,串行数据输出引脚(DO或IO0,IO2,IO3,IO4)处于高阻抗状态。当器件没有被选中时,其功耗将会处于待机状态下的水平,除非内部正在擦除、运行程序或状态寄存器周期。当片选引脚置为低时,器件被选中,电源消耗将增至活跃水平,可以进行读写操作。电源上电后,在执行一次操作之前,片选引脚必须由高电平转至低电平。

串行数据输入、输出: 
W25Q64支持标准的SPI,双输出SPI和四输出SPI操作。标准的SPI指令利用单向的数据输入引脚在串行时钟输入上升沿串行地向器件写入指令、地址或数据。标准的SPI也利用单向的数据输出引脚在串行时钟输入下降沿串行地从器件读取数据或状态。 
双输出和四输出SPI利用双向IO引脚在串行时钟输入上升沿串行地向器件写入指令、地址或数据,在串行时钟输入下降沿串行地从器件读取数据或状态。

写保护: 
WP引脚用来防止状态寄存器被写入。用于与状态寄存器的块保护位(SEC、TB、BP2、BP1和BP0)配合,状态寄存器保护位(SRP),部分或整个存储器阵列可以用硬件保护。WP引脚在低电平时有效。当状态寄存器2的QE位设置为四倍I/O时,则WP(硬件写保护)功能不可用,因为此时这个引脚被用为IO2。

HOID: 
HOID引脚允许器件在有效选择的情况下被终止。当HOLD引脚被置为低电平时,CS引脚为低,DO引脚将处于高阻态状态,并且DI和CLK引脚上的信号将被忽略。当HOLD引脚被置为高电平时,器件操作将被恢复。HOLD引脚的功能通常用在当多个器件共享同一个SPI信号的情况下。HOLD引脚在低电平时有效。当状态寄存器2的QE位设置为四倍I/O时,则HOLD引脚功能不可用,因为此时这个引脚被用为IO3。

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

由上图可知,对flash的读写擦除都是通过发送命令来实现的。命令的长短各有不同,以Ax开头的命令表示地址。

如A23-A16表示地址数据的BIT16位到23位,以Dx开头的命令表示数据,有括号的命令表示数据的方向是从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

/* WIP(busy)标志,FLASH内部正在写入 */
#define WIP_Flag                  0x01
#define Dummy_Byte                0xFF
/*命令定义-结尾*******************************/

STM32 与FLASH连接电路如下图:

WP是写保护,HOLD用于暂停通讯,都是低电平有效,我们这里不用这两个功能,因此直接接电源作为高电平关闭这两个功能.

第一步先配置需要用到的GPIO和SPI

void GPIO_FOR_FLASH_CONFIG(void)
{
//由电路图可知,用到的GPIO有
//PC0 -----CS  片选
//PA5-----SPI1 SCK
//PA6-----SPI1 MISO
//PA7-----SPI1 MOSI
//因此需要配置GPIOA和GPIOC的时钟

GPIO_InitTypeDef GPIO_InitStructure;


//使能GPIOA和GPIOC的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC|RCC_APB2Periph_GPIOA,ENABLE);

/* 配置SPI的 CS引脚,普通IO即可 */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(GPIOC, &GPIO_InitStructure);
  
/* 配置SPI的 SCK引脚*/
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;//复用推挽输出
GPIO_Init(GPIOA, &GPIO_InitStructure);

/* 配置SPI的 MISO引脚*/
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_Init(GPIOA, &GPIO_InitStructure);

/* 配置SPI的 MOSI引脚*/
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7;
GPIO_Init(GPIOA, &GPIO_InitStructure);

}

 void SPI_FOR_FLASH_CONFIG(void)
 {
 //由电路图可知,用到的spi是spi1
 
 
 SPI_InitTypeDef  SPI_InitStructure;

 
 
 /* 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_4;
  SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;
  SPI_InitStructure.SPI_CRCPolynomial = 7;
  SPI_Init(SPI1 , &SPI_InitStructure);

  /* 使能 SPI  */
  SPI_Cmd(SPI1 , ENABLE);
 
 }
  

复杂的问题都是由一些相对简单的问题拼凑起来的,因此可以将其分解为一些相对简单的问题来解决,要解决FLASH的驱动问题,先要实现flash的读写函数接口,要写一批数据,先要解决写入一个byte数据的问题.要想写入一个byte数据,必须先使能flash的读写功能.即将CS引脚电平置低,读写完后将其置高.用两个宏来实现.

#define          SPI_FLASH_CS_LOW()                             GPIO_ResetBits( GPIOC, GPIO_Pin_0 )
#define          SPI_FLASH_CS_HIGH()                            GPIO_SetBits( GPIOC, GPIO_Pin_0 )

CS置低电平后,就可以给FALSH发送命令了,要使能FLASH,先必须发送使能命令.要想能发送命令,必须先实现发送一个字节的函数.

该函数如下

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

 u8 SPI_FLASH_SendByte(u8 byte)
 {
	  SPITimeout = SPIT_FLAG_TIMEOUT;
   /* 等待发送缓冲区为空,TXE事件 */
   while (SPI_I2S_GetFlagStatus(SPI1 , SPI_I2S_FLAG_TXE) == RESET)
	 {
	 if((SPITimeout--) == 0) return SPI_TIMEOUT_UserCallback(0);
	}
 
   /* 写入数据寄存器,把要写入的数据写入发送缓冲区 */
   SPI_I2S_SendData(SPI1 , byte);
 
	 SPITimeout = SPIT_FLAG_TIMEOUT;
   /* 等待接收缓冲区非空,RXNE事件 */
   while (SPI_I2S_GetFlagStatus(SPI1 , SPI_I2S_FLAG_RXNE) == RESET)
   {
	 if((SPITimeout--) == 0) return SPI_TIMEOUT_UserCallback(1);
	}
 
   /* 读取数据寄存器,获取接收缓冲区数据 */
   return SPI_I2S_ReceiveData(SPI1 );
 }

#define Dummy_Byte                0xFF
u8 SPI_FLASH_ReadByte(void)
{
  return (SPI_FLASH_SendByte(Dummy_Byte));
}

 接着实现写使能函数

void SPI_FLASH_WriteEnable(void)
{
  /* 通讯开始:CS低 */
  SPI_FLASH_CS_LOW();

  /* 发送写使能命令*/
  SPI_FLASH_SendByte(W25X_WriteEnable);

  /*通讯结束:CS高 */
  SPI_FLASH_CS_HIGH();
}

 怎样知道写操作是否结束呢,可以通过读取FLASH的状态寄存器的BUSY位得知

 当BUSY为1时,表示芯片处于忙碌状态.

#define BUSY_Flag                  0x01
void SPI_FLASH_WaitForWriteEnd(void)
{
  u8 FLASH_Status = 0;

  /* 选择 FLASH: CS 低 */
  SPI_FLASH_CS_LOW();

  /* 发送 读状态寄存器 命令 */
  SPI_FLASH_SendByte(W25X_ReadStatusReg);

  /* 若FLASH忙碌,则等待 */
  do
  {
		/* 读取FLASH芯片的状态寄存器 */
    FLASH_Status = SPI_FLASH_SendByte(Dummy_Byte);	 
  }
  while ((FLASH_Status & WIP_Flag) == SET);  /* 正在写入标志 */

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

如果要写入的FLASH空间数据不是1,必须先擦除,

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

W25Q64的page size是256 byte.


 /**
  * @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);
  /*发送写地址的高位*/
  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();
}

 不定量数据写入函数

 
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;
			
			/* 先写完count个数据,为的是让下一次要写的地址对齐 */
      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);
      }
    }
  }
}

不定量数据读出函数

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--) /* while there is data to be read */
  {
    /* 读取一个字节*/
    *pBuffer = SPI_FLASH_SendByte(Dummy_Byte);
    /* 指向下一个字节缓冲区 */
    pBuffer++;
  }

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

为了验证上述代码能否正常工作,可以在main()函数中向FLASH写入一段数据,然后读出来,比较写入的数据和读出的数据是否一样


#define countof(a)      (sizeof(a) / sizeof(*(a)))
#define  BUFFERSIZE  (countof(Tx_Buffer))
uint8_t Tx_Buffer[] = "SPI NOR FLASH 读写测试\r\n";
uint8_t Rx_Buffer[BUFFERSIZE];
#define  FLASH_WriteAddress     0x00000
#define  FLASH_ReadAddress      FLASH_WriteAddress
#define  FLASH_SectorToErase    FLASH_WriteAddress

typedef enum { FAILED = 0, PASSED = !FAILED} TestStatus;

TestStatus Buffercmp(uint8_t* pBuffer1, uint8_t* pBuffer2, uint16_t BufferLength)
{
  while(BufferLength--)
  {
    if(*pBuffer1 != *pBuffer2)
    {
      return FAILED;
    }

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



int main(void)
{ 	
	
	
	
	/* 配置串口为:115200 8-N-1 */
	USART_Config();//
	printf("\r\n 这是一个8Mbyte串行flash(W25Q64)实验 \r\n");
	
	
	GPIO_FOR_FLASH_CONFIG();
	SPI_FOR_FLASH_CONFIG();


	
		printf("\r\n 检测到串行flash W25Q64 !\r\n");
		
		/* 擦除将要写入的 SPI FLASH 扇区,FLASH写入前要先擦除 */
		// 这里擦除4K,即一个扇区,擦除的最小单位是扇区
		SPI_FLASH_SectorErase(FLASH_SectorToErase);	 	 
		
		/* 将发送缓冲区的数据写到flash中 */
		// 这里写一页,一页的大小为256个字节
		SPI_FLASH_BufferWrite(Tx_Buffer, FLASH_WriteAddress, BufferSize);		
		printf("\r\n 写入的数据为:%s \r\t", Tx_Buffer);
		
		/* 将刚刚写入的数据读出来放到接收缓冲区中 */
		SPI_FLASH_BufferRead(Rx_Buffer, FLASH_ReadAddress, BufferSize);
		printf("\r\n 读出的数据为:%s \r\n", Rx_Buffer);
		
		
		if( PASSED == Buffercmp(Tx_Buffer, Rx_Buffer, BufferSize) )
		{ 
			LED_GREEN;
			printf("\r\n 8M串行flash(W25Q64)测试成功!\n\r");
		}
		else
		{        
			LED_RED;
			printf("\r\n 8M串行flash(W25Q64)测试失败!\n\r");
		}
	
	
	
	while(1);  
}

猜你喜欢

转载自blog.csdn.net/rannar/article/details/81261235