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 | CS | 片选引脚,用于选择芯片进行通信。当该引脚被拉低时,表示选中W25Q64芯片。 |
2 | DO | 数据输出引脚,用于将数据从W25Q64芯片传输到主控芯片。 |
3 | WP | 写保护引脚,用于控制W25Q64芯片是否进入写保护状态。当WP引脚被拉低时,芯片进入写保护状态,禁止对芯片进行写操作。 |
4 | GND | 地引脚,用于接地。 |
5 | DI | 数据输入引脚,用于将数据从主控芯片传输到W25Q64芯片。 |
6 | CLK | 时钟引脚,用于同步数据传输的时钟信号。 |
7 | HOLD | HOLD引脚用于暂停SPI通信,以便于主控芯片在需要时保持W25Q64芯片的状态。当HOLD引脚被拉低时,芯片会暂停当前的SPI通信操作,并保持之前的状态。当HOLD引脚恢复高电平时,SPI通信可以继续。 |
8 | VCC | 电源引脚,用于提供芯片的电源电压:2.7V~3.6V。 |
从上面的引脚说明,我们不需要使用WP引脚和HOLD引脚的功能,可以将它们直接连接到VCC电源引脚上。这样做可以确保这两个引脚保持在高电平状态,以便芯片正常工作。
连接WP和HOLD引脚到VCC上的好处是简化了连接和布线过程,节省了两个单片机IO口资源。
W25Q64的芯片ID为0xEF4017。这个ID是由该芯片制造商Winbond公司指定的,用于标识该芯片型号的唯一性。
W25Q64的芯片ID是由3个字节组成,其中第1个字节为制造商ID,固定为0xEF,代表Winbond公司;第2个字节和第3个字节表示设备类型和密度,固定为0x40和0x17,分别代表64Mbit(8MB)的串行闪存存储器。因此,W25Q64的完整芯片ID为0xEF4017。
存储结构:W25Q64通常以块(Block)、扇区(Sector)和页(Page)的形式组织存储数据。W25Q64一共有128块,每个块包含16个扇区,每个扇区包含16个页,每页最多256字节。
由此可知W25Q64容量的计算方式如下:
以1页/256字节,1扇区包含16页为基础,可以得到1扇区=16256Byte)=4096(Byte)=4KB;
又因1块包含16个扇区,可以得到1块=164096(Byte)=65536(Byte)=64KB;
最后总共128块,128*65536(Byte)=8388608(Byte)=8MB,所以
W25Q64寻址空间:0x000000~0x7FFFFF。
//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
//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
/* 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//根据实际的引脚修改
当 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();
}
/*******************************************************************************
* 函数名: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);
}
我这里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;
}
在进行写入操作之前,需要先调用写使能命令来允许写入操作,写入完成后再调用禁止写使能命令来禁止写入操作。如果不调用禁止写使能命令,则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();
}
该函数的作用是等待 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();
}
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禁止写使能
}
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禁止写使能
}
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;
当使用 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禁止写使能
}
/*******************************************************************************
* 函数名: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();
}
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();
}
/*******************************************************************************
* 函数名: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);
}
}
我们可以在OLED显示屏看到W25Q64的ID为EF4017,测试写入和读出的数据是一样的。
总的来说,使用 W25Q64 需要熟悉其相关的指令集、通信协议和时序要求,合理地设计读写操作流程,并注意处理特殊情况,比如数据跨页写入时的处理等。感谢你的观看,谢谢!