一、W25Q64是什么?

W25Q64是一种常见的串行闪存存储器,由Winbond公司生产。它的容量为64兆比特(8兆字节),采用SPI接口进行通信。

W25Q64主要特点包括:

1.容量大:W25Q64具有64兆比特(8兆字节)的存储容量,可以存储大量数据。

2.串行接口:W25Q64使用SPI(Serial Peripheral Interface)接口进行通信,它由四条线组成:时钟线(SCK)、数据输入线(MOSI)、数据输出线(MISO)和片选线(CS)。支持SPI模式0和模式3。在SPI模式0中,数据在时钟的下降沿采样,在上升沿传输;而在SPI模式3中,数据在时钟的上升沿采样,在下降沿传输。这两种模式的时序图可以在W25Q64的数据手册中找到。

3.高速读写:W25Q64的串行闪存存储器支持高速读取和写入操作,可达到快速的数据传输速度。

4.扇区擦除:W25Q64的存储空间被划分为多个扇区,每个扇区大小为4KB。可以通过扇区擦除来更新存储的数据。

5.低功耗:W25Q64具有低功耗特性,在待机模式下能够降低功耗,延长电池寿命。

在这里插入图片描述

二、使用步骤

1.硬件

1.1 引脚说明

在这里插入图片描述

引脚序号引脚名称引脚功能
1CS片选引脚,用于选择芯片进行通信。当该引脚被拉低时,表示选中W25Q64芯片。
2DO数据输出引脚,用于将数据从W25Q64芯片传输到主控芯片。
3WP写保护引脚,用于控制W25Q64芯片是否进入写保护状态。当WP引脚被拉低时,芯片进入写保护状态,禁止对芯片进行写操作。
4GND地引脚,用于接地。
5DI数据输入引脚,用于将数据从主控芯片传输到W25Q64芯片。
6CLK时钟引脚,用于同步数据传输的时钟信号。
7HOLDHOLD引脚用于暂停SPI通信,以便于主控芯片在需要时保持W25Q64芯片的状态。当HOLD引脚被拉低时,芯片会暂停当前的SPI通信操作,并保持之前的状态。当HOLD引脚恢复高电平时,SPI通信可以继续。
8VCC电源引脚,用于提供芯片的电源电压:2.7V~3.6V。

1.2 硬件连接

从上面的引脚说明,我们不需要使用WP引脚和HOLD引脚的功能,可以将它们直接连接到VCC电源引脚上。这样做可以确保这两个引脚保持在高电平状态,以便芯片正常工作。

连接WP和HOLD引脚到VCC上的好处是简化了连接和布线过程,节省了两个单片机IO口资源。
在这里插入图片描述

1.3 设备ID

W25Q64的芯片ID为0xEF4017。这个ID是由该芯片制造商Winbond公司指定的,用于标识该芯片型号的唯一性。

W25Q64的芯片ID是由3个字节组成,其中第1个字节为制造商ID,固定为0xEF,代表Winbond公司;第2个字节和第3个字节表示设备类型和密度,固定为0x40和0x17,分别代表64Mbit(8MB)的串行闪存存储器。因此,W25Q64的完整芯片ID为0xEF4017。

1.4 内部框架

存储结构:W25Q64通常以块(Block)、扇区(Sector)和页(Page)的形式组织存储数据。W25Q64一共有128块,每个块包含16个扇区,每个扇区包含16个页,每页最多256字节。

由此可知W25Q64容量的计算方式如下:
以1页/256字节,1扇区包含16页为基础,可以得到1扇区=16256Byte)=4096(Byte)=4KB;
又因1块包含16个扇区,可以得到1块=16
4096(Byte)=65536(Byte)=64KB;
最后总共128块,128*65536(Byte)=8388608(Byte)=8MB,所以
W25Q64寻址空间:0x000000~0x7FFFFF。

在这里插入图片描述

1.5 指令集

在这里插入图片描述

