STM32 SPI读写W25Q64(二)

接口/总线/驱动

1142人已加入

描述

W25Q64 将 8M 的容量分为 128 个块(Block),每个块大小为 64K 字节,每个块又分为 16个扇区(Sector),每个扇区 4K 个字节。 W25Q64 的最少擦除单位为一个扇区,也就是每次必须擦除 4K 个字节。操作需要给 W25Q64 开辟一个至少 4K 的缓存区,对 SRAM 要求比较高,要求芯片必须有 4K 以上 SRAM 才能很好的操作。

W25Q64支持SPI总线操作模式0(0,0)和3(1,1)。模式0和模式3之间的主要区别在于,当SPI总线主处于备用状态且数据没有传输到串行闪存时,CLK信号的正常状态。对于模式0,CLK信号通常在/CS的下降和上升边缘较低。对于模式3,CLK信号通常在/CS的下降和上升边缘较高。

W25Q64 的擦写周期多达 10W 次,具有 20 年的数据保存期限,支持电压为 2.7~3.6V,W25Q64 支持标准的 SPI,还支持双输出/四输出的 SPI,最大 SPI 时钟可以到 80Mhz(双输出时相当于 160Mhz,四输出时相当于 320M)。

W25Q64读写用的是SPI协议,想要读取数据时可以通过发送一个无效数据去接收数据。W25Q64 的读写通过发送指令来完成。

w25Q64

每次写数据之前都要进行写使能。W25Q64写入数据是把地址上的“1”变为“0”,所以要擦除要写的区域让要写入的地址都为“1”,然后在进行写入。

Write Enable指令将状态寄存器中的Write Enable Latch (WEL)位设置为1。每一页程序、扇区擦除、块擦除、芯片擦除和写入状态寄存器指令前必须设置好WEL位。允许写入指令输入驱动/ CS低,转移指令码06h进入数据输入(DI)销CLK的前沿,然后开车/ CS高。

w25Q64

Write Disable指令将状态寄存器中的Write Enable Latch (WEL)位重置为0。写入禁用指令是通过驱动/CS低,将指令代码04h转换到DI引脚,然后驱动/CS高来输入的。注意,WEL位在通电后,并在写状态寄存器、页面程序、扇区擦除、块擦除和芯片擦除指令完成后自动复位。

w25Q64

读取状态寄存器指令允许读取8位状态寄存器。将指令压低/CS,将状态寄存器-1指令05h和状态寄存器-2指令35h移至CLK上升沿DI引脚,即可进入指令。然后将状态寄存器位移到CLK下降边缘的DO pin上,最重要的位(MSB)先移出。状态寄存器位包括BUSY位、WEL位、BP2-BP0位、TB位、SEC位、SRP0位、SRP1位和QE位。

w25Q64

读取状态寄存器指令可以在任何时候使用,甚至在程序、擦除或写入状态寄存器周期正在进行时也可以使用。这允许检查繁忙状态位,以确定周期何时完成,以及设备是否可以接受另一条指令。状态寄存器可以连续读取,指令是通过提高/CS值来完成的。

w25Q64

写状态寄存器指令允许写状态寄存器。允许写入指令之前必须被执行的设备接受写状态寄存器指令(状态寄存器位WEL必须等于1)。写启用后,输入的指令驱动/ CS低,发送指令码 01 h ,然后写状态寄存器的数据字节。

只能写入非易失性状态寄存器位SRP0、SEC、TB、BP2、BP1、BP0(状态寄存器-1的第7、5、4、3、2位)和QE、SRP1(状态寄存器-2的第9和8位)。所有其他状态寄存器位位置都是只读的,不受写状态寄存器指令的影响。

/CS引脚必须在锁定的第8位或第16位数据之后驱动高。如果不这样做,写状态寄存器指令将不会执行。如果/CS在第8个时钟之后驱动高(与25X系列兼容),QE和SRP1位将被清除为0。当/CS被驱动高后,自动定时写状态寄存器周期将开始,持续时间为t - W。在写状态寄存器周期进行时,仍然可以访问读状态寄存器指令来检查忙位的状态。繁忙位在写状态寄存器周期中为1,在周期结束并准备再次接受其他指令时为0。写入寄存器周期结束后,状态寄存器中的写入使能锁存器(WEL)位将被清除为0。

写入状态寄存器指令允许块保护位(SEC、TB、BP2、BP1和BP0)被设置为保护所有、一部分或没有内存不被擦除和程序指令。受保护区域变为只读(请参阅状态寄存器内存保护表和描述)。写入状态寄存器指令还允许设置状态寄存器保护位(SRP0, SRP1)。这些位与写入保护(/WP) pin、锁定或OTP功能一起使用,以禁用对状态寄存器的写入。

w25Q64

读取数据指令允许从内存中再按顺序读取一个数据字节。指令通过将/CS引脚压低,然后将指令代码03h和一个 24位地址(A23-A0) 转换到DI引脚来启动。代码和地址位被锁定在CLK引脚的上升边缘。接收到地址后,首先在CLK下降沿的DO pin上以最有效位(MSB)将所寻址内存位置的数据字节移出。每个字节的数据移出后,地址会自动增加到下一个更高的地址,从而允许连续的数据流。这意味着只要时钟继续运行,就可以用一条指令访问整个内存。指令是通过提高/CS值来完成的。

