W25QXX系列芯片闪存为空间、引脚和电源都优先的系统提供了存储解决方案,25Q系列提供的灵活性和性能远远超过串行闪存设备。他们将代码隐藏到RAM,直接从双/四SPI(XIP)执行代码以及存储语音、文本和数据的理想选择。
W25QXX芯片
- W25QXX系列中的xx表说容量,单位是Mb。
- W25QXX系列的Flash内部都是按照Page页、Sector扇区、Block快结构来进行划分的;每一页有256个字节,每个扇区有16页,一个扇区有4KB;每一块有16个扇区,也就是64KB。
- W25QXX最小的擦除单位是一个扇区,也就是说每一次擦除至少要擦除4KB
- W25QXX最大的写入单位是一页,也就是一次最多写入256个字节
以W25Q128为例,W25Q128的容量为128Mb,也就是16MB,包含256个可擦除块,4096个可擦除扇区,32768个可编程页。页面可以按照16组擦除(4KB)、128组擦除(32KB)、256组擦除(64KB)或者整个芯片擦除。其框架图如下:
芯片常用的函数如下:
// ID读取函数,返回值如下:
// 0XEF13,表示芯片型号为W25Q80
// 0XEF14,表示芯片型号为W25Q16
// 0XEF15,表示芯片型号为W25Q32
// 0XEF16,表示芯片型号为W25Q64
// 0XEF17,表示芯片型号为W25Q128
// 0XEF18,表示芯片型号为W25Q256
uint8_t W25qxx_ReadID(void){
uint8_t Temp0=0,Temp1=0;
W25qxx_cs(0);// 使能cs,处于低电平的时候表示激活状态
W25qxx_SPIReadWrite(W25X_ManufactDeviceID);//发送读取ID命令
W25qxx_SPIReadWrite(0x00);
W25qxx_SPIReadWrite(0x00);
W25qxx_SPIReadWrite(0x00);
Temp0=W25qxx_ReadWrite(W25QXX_DUMMY_BYTE);
Temp1=W25qxx_ReadWrite(W25QXX_DUMMY_BYTE);
W25qxx_cs(1);// 使能cs,处于高电平的时候表示为待机状态,芯片被禁能
if(Temp0==0xef&&Temp1>=0x10&&Temp1<=0x19){
return Temp1;
}else
return 0;
}
// 扇擦除函数
void W25qxx_EraseSector(uint32_t SectoeAddr){
W25qxx_WaitForWriteEnd();
SectorAddr=SectorAddr*W25X_SectorSize;
W25qxx_WriteEnable();
W25qxx_cs(0);
if(w25qxx.ID>=W25Q256){
W25qxx_SPIReadWrite(0x21);
W25qxx_SPIReadWrite((SectoeAddr&0xFF000000)>>24)
}else{
W25qxx_SPIReadWrite(W25X_SectorErase);
}
W25qxx_SPIReadWrite((SectorAddr & 0xFF0000) >> 16);
W25qxx_SPIReadWrite((SectorAddr & 0xFF00) >> 8);
W25qxx_SPIReadWrite(SectorAddr & 0xFF);
W25qxx_cs(1);
W25qxx_WaitForWriteEnd();
W25qxx_Delay(1);
}
// 在指定的地址开始读取指定长度的数据
// pBuffer:数据存储区
// ReadAddr:开始读取的地址
// NumByteToRead:要读取的字节数
void W25qxx_ReadBytes(uint8_t*pBuffer,uint32_t ReadAddr,uint32_t NumByteToRead){
W25qxx_cs(0);
if(w25qxx.ID>=W25Q256){
W25qxx_SPIreadWrite(0x0C);
W25qxx_SPIReadWrite((ReadAddr&0xFF000000)>>24);
}else{
W25qxx_SPIReadWrite(W25X_SectorErase);
}
W25qxx_SPIReadWrite((SectorAddr & 0xFF0000) >> 16);
W25qxx_SPIReadWrite((SectorAddr & 0xFF00) >> 8);
W25qxx_SPIReadWrite(SectorAddr & 0xFF);
W25qxx_SPIReadWrite(0);
for(uint32_t i=0;i<NumByteToRead;i++){
pBuffer[i]=W25qxx_SPIReadWrite(0xFF);//循环读
}
W25qxx_cs(1);
}
// 在指定地址开始写入不大于一页的数据,写入前要确保扇区已经擦除
// pBuffer:要写入的数据
// Page_Address:要写入的页地址(第几页
// OffsetInByte:在页内的偏移字节
//NumByteTpWrite_up_to_PageSize:要写入的字节数(最大256),该数不应该超过该页的剩余字节数
void W25qxx_WritePage(uint8_t* pBuffer,uint32_t Page_Address,uint32_t OffsetInByte,uint32_t NumByteToWrite_up_to_PageSize){
if(((NumByteToWrite_up_to_PageSize+OffsetInByte)>W25X_PageSize)||NumByteToWrite_up_to_PageSize==0){//判断要写入的数据是否合法
NumByteToWrite_up_to_PageSize=W25X_PageSize-OffsetByte;
W25qxx_WaitForWriteEnd();
W25qxx_WriteEnable();
W25qxx_cs(0);
Page_Address=(Page_Address*W25X_PageSize)+OffsetByte;
if(w25qxx.ID>=W25Q256){
W25qxx_SPIReadWrite(0x12);
W25qxx_SPIReadWrite((Page_Address&0xFF000000)>>24);
}else{
W25qxx_SPIreadWrite(W25X_PageProgram);
}
W25qxx_SPIReadWrite((Page_Address & 0xFF0000) >> 16);
W25qxx_SPIReadWrite((Page_Address & 0xFF00) >> 8);
W25qxx_SPIReadWrite(Page_Address & 0xFF);
W25qxx_SPIWrite(pBuffer, NumByteToWrite_up_to_PageSize);
W25qxx_cs(1);
W25qxx_WaitForWriteEnd();
W25qxx_Delay(1);
}
标准的SPI引脚
引脚说明
SPI初始化
#include "spi.h"
// SPI口初始化
void spi1_init(void){
RCC->APB2ENR|=1<<2;//PORTA时钟使能
RCC->APB2ENR|=1<<12;//SPI21时钟使能
GPIOA->CRL&=0x000FFFFF;
GPIOA->CRL|=0xB8B00000;//PA5 PA7复用推挽输出
GPIOA->ODR|=0x07<<5;//PA6 上拉输出
SPI1->CR1|=0<<11;//8bit数据格式
SPI1->CR1|=0<<10;//全双工模式
SPI1->CR1|=1<<9;//软件nss模式
SPI1->CR1|=1<<8;
SPI1->CR1|=0<<7;//MSB first
SPI1->CR1|=3<<3;//时钟频率为72M
SPI1->CR1|=1<<2;//SPI主机;
SPI1->CR1|=1<<1;//空闲模式下SCK为1,CPOL=1
SPI1->CR1|=1<<0;//CPHA=1
SPI1->CR1|=1<<6;//SPI设备使能
}
//spi1发送数据
void spi1_send_byte(u8 data){
//等待前面的发送完毕
while((SPI1->SR&(1<<1))==0);//发送区空
//把数据给DR寄存器
SPI1->DR;//这一步指定完并不会立即传输完成
while((SPI1->SR&(1<<0))==0);//等待接收完成,spi是全双工的,发送一个字节必然接收一个字节
data=SPI1->DR;//读取接收到的无用的数据
}
//spi1接收数据,返回值表示接收到的数据
u8 spi1_receive_byte(void){
u8 data;
while((SPI1->SR&(1<<1))==0);//发送区空
SPI1->DR=0xaa;//传数据给DR,这个数据是一个假数据,只是为了维持时钟信号,本身并不重要
while((SPI1->SR&(1<<0))==0);//等待接收完成
data=SPI1->DR;//读取数据
return data;
}
//读写数据,其中data表示要写入的字节,返回值表示读到的字节
u8 spi1_read_write_byte(u8 data){
while((SPI1->SR&(1<<1))==0);//发送区空
SPI1->DR=data;
while((SPI1->SR&(1<<0))==0);//接收区空
return SPI1->DR;
}
W25QXX
W25QXX控制和状态寄存器
芯片本身是带有控制和状态寄存器的,要控制他读写,就需要控制操作寄存器来编写代码
-
BUSY 忙位:是一个只读位,位于状态寄存器中的S0,当器件在执行“页编程” “扇区擦除” “快区擦除” “芯片擦除” “写状态寄存器” 指令时,该位自动置1,这个时候,除了“读状态寄存器”指令,其他的指令都忽略。当编程、擦除和写状态寄存器指令执行完毕之后,这个寄存器自动变成0,表示芯片可以接收其他的指令。
-
WEL 写保护位:写保护位是一个只读位2,位于状态寄存器的S1,执行完“写使能”后该位置置位为1,当芯片处于“写保护状态下”,该位为0。在下面两种情况下会进入到写保护状态:
掉电后
执行以下指令:写禁能、页编程、扇区擦除、块区擦除、芯片擦除等这个寄存器的第0位和第1位都是只读位,在执行操作的时候才会置位为1,平常的时候默认是0
-
BP0 BP1 BP2块保护位:BP是可读可写位,分布在状态寄存器的2-4位,可以使用写状态寄存器命令置位这些块区保护位,在默认状态下,这些位都是0,即在块区处于未保护状态下,可以设置块区没有保护、部分保护或者全部处于保护状态下。当SPR位为1或者/WP引脚为低的时候,这些位不可以被更改。
-
SRP 状态寄存器保护位:SRP是可读可写位,位于状态寄存器的S7。该位结合/WP引脚可以实现禁能写状态寄存器的功能。这个位的默认值是0。当SRP=1,/WP=0的时候,写状态寄存器命令失效。
W25QXX指令
数据传输高位在前,带括号的数据表示数据从DO引脚读出
1)写使能 06h
写使能指令会将状态寄存器的WEL位置位,在执行每个“页编程” “扇区擦除” “块区擦除” “芯片擦除” 和“写状态寄存器”命令之前,都要先置位WEL
其代码和逻辑如下:
- 先拉低片选引脚
- 通过spi协议编写的读写函数,把0x06的指令发送过去
- 拉高片选
//写使能
vois W25QXX_WriteEnable(void){
W25QXX_CS=0;
spi1_read_write_byte(W25QXX_WriteEnable);
W25QXX_CS=1;
}
2)读状态寄存器指令 05h
当/CS拉低后,开始把05h从DIO引脚送到芯片,在CLK的上升沿数据被芯片采集,当芯片认出采集到的数据是05h的时候,芯片就会把“状态寄存器”的值从DO引脚输出,输出在CLK的下降沿输出,高位在前。
“读状态寄存器”指令在任何时候都可以用,甚至在编程、擦除和写状态寄存器的过程中也可以用,这样,就可以从状态几孙琦的BUSY位判断编程、擦除和写状态寄存器周期有没有结束,从而让我们知道是否可以接收下一条指令了。如果CS没有被拉高,状态寄存器的值一直都从DO引脚输出,CS被拉高后,指令结束。
读状态寄存器指令逻辑以及代码如下:
- 拉低片选引脚
- 通过SPI协议编写的速写函数,把0x05的指令发过去
- 从机返回一个随机数据,这里我们读取一个0xff
- 拉高片选,完成度状态寄存器的指令函数
u8 W25QXX_ReadSR(void){
u8 byte=0;
//拉低片选引脚,使能器件
W25QXX_CS=0;
//发送读取状态寄存器命令,把0x55发送过去
spi1_read_write_byte(W25QXX_ReadStatusReg);
//读取一个字节
byte=spi1_read_write_byte(0xff);
//取消片选
W25QXX_CS=1;
return byte;
}
3)写状态寄存器命令 01h
在执行“写状态寄存器”指令之前,需要先执行“写使能”指令。先拉低CS引脚,然后把01h从DIO引脚送到芯片,然后在把你想要设置的状态寄存器的值,通过DIO引脚送到芯片,拉高CS引脚,指令结束。如果CS没有被拉高或者拉的很晚,值就不会被写入,指令无效。
通过对TB BP2 BP1 BP0位写1,可以实现将芯片的部分或者全部存储区域设置为只读,通过对SRP写1,WP写0,可以实现禁止写入状态寄存器的功能。
其逻辑以及代码如下:
- 拉低引脚
- 通过spi协议编写好的读写函数,把0x01指令发送过去
- 写好我们需要发送的数据
- 拉高片选
u8 W25QXX_Write_SR(u8 sr){
W25QXX_CS=0;
spi1_read_write_byte(W25QXX_WriteStatusReg);//发送写读取状态寄存器命令
spi1_read_write_byte(sr);//写入一个字节
W25QXX_CS=1;
}
4)页编程指令 02h
我们要把数据写入到flash,就㤇用到这个页编程指令,在页内写入数据,写这个函数要用到上面的读写函数。
- 执行“页编程”指令之前,要先执行“写使能”指令,并且要求写入的区域位都为1,也就是需要先把写入的区域擦除。然后先把cs拉低,把代码02h通过DIO引脚送到芯片,然后把24位地址送到芯片,接着把要写的字节写到芯片,写完后把cs拉高,完成。
- 写完一页,也就是256个字节后,必须把地址改为0,不然的话,如果时钟还在继续,地址将会自动变成页的开始地址。某一些时候,需要写入的字节不足256个字节的话,其他写入的字节是没有意义的,如果写入的字节大于256个字节的话,多余的字节将会加上无用的字节覆盖掉刚刚写入的256个字节,所以在页编程中,要保证写入的字节小于等于256个字节。
- 在指令执行过程中,用“读状态寄存器”可以发现BUSY位为1,当指令执行完毕,BUSY位会自动变成0。但是如果写入的地址处于“写保护”状态,“页编程”指令无效。
为什么要发送24位地址
首先我们要知道SPI每次只能发送8位地址,所以这个24位地址需要分三次发送;这个24位的地址,本质上就是数据存放的地址,为什么是24位,这个是在手册里面固定的。比如说需要发送0x123456:
发送完24位地址之后,就可以发送字节,最大只能发送256个字节,超过就不行了
其代码以及逻辑如下:
- 首先写使能,使用刚刚的写使能函数
//页编程
//pBuffer:数据存储区
//WriteAddr:开始写入的地址
//NumByteToWrite:要写入的字节数,最大256字节
void W25QXX_Write_Page(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite){
u16 i;
W25Qxx_Write_Enable();//set WEL
W25QXX_CS=0;
spi1_read_write_byte(W25QXX_PageProgram);//发送页命令
//发送24bit地址
spi1_read_write_byte((u8)((WriteAddr)>>16));
spi1_read_write_byte((u8)((WriteAddr)>>8));
spi1_read_write_byte((u8)(WriteAddr);
for(i=0;i<NumByteToWrite;i++){
spi1_read_write_byte(pBuffer[i]);//循环写入数据
}
W25QXX_CS=1;//拉高片选
W25QXX_Wait_Busy();//等待写入结束
}
//等待写入结束函数
void W25QXX_Wait_Busy(void){
while((W25QXX_ReadSR()&0x01)==0x01);//等待BUSY位清空,等于0说明已经写完
}
5)读数据寄存器 03h
这个时序要注意,如果使用硬件SPI功能,一般需要主机先发送一个字节数据后才能接收到flash发送的数据,实际上是为了给flash提供发送的时钟(因为时钟只能由主机产生)
“读数据”指令允许读出一个或以上的字节,先把cs拉低,然后把03h通过DIO引脚送到芯片,之后再总入24位的地址,这些数据在CLK的上升沿被芯片采集。芯片接收完24位地址后,就会把相应地址的数据在CLK引脚的下降沿从DO引脚送出去,高位在前。当读完这个地址的数据之后,地址自动增加,然后通过DO引脚把下一个地址的数据送出去,形成一个数据流。也就是说,只要时钟在工作,通过一条读指令,就可以把整个芯片存储区的数据读出来。把cs引脚拉高,“读指令结束。当芯片在执行变成、擦除和读状态寄存器指令的周期内,读指令不起作用。
其代码以及逻辑如下:
//读数据指令
//pBuffer:数据存储区
//ReadAddr:开始读取的地址
//NumByteToRead:要读取的字节数,最大32768
void W25QXX_Read(u8* pBUffer,u32 read_address, u16 NumByteToRead){
u16 i;
W25QXX_CS=0;
spi1_read_write_byte(W25QXX_ReadData);//发送读取命令
spi1_read_write_byte((u8)(read_address>>16));//发送24bit读取地址
spi1_read_write_byte((u8)(read_address>>8));
spi1_read_write_byte((u8)read_address);
for(i=0;i<NumByteToRead;i++){
buffer[i]=spi1_read_write_byte(0xff);//循环读取数据,发送空字节以获取时钟信号
}
W25QXX_CS=1;
}
6)扇区擦除指令 20h
“扇区擦除指令”将一个扇区4K字节全部擦除,擦除后的扇区位都为1,扇区字节都为ffh。在执行“扇区擦除”指令之前,要先执行“写使能”指令,保证WEL为为1
先拉低cs引脚,然后把指令代码20h通过DIO引脚送到芯片,然后接着把24位扇区地址送到芯片,拉高cs引脚。如果没有及时把cs引脚拉高,指令将不会起作用。在指令执行期间,BUSY位为1,可以通过“读状态寄存器”指令观察。当指令执行完毕,BUSY位变成0,WEL位也会变成0.如果需要擦除的地址处于只读状态,指令将不会起作用。
//擦除扇区指令
//Dst_Addr:扇区编号,是第几个扇区,不是绝对地址
//擦除一个扇区的最少时间:150ms
void W25QXX_Erase_Sector(u32 Dst_Addr){
Dst_Addr*=4097;
W25QXX_WriteEnable();//写使能
W25QXX_Wait_Busy();//等待空闲
W25QXX_CS=0;//使能器件
spi1_read_write_byte(W25QXX_SectorErase);//发送扇区擦除指令
spi1_read_write_byte((u8)(Dst_Addr>>16));//发送地址
spi1_read_write_byte((u8)(Dst_Addr>>8))
spi1_read_write_byte((u8)Dst_Addr);
W25QXX_CS=1;//取消片选
W25QXX_Wait_Busy();//等待擦除完成
}
7)块擦除指令 D8h
块擦除指令是将一个块区64K全部变成1,即字节全部变成ffh,在块擦除指令执行之前要先执行“写使能”指令
先把cs拉低,然后把指令代码D8h通过DIO引脚送入到芯片,然后把24位块区地址送到芯片,把地址拉高。
//擦除块指令
//Addr:块区地址
void W25QXX_Erase_Block(u32 Addr){
W25QXX_WriteEnable();//写使能
W25QXX_Wait_Busy();//等待空闲
W25QXX_CS=0;//使能器件
spi1_read_write_byte(W25QXX_BlockErase);//发送扇区擦除指令
spi1_read_write_byte((u8)(Dst_Addr>>16));//发送地址
spi1_read_write_byte((u8)(Dst_Addr>>8))
spi1_read_write_byte((u8)Dst_Addr);
W25QXX_CS=1;//取消片选
W25QXX_Wait_Busy();//等待擦除完成
}
8)芯片擦除指令 C7h
芯片擦除指令会使得整个芯片的存储区变成1,也就是所有字节变成ffh,在执行“芯片擦除”指令之前要先执行“写使能”指令
先把cs拉低,把指令代码C7h通过DIO引脚送入到芯片中,然后拉高cs引脚
//擦除整个芯片,需要比较长的等待时间
void W25QXX_Erase_Chip(void){
W25QXX_WriteEnable();//ser WEL
W25QXX_Wait_Busy();
W25QXX_CS=0;
spi1_read_write_byte(W25QXX_ChipErase);//发送片擦除指令
W25QXX_CS=1;
W25QXX_Wait_Busy();//等待芯片擦除完成
}
9)掉电指令 B9h
尽管在待机状态下的电流小韩很低,但是掉电指令可以使得待机电流消耗更低。
先把CS拉低,把指令代码B9h通过DIO引脚送到芯片,然后把cs拉高,指令执行完毕,如果没有及时拉高,指令无效。执行完掉电指令后,除了“释放掉电/器件ID指令”,其他指令无效
//进入掉电模式
void W25QXX_PowerDown(void){
W25QXX_CS=0;
spi1_read_write_byte(W25QXX_PowerDown);//发送掉电命令
W25QXX_CS=1;
delay_us(3);//等待TPD
}
10)读ID指令 90h
因为我们这个W25QXX,这个程序大部分都适用,所以当我们使用不同的flash芯片的时候,我们可以通过读取ID来选到自己对应的芯片
“读制造/器件号”指令不同于“释放掉电/器件ID指令”,“读制造/器件号”指令读出来的数据包括JEDDC标准制造号和特殊器件ID号。
先把cs拉低,然后把指令90h通过引脚DIO送到芯片,然后接着把24位地址000000h送到芯片,然后芯片就会先后把“生产ID”和“器件ID”通过DO引脚在CLK的上升沿发送出去,如果把24位地址谢伟000001h,ID号的发送顺序就会颠倒,也就是先发了“器件号”再发“生产号”,ID号是8位数据。
//读芯片ID
//返回值如下:
//0XEF13,表示芯片型号为W25Q80
//0XEF14,表示芯片型号为W25Q16
//0XEF15,表示芯片型号为W25Q32
//0XEF16,表示芯片型号为W25Q64
//0XEF17,表示芯片型号为W25Q128
u16 W25QXX_ReadID(void){
u1616 Temp=0;
W25QXX_CS=0;
spi1_read_write_byte(0x90);//发送读取ID命令
spi1_read_write_byte(0x00);//发送24bit地址
spi1_read_write_byte(0x00);
spi1_read_write_byte(0x00);
Temp|=spi1_read_write_byte(0xff)<<8;
Temp|=spi1_read_write_byte(0xff);
W25QXX_CS=1;
return Temp;
}