1.5.1 指令集1
//W25Q64指令表1
#define W25Q64_Write_Enable						          0x06
#define W25Q64_Write_Disable                              0x04
#define W25Q64_Read_Status_register_1				      0x05
#define W25Q64_Read_Status_register_2				      0x35
#define W25Q64_Write_Status_register				      0x01
#define W25Q64_Page_Program							      0x02
#define W25Q64_Quad_Page_Program				          0x32
#define W25Q64_Block_Erase_64KB						      0xD8
#define W25Q64_Block_Erase_32KB						      0x52
#define W25Q64_Sector_Erase_4KB						      0x20
#define W25Q64_Chip_Erase							      0xC7
#define W25Q64_Erase_Suspend					          0x75
#define W25Q64_Erase_Resume							      0x7A
#define W25Q64_Power_down							      0xB9
#define W25Q64_High_Performance_Mode				      0xA3
#define W25Q64_Continuous_Read_Mode_Reset			      0xFF
#define W25Q64_Release_Power_Down_HPM_Device_ID		      0xAB
#define W25Q64_Manufacturer_Device_ID				      0x90
#define W25Q64_Read_Uuique_ID						      0x4B
#define W25Q64_JEDEC_ID								      0x9F

在这里插入图片描述

1.5.2 指令集2
//W25Q64指令集表2(读指令)
#define W25Q64_Read_Data						          0x03
#define W25Q64_Fast_Read						          0x0B
#define W25Q64_Fast_Read_Dual_Output				      0x3B
#define W25Q64_Fast_Read_Dual_IO					      0xBB
#define W25Q64_Fast_Read_Quad_Output				      0x6B
#define W25Q64_Fast_Read_Quad_IO					      0xEB
#define W25Q64_Octal_Word_Read_Quad_IO				      0xE3

2.软件

2.1 W25Q64引脚定义代码如下(示例):

/* Defines ------------------------------------------------------------------*/
#define W25Q64_GPIO_RCC    RCC_APB2Periph_GPIOA
#define W25Q64_GPIO_Port   GPIOA
#define SPI_CS_Pin         GPIO_Pin_6//CS
#define SPI_DO_Pin         GPIO_Pin_5//MISO
#define SPI_SLK_Pin        GPIO_Pin_4//CLK
#define SPI_DI_Pin         GPIO_Pin_3//MOSI//根据实际的引脚修改

2.2 W25Q64初始化代码如下(示例):

当 CS 为高电平时,表示不选中该芯片,芯片将不会响应 SPI 总线上传来的数据。因此,为了确保在系统上电后不会意外触发芯片操作,一般会在初始化时将 CS 引脚设置为高电平状态。

/*******************************************************************************
 * 函数名:User_W25Q64_Init
 * 描述  :W25Q64初始化
 * 输入  :void
 * 输出  :void
 * 调用  :初始化
 * 备注  :
*******************************************************************************/
void User_W25Q64_Init(void)
{
    W25Q64_GPIO_Init();	
    W25Q64_DATA_Init();		
}

/*******************************************************************************
 * 函数名:W25Q64_GPIO_Init
 * 描述  :W25Q64引脚初始化
 * 输入  :void
 * 输出  :void
 * 调用  :初始化
 * 备注  :
*******************************************************************************/
void W25Q64_GPIO_Init(void)
{
	GPIO_InitTypeDef GPIO_InitStructure;
	RCC_APB2PeriphClockCmd(W25Q64_GPIO_RCC, ENABLE);
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;      
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; 
	GPIO_InitStructure.GPIO_Pin = SPI_CS_Pin | SPI_DI_Pin | SPI_SLK_Pin;		
	GPIO_Init(W25Q64_GPIO_Port, &GPIO_InitStructure);	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;   
	GPIO_InitStructure.GPIO_Pin = SPI_DO_Pin;			
	GPIO_Init(W25Q64_GPIO_Port, &GPIO_InitStructure);		
}

/*******************************************************************************
 * 函数名:W25Q64_DATA_Init
 * 描述  :W25Q64数据初始化
 * 输入  :void
 * 输出  :void
 * 调用  :初始化
 * 备注  :
*******************************************************************************/
void W25Q64_DATA_Init(void)
{
    SPI_CS_HIGH();	
}

2.3 W25Q64引脚配置代码如下(示例):