如果在擦除、程序或写周期(BUSY=1)的过程中发出读数据指令,则该指令将被忽略,并且不会对当前周期产生任何影响。

w25Q64

页写指令允许在以前擦除( FFh )内存位置上编程的数据从一个字节到256字节(一页)。允许写入指令之前,必须执行设备将接受页面程序指令(状态寄存器位逢= 1)。指令是由驱动/ CS销低然后转移 指令代码02h ,后跟一个 24位地址(A23-A0) 和至少一个数据字节,残障保险销。当数据被发送到设备时,在指令的整个长度内,/CS引脚必须保持在较低的位置。

如果要对整个256字节的页面进行编程,最后一个地址字节(8个最不重要的地址位)应该设置为0。如果最后一个地址字节不为零,并且时钟的数量超过剩余的页长,则寻址将换行到页的开头。在某些情况下,可以对少于256字节(部分页面)进行编程,而不会对同一页面中的其他字节产生任何影响。执行部分分页程序的一个条件是时钟的数量不能超过剩余的分页长度。如果发送到设备的字节数超过256,则寻址将自动换行到页面的开头并覆盖之前发送的数据。

与写和擦除指令一样,/CS引脚必须在锁住最后一个字节的第8位之后驱动到高位。如果不这样做,页面程序指令将不会执行。当/CS驱动高后,自动计时的页面程序指令将在tpp期间开始(参见AC特性)。当页面程序周期正在进行时,仍然可以访问Read Status寄存器指令来检查忙位的状态。在页面程序周期中,忙碌位是1,当周期结束,设备准备再次接受其他指令时,忙碌位变为0。当页面程序周期完成后,状态寄存器中的写使能锁存器(WEL)位被清除为0。如果所寻址的页由块Protect (BP2、BP1和BP0)位保护,则不会执行页程序指令。

w25Q64

扇区擦除指令(20h)、32K块擦除指令(52h)、64K块擦除指令(D8h)、均可通过发送扇区或者块首地址进行擦除数据。整片擦除指令(C7h/60h)可擦除整片8M的数据。

#include "spi.h"
#include "delay.h"
#include "w25q64.h"


void W25Q64_Init()
{
  Spi1_Init();
}


//写使能
//在页编程、擦除(扇区、块、全片)、写状态寄存器前,需要写使能
void W25Q64_WriteEnable()
{
  F_CS_L();
  Spi1_RevSendByte(0x06);
  F_CS_H();
}


//注意:
//在页编程、擦除(扇区、块、全片)、写状态寄存器等操作完成后,WEL位会自动清零
//因此,该函数也可以不使用
void W25Q64_WriteDisable()
{
  F_CS_L();
  Spi1_RevSendByte(0x04);
  F_CS_H();
}


//读状态寄存器1
u8 W25Q64_ReadStatusReg1()
{
  u8 temp = 0;

  F_CS_L();
  Spi1_RevSendByte(0x05);
  temp = Spi1_RevSendByte(0xff);
  F_CS_H();

  return temp;
}


//判断忙标记
void W25Q64_WaitBusy()
{
  u8 temp = 0;

  do{
    temp = W25Q64_ReadStatusReg1();
    temp &= 0x01;
  }while(temp);
}


//写状态寄存器1,用于对存储区间进行写保护
void W25Q64_WriteStatusReg1(u8 status)
{
  W25Q64_WriteEnable();
  F_CS_L();
  Spi1_RevSendByte(0x01);
  status < <=2;
  Spi1_RevSendByte(status);
  F_CS_H();
  W25Q64_WaitBusy();
}


//读数据
void W25Q64_ReadBytes(u32 add,u8 *buf,u32 size)
{
  u32 i=0;

  F_CS_L();
  Spi1_RevSendByte(0x03);
  Spi1_RevSendByte((u8)(add > >16));
  Spi1_RevSendByte((u8)(add > >8));
  Spi1_RevSendByte((u8)(add > >0));

  for(i=0;i< size;i++)
    buf[i]=Spi1_RevSendByte(0xff);

  F_CS_H();
}


//页编程
//条件:要写的空间,事先要擦除过
void W25Q64_PageProgram(u32 add,u8 *buf,u16 size)
{
  u16 i = 0;

  W25Q64_WriteEnable();
  F_CS_L();
  Spi1_RevSendByte(0X02);
  Spi1_RevSendByte((u8)(add > >16));
  Spi1_RevSendByte((u8)(add > >8));
  Spi1_RevSendByte((u8)(add > >0));

  for(i=0;i< size;i++)
  {
    Spi1_RevSendByte(*buf++);
  }
  F_CS_H();
  W25Q64_WaitBusy();
}


