W25Qxx - FLASH大容量存储芯片
芯片介绍/引脚介绍
W25Q64 (64M-bit),W25Q16(16M-bit)和 W25Q32(32M-bit)是为系统提供一个最小的空间、引脚和功耗的存储器解决方案的串行 Flash 存储器。25Q 系列比普通的串行 Flash 存储器更灵活,性能更优越。基于双倍/四倍的 SPI,它们能够可以立即完成提供数据给 RAM,包括存储声音、文本和数据。芯片支持的工作电压 2.7V 到 3.6V,正常工作时电流小于 5mA,掉电时低于 1uA。
W25Q64/16/32 由每页 256 字节组成。每页的 256 字节用一次页编程指令即可完成。每次可以擦 除 16 页(1 个扇区)、128 页(32KB 块)、256 页(64KB 块)和全片擦除。
W25Q64 的内存空间结构:一页 256 字节,4K(4096 字节)为一个扇区,16 个扇区为 1 块,容量为 8M 字节,共有 128 个块,2048 个扇区。
W25Q64/16/32 支持标准串行外围接口(SPI),和高速的双倍/四倍输出,双倍/四倍用的引脚:串行时钟、片选端、串行数据 I/O0(DI)、I/O1(DO)、I/O2(WP)和 I/O3(HOLD)。SPI 最高支持 80MHz,当用快读双倍/四倍指令时,相当于双倍输出时最高速率 160MHz,四倍输出时最高速率 320MHz。这个传输速率比得上 8 位和 16 位的并行 Flash 存储器。
HOLD 引脚和写保护引脚可编程写保护。此外,芯片支持 JEDEC 标准,具有唯一的 64 位识别序列号。
该存储芯片由SPI通信驱动,关于SPI通信相关内容,详见协议使用笔记SPI章节。
引脚排列(以 SOIC 208-mil 为例)
CS - 片选
DO - SPI数据输出,当状态寄存器2的QE位设置为四倍I/O时,此时这个引脚被用为IO1
WP - 写保护输入,当状态寄存器2的QE位设置为四倍I/O时,则WP(硬件写保护)功能不可用,因为此时这个引脚被用为IO2
HOLD - Hold输入,当状态寄存器2的QE位设置为四倍I/O时,则HOLD引脚功能不可用,因为此时这个引脚被用为IO3
CLK - 时钟
DI - SPI数据输入
**注意:**本笔记采用标准SPI模式,设备数据传输是从高位开始,数据传输的格式为 8bit,数据采样从第二个时间边沿开始,空闲状态时,时钟线 clk 为高电平。
部分命令
常用指令由上图所示,不带括号表示主机发送,带括号表示主机接收,下面解释几个命令。
写使能 - 06H
在进行写相关操作(如写数据、擦除(写0xFF))时,必须完成:拉低片选 - 发送写使能命令 - 拉高片选 后才可进行写操作。
读数据 - 03H(表中未列出)
指令格式:| 03h | A23-A16 | A15-A8 | A7-A0 | (D7-D0)… |
使用该指令在读取字节数据后,其地址会自动增1,因此只需要在开始时设置一次地址便可连续读取数据。
读状态寄存器1 - 05H
寄存器内容:
7 6 5 4 3 2 1 0 SRP0 SEC TB BP2 BP1 BP0 WEL BUSY 我们主要关注该寄存器第0位 - 忙状态位:
当该位为1时,表示芯片忙,此时无法对其进行任何操作,需等待忙结束时,才可继续操作
页编程 - 02H
页编程指令允许从一个字节到 256 字节的数据编程(一页)(编程之前必须保证内存空间是 0XFF)
**注意:**使用页写时,写入字节数最少为1个,最多为256个。每次写入至多一页(256字节)数据,若写入数据量大于该页内剩余空间,则其将从该页头部继续写入,直至写完数据为止,因此,在页写过程中务必检查剩余字节数是否足够写入!
扇区擦除 - 20H
该芯片一次至少擦除一个扇区(4KB),擦除即将其中的内容写为0xFF,为写入数据做准备,由于其一次擦除数据偏大,因此在擦除前应检查该扇区内是否存在有用数据,以此进行备份保护。
代码示例
变量介绍
#define W25QXX_CS //片选
#define W25QXX_MOSI //主出从入
#define W25QXX_MISO //主入从出
#define W25QXX_SCK //时钟
u8 W25QXX_SendReceive_Byte(u8 tdata) //SPI发送接收
写使能
void W25Q64_Write_Enable(void)
{
SPI_CS = 0;
W25QXX_SendReceive_Byte(0x06); //命令0x06
SPI_CS = 1;
}
设备探忙
u8 W25QXX_DeviceBusy(void)
{
u8 deviceInfo = 0;
W25QXX_CS = 0;
W25QXX_SendReceive_Byte(0x05); //命令0x05
deviceInfo = W25QXX_SendReceive_Byte(None); //返回值,我们需要判断返回值最低位
W25QXX_CS = 1;
return (deviceInfo & 1);
}
字符读取
void W25QXX_Read(u32 StartADDR,u32 length,u8 *str)
{
u32 i;
while(W25QXX_DeviceBusy());
W25QXX_CS = 0;
W25QXX_SendReceive_Byte(0x03); //读取命令0x03
W25QXX_SendReceive_Byte(StartADDR >> 16); //24位地址,从高位开始
W25QXX_SendReceive_Byte(StartADDR >> 8);
W25QXX_SendReceive_Byte(StartADDR >> 0);
for(i=0;i<length;i++) //读取length长的字节
{
*str = W25QXX_SendReceive_Byte(None);
str++;
}
W25QXX_CS = 1;
}
扇区擦除
void W25QXX_Sector_Erase(u32 StartADDR)
{
W25Q64_Write_Enable();
while (W25QXX_DeviceBusy());
W25QXX_CS = 0;
W25QXX_SendReceive_Byte(0x20); //擦除命令
W25QXX_SendReceive_Byte(StartADDR >> 16);
W25QXX_SendReceive_Byte(StartADDR >> 8);
W25QXX_SendReceive_Byte(StartADDR >> 0);
W25QXX_CS = 1;
while (W25QXX_DeviceBusy());
}
字符写入
//页内写
void W25QXX_Write_Page(u32 StartADDR,u32 length,const u8 *str)
{
u16 i;
W25Q64_Write_Enable(); //写使能要单独拎出来写一次,在片选拉低之前写
while (W25QXX_DeviceBusy());
W25QXX_CS = 0;
W25QXX_SendReceive_Byte(0x02);
W25QXX_SendReceive_Byte(StartADDR >> 16);
W25QXX_SendReceive_Byte(StartADDR >> 8);
W25QXX_SendReceive_Byte(StartADDR >> 0);
for(i=0;i<length;i++)
{
W25QXX_SendReceive_Byte(*str);
str++;
}
W25QXX_CS = 1;
while (W25QXX_DeviceBusy());
}
//跨页写
void W25QXX_Write_CrossPage(u32 StartADDR,u32 length,const u8 *str)
{
u16 offset = StartADDR % 256;
u16 remain = 256 - offset;
if(remain > length) remain = length;
while (1)
{
W25QXX_Write_Page(StartADDR,remain,str);
if(remain == length) break;
StartADDR += remain;
str += remain;
length -= remain;
if(length > 256) remain = 256;
else remain = length;
}
}
//写字符串
u8 tempData[4096];
void W25QXX_Write_String(u32 StartADDR,u32 length,const u8 *str)
{
u32 i;
u16 SecOffset = StartADDR % 4096; //在当前扇区内的偏移量
u16 SectorRemain = 4096 - SecOffset; //计算扇区剩余容量
u16 SecAddr = StartADDR - SecOffset; //找到扇区起始位置
if(SectorRemain > length) SectorRemain = length;
while (1)
{
W25QXX_Read(StartADDR,SectorRemain,(tempData + SecOffset));
for(i = 0; i < SectorRemain; i++)
{
if(tempData[SecOffset + i] != 0xff) break; //有数据
}
if(i != SectorRemain)
{
//把当前扇区的前半截数据保存缓冲区
W25QXX_Read(SecAddr, SecOffset, tempData);
//把当前扇区的后半截数据保存缓冲区
W25QXX_Read(StartADDR+SectorRemain, 4096-(SecOffset+SectorRemain), tempData+(SecOffset+SectorRemain));
//擦除扇区
W25QXX_Sector_Erase(SecAddr);
for(i=0;i<SectorRemain;i++)
{
tempData[SecOffset+i] = str[i];
}
W25QXX_Write_CrossPage(SecAddr, 4096, tempData);
SecOffset = 0; //扇区内偏移量归零
}
else W25QXX_Write_CrossPage(StartADDR, SectorRemain, str);
if(SectorRemain == length) break;
StartADDR += SectorRemain;
str += SectorRemain;
length -= SectorRemain;
SecAddr = StartADDR; //更新扇区起始地址
if(length > 4096) SectorRemain = 4096;
else SectorRemain = length;
}
}