/*******************************************************************************
 * 函数名:SPI_CS_HIGH
 * 描述  :CS输出高电平
 * 输入  :void
 * 输出  :void
 * 调用  :内部调用
 * 备注  :
 *******************************************************************************/
void SPI_CS_HIGH(void)
{
    GPIO_SetBits(W25Q64_GPIO_Port,SPI_CS_Pin);	
}

/*******************************************************************************
 * 函数名:SPI_CS_LOW
 * 描述  :CS输出低电平
 * 输入  :void
 * 输出  :void
 * 调用  :内部调用
 * 备注  :
 *******************************************************************************/
void SPI_CS_LOW(void)
{
    GPIO_ResetBits(W25Q64_GPIO_Port,SPI_CS_Pin);		
}

/*******************************************************************************
 * 函数名:SPI_SLK_HIGH
 * 描述  :SLK输出高电平
 * 输入  :void
 * 输出  :void
 * 调用  :内部调用
 * 备注  :
 *******************************************************************************/
void SPI_SLK_HIGH(void)
{
    GPIO_SetBits(W25Q64_GPIO_Port,SPI_SLK_Pin);	
}

/*******************************************************************************
 * 函数名:SPI_SLK_LOW
 * 描述  :SLK输出低电平
 * 输入  :void
 * 输出  :void
 * 调用  :内部调用
 * 备注  :
 *******************************************************************************/
void SPI_SLK_LOW(void)
{
    GPIO_ResetBits(W25Q64_GPIO_Port,SPI_SLK_Pin);		
}

/*******************************************************************************
 * 函数名:SPI_SLK_HIGH
 * 描述  :SI输出高电平
 * 输入  :void
 * 输出  :void
 * 调用  :内部调用
 * 备注  :
 *******************************************************************************/
void SPI_DI_HIGH(void)
{
    GPIO_SetBits(W25Q64_GPIO_Port,SPI_DI_Pin);	
}

/*******************************************************************************
 * 函数名:SPI_DI_LOW
 * 描述  :SI输出低电平
 * 输入  :void
 * 输出  :void
 * 调用  :内部调用
 * 备注  :
 *******************************************************************************/
void SPI_DI_LOW(void)
{
    GPIO_ResetBits(W25Q64_GPIO_Port,SPI_DI_Pin);		
}

/*******************************************************************************
 * 函数名:Read_DO_Level
 * 描述  :读取DO电平
 * 输入  :void
 * 输出  :void
 * 调用  :内部调用
 * 备注  :
 *******************************************************************************/
uint8_t Read_DO_Level(void)
{
    return GPIO_ReadInputDataBit(W25Q64_GPIO_Port,SPI_DO_Pin);
}

2.4 SPI读取数据代码如下(示例):

我这里SPI读数据是采用了SPI模式0的方式,在这种模式下,数据在时钟的下降沿进行采样,并且在时钟的上升沿进行传输,最后将数据保存到Outdata变量中。

SPI模式0是指在时钟空闲时,时钟线为低电平,在读取数据时,数据从MISO引脚上升沿进行采样,这是W25Q64芯片的SPI通信方式之一。

/*******************************************************************************
 * 函数名:SPI_SendByte
 * 描述  :SPI读取数据
 * 输入  :data
 * 输出  :Outdata
 * 调用  :内部调用
 * 备注  :通过发送一个字节的数据并同时接收一个字节的数据,实现了数据的读取操作。
 *******************************************************************************/
uint8_t SPI_SendByte(uint8_t data)
{
	uint8_t i, Outdata = 0x00;	
	for (i = 0; i < 8; i ++)
	{
		if(data & (0x80 >> i))//在下降沿,把数据移到MOSI总线上
		{
			SPI_DI_HIGH();
		}
		else
		{
			SPI_DI_LOW();
		}		
		SPI_SLK_HIGH();	// 上升沿读取数据
		if (Read_DO_Level())
		{
			Outdata |= (0x80 >> i);	//掩码提取数据
		}
		SPI_SLK_LOW();	// 下降沿
	}	
	return Outdata;
}

2.5 写使能/禁止写使能代码如下(示例):