//扇区擦除
void W25Q64_SectorErase(u32 add)
{
  W25Q64_WriteEnable();
  F_CS_L();
  Spi1_RevSendByte(0X20);
  Spi1_RevSendByte((u8)(add > >16));
  Spi1_RevSendByte((u8)(add > >8));
  Spi1_RevSendByte((u8)(add > >0));
  F_CS_H();
  W25Q64_WaitBusy();
}


//64K块擦除
void W25Q64_BlockErase64K(u32 add)
{
  W25Q64_WriteEnable();
  F_CS_L();
  Spi1_RevSendByte(0XD8);
  Spi1_RevSendByte((u8)(add > >16));
  Spi1_RevSendByte((u8)(add > >8));
  Spi1_RevSendByte((u8)(add > >0));
  F_CS_H();
  W25Q64_WaitBusy();
}


//全片擦除
void W25Q64_ChipErase()
{
  W25Q64_WriteEnable();
  F_CS_L();
  Spi1_RevSendByte(0Xc7);
  F_CS_H();
  W25Q64_WaitBusy();
}

跟AT24C02一样,如果要对W25Q64写入大量数据,通过调用页写函数就会很不方便,所以可以借用AT24C02连续写的思想,编写W25Q64扇区写和连续写的函数。

//由于擦除的最小单位是扇区(4K),而写一次的单位是页(256字节),两者存在大小上的不匹配,
//这里扩充写操作,也将范围扩充到4K。这就要求在写操作时要能实现换页。
//另外:擦除涉及到对已存储数据的破坏,所以实际处理时,需考虑数据保护。
//为实现方便,这里假定所操作的4K已经被擦除过。
//设计不检查空间擦除的操作函数
//条件:要写的空间已经擦除过
void W25Q64_SectorWrite(u32 add,u8 *buf,u32 size)
{
  u16 CanWriteBytes = 0;

  while(1)
  {
    CanWriteBytes = 256 - add%256;          //计算当前页可写入的字节数
    if(CanWriteBytes >= size)
    {
      W25Q64_PageProgram(add,buf,size);
      break;
    }
    else
    {
      W25Q64_PageProgram(add,buf,CanWriteBytes);
      add += CanWriteBytes;
      buf += CanWriteBytes;
      size -= CanWriteBytes;
    }
  }
}


//连续写操作
//所操作的空间可以没有擦除过,如果没有擦除过,则擦除
//擦除前,需对数据进行保护


u8 W25Q64_BUF[4096] = {0xff};


void W25Q64_ContinueWrite(u32 add,u8 *buf,u32 size)
{
  u16 CanWriteBytes = 0;        //计算当前扇区能写的字节数
  u32 SectorAdd = 0;            //当前扇区的首地址
  u16 i = 0;

  while(1)
  {
    SectorAdd = (add/4096)*4096;    //计算出当前扇区的首地址
    W25Q64_ReadBytes(SectorAdd,W25Q64_BUF,4096);  //读取当前扇区内容
    //判断要写的那部分扇区空间是否擦除过
    CanWriteBytes = 4096 - add%4096;

    if(CanWriteBytes >= size)
      CanWriteBytes = size;          //如果可写空间比要写的内容多,则只判断size个字节即可

    for(i=0;i< CanWriteBytes;i++)
    {
      if(W25Q64_BUF[add%4096 + i] != 0xff)        //没有擦除
        break;
    }

    if(i< CanWriteBytes)     //说明要写的那部分扇区,存在没有擦除过的空间
      //1. 擦除
      W25Q64_SectorErase(SectorAdd);  //擦除整个扇区
    //用要写入的数据,填充W25Q64_BUF
    for(i=0;i< CanWriteBytes;i++)
    {
      W25Q64_BUF[add%4096 + i] = buf[i];
    }

    W25Q64_SectorWrite(SectorAdd,W25Q64_BUF,4096);
    if(CanWriteBytes == size)
      break;

    add += CanWriteBytes;
    buf += CanWriteBytes;
    size -= CanWriteBytes;
  }
}

读写过程中,如果想让一些数据不丢失,W25Q64可以通过硬件进行内存保护,但是硬件保护是整片保护的,操作起来不灵活。所以采用指令进行内存保护,可以通过写状态寄存器1对特定区域进行内存保护。

w25Q64

W25Q64可擦写次数只有10万多次,所以写函数同样不推荐放入while循环中。

主文件

#include "stm32f4xx.h"
#include "led.h"
#include "usart.h"
#include "delay.h"
#include "stdio.h"
#include "W25Q64.h"


int main()
{
  u8 buf[]="Hello world!Hello world!Hello world!rn";
  u8 rec[50] = {0};

  NVIC_SetPriorityGrouping(5);    //4层嵌套,4个响应优先级
  Usart1_Init(9600);  
  AT24C02_Init();
  W25Q64_Init();

  W25Q64_ContinueWrite(4090,buf,sizeof(buf));

  while(1)
  {    
    W25Q64_ReadBytes(4090,rec,sizeof(buf));
    printf("%s",rec);
  }
}

w25Q64

最后,编写测试函数,将程序烧入开发板中,读出的数据和写入的数据完全一致,SPI读写W25Q64成功。

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分