在进行写入操作之前,需要先调用写使能命令来允许写入操作,写入完成后再调用禁止写使能命令来禁止写入操作。如果不调用禁止写使能命令,则W25Q64芯片仍然处于写入状态,可能会导致数据错误或者芯片损坏。

写使能/禁止写使能的时序图在规格书上有,这里就不贴出来了。


/*******************************************************************************
 * 函数名:W25Q64_WriteEnable
 * 描述  :W25Q64发送写使能命令
 * 输入  :void
 * 输出  :void
 * 调用  :内部调用
 * 备注  :
 *******************************************************************************/
void W25Q64_WriteEnable(void)
{
	SPI_CS_LOW();
	SPI_SendByte(W25Q64_Write_Enable);
	SPI_CS_HIGH();
}

/*******************************************************************************
 * 函数名:W25Q64_WriteDisable
 * 描述  :W25Q64发送禁止写使能命令
 * 输入  :void
 * 输出  :void
 * 调用  :内部调用
 * 备注  :
 *******************************************************************************/
void W25Q64_WriteDisable(void)
{
	SPI_CS_LOW();
	SPI_SendByte(W25Q64_Write_Disable);
	SPI_CS_HIGH();
}

2.6 W25Q64 判断忙状态代码如下(示例):

该函数的作用是等待 W25Q64 芯片的忙状态结束。在进行一些与 Flash 写入相关的操作之前,需要检查 W25Q64 芯片是否处于忙状态,以确保写入操作的正确性。该函数使用 SPI 协议与 W25Q64 通信,读取状态寄存器中的信息,并检查其中的位 0 是否为 0。如果位 0 为 0,表示忙状态已经结束,函数退出;否则,继续等待。如果等待时间超过了设定的 Timeout 时间,函数将会输出错误信息并退出。

/*******************************************************************************
 * 函数名:W25Q64_WaitForBusyStatus
 * 描述  :等待 W25Q64 芯片的忙状态结束
 * 输入  :void
 * 输出  :void
 * 调用  :内部调用
 * 备注  :
 *******************************************************************************/
void W25Q64_WaitForBusyStatus(void)
{
    uint16_t Timeout = 0xFFFF;
    SPI_CS_LOW();
    SPI_SendByte(W25Q64_Read_Status_register_1);    
    while (Timeout > 0)
    {
        uint8_t status = SPI_SendByte(W25Q64_DUMMY_BYTE);
        if ((status & 0x01) == 0)// 检查忙状态是否结束
        {
            break;
        }    
        Timeout--;
        if (Timeout == 0)
        {
            printf("W25Q64 ERROR \r\n");
            break;
        }
    }    
    SPI_CS_HIGH();
}

2.7 W25Q64 扇区/块/芯片擦除代码如下(示例):

2.7.1 W25Q64 扇区擦除

W25Q64 芯片的扇区大小为 4KB


/*******************************************************************************
 * 函数名:W25Q64_SectorErase
 * 描述  :扇区擦除函数
 * 输入  :Sector_Address		
 * 输出  :void
 * 调用  :内部调用
 * 备注  :在执行写入操作前要进行擦除
 *******************************************************************************/
void W25Q64_SectorErase(uint32_t Sector_Address)
{
	W25Q64_WriteEnable();//W25Q64写使能	
	SPI_CS_LOW();	
	SPI_SendByte(W25Q64_Sector_Erase_4KB);	
	SPI_SendByte(Sector_Address >> 16);//24位扇区地址
	SPI_SendByte(Sector_Address >> 8);  
	SPI_SendByte(Sector_Address);		  
	SPI_CS_HIGH();
	W25Q64_WaitForBusyStatus();//等待 W25Q64 芯片的忙状态结束	
	W25Q64_WriteDisable();//W25Q64禁止写使能
}
2.7.2 W25Q64 块擦除

W25Q64 芯片的块大小为 64KB


/*******************************************************************************
 * 函数名:W25Q64_BlockErase
 * 描述  :块擦除函数
 * 输入  :Address		
 * 输出  :void
 * 调用  :内部调用
 * 备注  :在执行写入操作前要进行擦除
 *******************************************************************************/
void W25Q64_BlockErase(uint32_t Block_Address)
{
    W25Q64_WriteEnable();//W25Q64写使能	 
    SPI_CS_LOW();
    SPI_SendByte(W25Q64_Block_Erase_64KB);
    SPI_SendByte((Block_Address & 0xFF0000) >> 16);//24位扇区地址
    SPI_SendByte((Block_Address & 0xFF00) >> 8);
    SPI_SendByte(Block_Address & 0xFF);
    SPI_CS_HIGH();    
    W25Q64_WaitForBusyStatus();//等待 W25Q64 芯片的忙状态结束	
	W25Q64_WriteDisable();//W25Q64禁止写使能
}
2.7.3 W25Q64 芯片擦除

W25Q64 芯片的芯片大小为 8MB

/*******************************************************************************
 * 函数名:W25Q64_ChipErase
 * 描述  :芯片擦除函数
 * 输入  :void		
 * 输出  :void
 * 调用  :内部调用
 * 备注  :在执行写入操作前要进行擦除
 *******************************************************************************/
void W25Q64_ChipErase(void)
{
    W25Q64_WriteEnable();//W25Q64写使能	 
    SPI_CS_LOW();
    SPI_SendByte(W25Q64_Chip_Erase);
    SPI_CS_HIGH();
    W25Q64_WaitForBusyStatus();//等待 W25Q64 芯片的忙状态结束	
	W25Q64_WriteDisable();//W25Q64禁止写使能
}

这些擦除函数之间的不同在于擦除的范围。扇区擦除函数擦除的是单个扇区(4KB)的数据;块擦除函数擦除的是单个块(64KB)的数据;而芯片擦除函数则会擦除整个芯片(8MB)的数据。根据实际需要,选择适当的擦除函数进行操作。

注意,如果是选用芯片擦除函数,W25Q64_WaitForBusyStatus();函数中的等待时间uint16_t Timeout = 0xFFFF;需更改为uint32_t Timeout = 0xFFFFFFFF;

2.8 W25Q64页写操作代码如下(示例):

当使用 W25Q64 芯片进行页写操作时,需要考虑到写入字节数大于 256 字节和不大于 256 字节的两种情况。

如果小于等于 256 字节,直接进行一次写操作;如果大于 256 字节,则先写入前 256 字节的数据,然后循环写入剩余数据,同时需要在每次写入之前等待上次写操作完成。


/*******************************************************************************
 * 函数名:W25Q64_PageProgram
 * 描述  :W25Q64页写操作
 * 输入  :address	 要写入的起始地址
           *data	 要写入的数据缓冲区指针。
           dataSize	 要写入的数据大小,单位为字节。
 * 输出  :void
 * 调用  :内部调用
 * 备注  :
 *******************************************************************************/
void W25Q64_PageWrite(uint32_t address, uint8_t *data, uint16_t dataSize)
{
    W25Q64_WriteEnable();//W25Q64写使能    
    SPI_CS_LOW();
    SPI_SendByte(W25Q64_Page_Program);
    SPI_SendByte((address & 0xFF0000) >> 16);
    SPI_SendByte((address & 0xFF00) >> 8);
    SPI_SendByte(address & 0xFF);    
    if (dataSize <= 256)
    {
        for (uint16_t i = 0; i < dataSize; i++)
        {
            SPI_SendByte(data[i]);
        }
    }
    else
    {
        for (uint16_t i = 0; i < 256; i++)
        {
            SPI_SendByte(data[i]);
        }
        dataSize -= 256;
        data += 256;        
        // 写入剩余数据
        W25Q64_WaitForBusyStatus();//等待 W25Q64 芯片的忙状态结束	
        address += 256;        
        while (dataSize > 0)
        {
            W25Q64_WriteEnable();//W25Q64写使能 
            SPI_CS_LOW();
            SPI_SendByte(W25Q64_Page_Program);
            SPI_SendByte((address & 0xFF0000) >> 16);
            SPI_SendByte((address & 0xFF00) >> 8);
            SPI_SendByte(address & 0xFF);       
            uint16_t chunkSize = (dataSize > 256) ? 256 : dataSize;
            for (uint16_t i = 0; i < chunkSize; i++)
            {
                SPI_SendByte(data[i]);
            }       
            SPI_CS_HIGH();
            W25Q64_WaitForBusyStatus();//等待 W25Q64 芯片的忙状态结束	         
            dataSize -= chunkSize;
            data += chunkSize;
            address += chunkSize;
        }
    }    
    SPI_CS_HIGH();
    W25Q64_WaitForBusyStatus();//等待 W25Q64 芯片的忙状态结束	
	W25Q64_WriteDisable();//W25Q64禁止写使能
}

2.9 W25Q64读数据代码如下(示例):

/*******************************************************************************
 * 函数名:W25Q64_ReadData
 * 描述  :W25Q64读数据
 * 输入  :void
 * 输出  :void
 * 调用  :
 * 备注  :
 *******************************************************************************/
void W25Q64_ReadData(uint32_t address, uint8_t *data, uint16_t dataSize)
{
	uint16_t i;
    SPI_CS_LOW();
    SPI_SendByte(W25Q64_Read_Data);
    SPI_SendByte((address & 0xFF0000) >> 16);
    SPI_SendByte((address & 0xFF00) >> 8);
    SPI_SendByte(address & 0xFF);    
    for (i = 0; i < dataSize; i++)
    {
        data[i] = SPI_SendByte(W25Q64_DUMMY_BYTE);
    }    
    SPI_CS_HIGH();
}

2.10 W25Q64读取设备ID代码如下(示例):

W25Q64的芯片ID为0xEF4017,用于标识该芯片型号的唯一性。

/*******************************************************************************
 * 函数名:W25Q64_ReadID
 * 描述  :W25Q64读取设备ID
 * 输入  :*ID	存储ID的变量	
 * 输出  :void
 * 调用  :内部调用
 * 备注  :EF4017
 *******************************************************************************/
void W25Q64_ReadID(uint32_t *ID)
{
    SPI_CS_LOW();
    SPI_SendByte(W25Q64_JEDEC_ID);         // 读ID号指令
    *ID = SPI_SendByte(W25Q64_DUMMY_BYTE); // 厂商ID,默认为0xEF
    *ID <<= 8;
    *ID |= SPI_SendByte(W25Q64_DUMMY_BYTE); // 设备ID,表示存储类型,默认为0x40
    *ID <<= 8;
    *ID |= SPI_SendByte(W25Q64_DUMMY_BYTE); // 设备ID,表示容量,默认为0x17
    SPI_CS_HIGH();
}

2.11 W25Q64测试代码如下(示例):

/*******************************************************************************
 * 函数名:W25Q64_Test
 * 描述  :W25Q64测试函数
 * 输入  :void
 * 输出  :void
 * 调用  :测试
 * 备注  :
 *******************************************************************************/
void W25Q64_Test(void)
{
	uint8_t i,j;	
	W25Q64_ReadID(&W25Q64_ID);	
	OLED_ShowString(1, 1, "ID:");
	OLED_ShowString(2, 1, "W:");
	OLED_ShowString(3, 1, "R:");	
	OLED_ShowHexNum(1, 5, W25Q64_ID, 6);	
	//printf("W25Q64_ID=%d\r\n",W25Q64_ID);	
	W25Q64_SectorErase(0x000000);// 擦除扇区的起始地址
	W25Q64_PageWrite(0x000000, TestWrite, 4);// 写入数据	
	W25Q64_ReadData(0x000000, TestRead, 4);	// 读取数据		
	for(i = 0;i < 4; i++)
	{
	    OLED_ShowHexNum(2, 3+i*3, TestWrite[i], 2);
	}
	
	for(j = 0;j < 4; j++)
	{
		OLED_ShowHexNum(3, 3+j*3, TestRead[j], 2);
	}		
}

2.12 W25Q64测试结果:

我们可以在OLED显示屏看到W25Q64的ID为EF4017,测试写入和读出的数据是一样的。

在这里插入图片描述


三、总结

总的来说,使用 W25Q64 需要熟悉其相关的指令集、通信协议和时序要求,合理地设计读写操作流程,并注意处理特殊情况,比如数据跨页写入时的处理等。感谢你的观看,谢谢!

在这里插入图片描述

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