一、W25Q64相关介绍
1.总体说明:
W25Q64需在2.7V到3.6V之间的电压运行,W25Q64JV阵列被组织成32,768个可编程页,每个页有256字节。一次最多可以编程256个字节。页面可分为16组(4KB扇区清除)、128组(32KB块删除)、256组(64KB块删除)或整个芯片(芯片清除)。W25Q64JV分别有2,048个可擦除扇区和128个可擦除块。
W25Q64通过SPI兼容总线访问,该总线包括四个信号:串行时钟(CLK)、芯片选择(/CS)、串行数据输入(DI)和串行数据输出(DO)。标准SPI指令使用DI输入引脚将指令、地址或数据串行写入CLK上升边缘的设备。DO输出引脚用于从CLK下降边缘的设备读取数据或状态。W25Q64支持SPI总线运行模式0(0、0)和3(1、1)。本文采用模式0的方式驱动,具体传输方式后文介绍。
2.以8脚封装为例,引脚图及定义如下:
3.结构方框图
如上图所示,W25Q64的整体由Block0-Block127共128个块组成,每个块都是64KB的大小,每一块的地址可以表示为:0xXX0000h-0xXXFFFFh;
每个块又由16个扇区组成:Sector0-Sector15,每个扇区的大小为4KB,每一个扇区的地址可以表示为:0xXXX000h-0xXXXFFFh;
每一个扇区由16页组成,每一页大小为256个字节,地址表示:0xXXXX00h-0xXXXXFFh。W25Q64内存大小一共是:256*16*16*128/1024/1024 = 8MB
4.标准指令集
程序中我们需要用到下表中的指令来实现相应的功能,如写使能是发送0x06指令。
5.状态寄存器1
上图所示是状态寄存器1的八位的详细信息,BUSY是状态寄存器1最低位(S0)中的只读位,在设备执行页面程序、四页程序、扇区擦除、块擦除、芯片擦除、写入状态寄存器或擦除/程序安全寄存器指令时设置为1状态。在此期间,设备将忽略除读取状态寄存器和擦除/程序暂停指令外的其他指令。当程序、擦除或写入状态/安全寄存器指令完成后,BUSY位将被清除为0状态,表明设备已经准备好接受进一步的指令。所以在每次执行完页面程序、四页程序、扇区擦除、块擦除、芯片擦除、写入状态寄存器或擦除/程序安全寄存器指令时需要在其末尾加一个等待BUSY是否清除为0的函数,如果BUSY为1时发送读写操作是不会被执行的。等待BUSY函数也可以放在各个读写操作函数的头部,具体看个人喜好和项目需求。
6.四种传输模式
6.1四种模式介绍
SPI有四种传输模式,如下表所示,主要差别在于CPOL和CPHA的不同。
CPOL(Clock Polarity,时钟极性)表示SCK在空闲时为高电平还是低电平。当CPOL=0,SCK空闲时为 低电平,当CPOL=1,SCK空闲时为高电平。
6.2 模式0具体介绍
如下图,模式0开始前,SS是高电平,SCK是低电平,当SS变为低电平时表示传输开始了。该模式在SCK的第一个边沿就开始将数据移入W25Q64和单片机,第二个边沿将数据从单片机和W25Q64移出到数据缓冲区,是先有时钟边沿的产生才有数据的移入移出。所以写程序的逻辑是先将SS拉低(开始传输),再把要写入的数据(一个字节大小)的最高位(B7)移出给MOSI,W25Q64端的传输不用我们写程序控制。接着拉高SCK,这时第一个SCK边沿产生了,单片机通过读取MISO来采样数据,然后将SCK拉低,单片机开始移出数据的次低位......直到数据的最低位被移出,MISO采样到8位数据。根据下图,完成操作后,将SS拉高,(SCK交换完就是低电平,所以不用操作)这样就完成了一个字节的交换。因为大多数命令需要交换不止一个字节,所以最后的SS拉高可以不用封装在交换字节的函数里,单独封装,等传输完以后调用一次就好了。
7.几种常用指令的时序图
7.1读取设备ID(0x9F)
出于兼容性的原因,W25Q64提供了几条指令来以电子方式确定设备的身份。Read JEDEC ID指令与2003年采用的SPI兼容串行内存的JEDEC标准兼容。通过降低/CS引脚并移动指令代码“9Fh”来启动指令。JEDEC为Winbond(EFh)分配的制造商ID字节和两个设备ID字节,内存类型(ID15-ID8)和容量(ID7-ID0)首先移到最重要的下降边,最显著位(MSB),如下图所示。先用6.2中的交换字节函数发送一个0x9F给W25Q64,再连续接收三个字节的设备信息,然后结束。
7.2写使能(0x06)
写使能指令将状态寄存器中的写入启用锁存器(WEL)位设置为1。WEL位必须在每个页面程序、四页程序、扇区擦除、块擦除、芯片擦除、写状态寄存器和擦除/程序安全寄存器指令之前都设置为1。每个页面程序、四页程序、扇区擦除、块擦除、芯片擦除、写状态寄存器和擦除/程序安全寄存器指令执行完之后WEL位会自动置为0,不需要我们写写失能。
7.3扇区擦除(0x20)
扇区擦除指令将指定扇区(4k字节)内的所有内存设置为擦除状态(FFh)。在设备接受扇区清除指令之前,必须执行写入使能指令。扇区擦除指令序列如下图所示。当扇区擦除周期正在进行时,仍然可以访问读取状态寄存器指令,以检查BUSY位的状态。BUSY位在扇区擦除周期中为1,当扇区擦除周期结束,设备准备再次接受其他指令时变为0。
7.4页编程(0x02)
执行页编程指令之前,需要先对该页所对应的扇区进行扇区擦除操作,不然会造成数据写入出错。在设备接受页编程指令之前,必须执行写使能指令。指令通过将/CS引脚调低,然后发送指令码“02h”,然后是一个24位地址(A23-A0)和至少一个数据字节转移到DI引脚。当数据发送到设备时,整个指令长度的/CS引脚必须保持低电平。页面程序指令序列如图29所示。
如果要编程一个整个256字节的页面,则最后一个地址字节(8个最不重要的地址位)应设置为0。如果最后一个地址字节不是零,且时钟数超过了剩余的页面长度,则该寻址将包装到页面的开头。在某些情况下,可以编程的长度小于256个字节(一个部分页面),而不会对同一页面内的其他字节产生任何影响。执行部分页面程序的一个条件是,时钟的数量不能超过剩余的页面长度。如果发送到设备超过256个字节,寻址将包装到页面的开头,并覆盖以前发送的数据。
7.5读数据(0x03)
读取数据指令允许从内存中顺序读取一个或多个数据字节。通过将/CS引脚驱动到较低,然后将指令代码“03h”和一个24位地址(A23-A0)移到DI引脚中来启动该指令。代码和地址位被锁在CLK引脚的上升边缘。在接收到地址后,寻址存储器位置的数据字节将首先在CLK下降边缘的DO引脚上向外移动,具有最显著位(MSB)。在每个数据字节被移出后,该地址会自动增加到下一个更高的地址,从而允许一个连续的数据流。这意味着,只要时钟继续运行,就可以用一条指令访问整个内存。该指令通过驾驶/CS高完成。
读取数据指令序列如下图14所示。如果在擦除、程序或写入周期中发出读数据指令(BUSY=1),该指令将被忽略,并且不会对当前周期产生任何影响。读取数据(03h)指令仅支持在标准SPI模式下。
8.注意事项
8.1写入操作时:
1)写入操作前,必须先进行写使能;
2)每个数据位只能由1改写为0,不能由0改写为1;
3)写入数据前必须先擦除,擦除后,所有数据位变为1;
4)擦除必须按最小擦除单元进行 连续写入多字节时,最多写入一页的数据,超过页尾位置的数据,会回到页首覆盖写入;
5)写入操作结束后,芯片进入忙状态,不响应新的读写操作。
8.2读取操作时:
1)直接调用读取时序,无需使能,无需额外操作,没有页的限制,读取操作结束后不会进入忙状态,但不能在忙状态时读取。
二、软件操作
1.CubeMX设置
下图为我使用的开发板Flash部分原理图,MCU是STM32F103C8T6,Flash是W25Q64。
按照上面的原理图,配置GPIO如下,其他的部分就不一一贴图了。
2.程序部分
2.1 SPI.c
2.1.1 GPIO写入和读出状态
void SPI_W_SS(uint8_t BitValue)
{
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_15, BitValue?GPIO_PIN_SET:GPIO_PIN_RESET);
}
void SPI_W_SCK(uint8_t BitValue)
{
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_3, BitValue?GPIO_PIN_SET:GPIO_PIN_RESET);
}
void SPI_W_MOSI(uint8_t BitValue)
{
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_5, BitValue?GPIO_PIN_SET:GPIO_PIN_RESET);
}
uint8_t SPI_R_MISO(void)
{
return HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_4);
}
2.1.2 SPI的开始和结束信号
void MySPI_Start(void)
{
SPI_W_SS(0);
}
void MySPI_Stop(void)
{
SPI_W_SS(1);
}
2.1.3 SPI交换一个字节
uint8_t MySPI_ExchangeByte(uint8_t pdata)
{
uint8_t i;
uint8_t Data_Receive = 0x00;
for(i=0;i<8;i++)
{
Data_Receive <<= 1;
SPI_W_MOSI(pdata & (0x80 >> i));
SPI_W_SCK(1);
Data_Receive |= SPI_R_MISO();
SPI_W_SCK(0);
}
return Data_Receive;
}
2.2 W25Q64.c
2.2.1读取W25Q64的设备ID和厂商ID
void W25Q64_ReadID(uint8_t *MID,uint16_t *DID)
{
MySPI_Start();
MySPI_ExchangeByte(W25Q64_JEDEC_ID);
*MID = MySPI_ExchangeByte(W25Q64_DUMMY_BYTE);
*DID = MySPI_ExchangeByte(W25Q64_DUMMY_BYTE);
*DID <<= 8;
*DID |= MySPI_ExchangeByte(W25Q64_DUMMY_BYTE);
MySPI_Stop();
}
2.2.2写使能和判断等待BUSY
void W25Q64_WriteEnable(void)
{
MySPI_Start();
MySPI_ExchangeByte(W25Q64_WRITE_ENABLE);
MySPI_Stop();
}
void W25Q64_WaitBusy(void)
{
uint8_t busy = 0;
uint32_t time = 10000;
MySPI_Start();
MySPI_ExchangeByte(W25Q64_READ_STATUS_REGISTER_1);
busy = MySPI_ExchangeByte(W25Q64_DUMMY_BYTE) & 0x01;
while(busy)
{
time--;
if(time == 0)
{
break;
}
}
MySPI_Stop();
}
2.2.3 扇区擦除
void W25Q64_SectorErase(uint32_t AddressErase)
{
W25Q64_WriteEnable();
MySPI_Start();
MySPI_ExchangeByte(W25Q64_SECTOR_ERASE_4KB);
MySPI_ExchangeByte(AddressErase >> 16);
MySPI_ExchangeByte(AddressErase >> 8);
MySPI_ExchangeByte(AddressErase);
MySPI_Stop();
W25Q64_WaitBusy();
}
2.2.4 页编程和读数据
void W25Q64_PageProgram(uint32_t AddressWrite,uint8_t *DataWrite,uint16_t count)
{
uint16_t i;
W25Q64_WriteEnable();
MySPI_Start();
MySPI_ExchangeByte(W25Q64_PAGE_PROGRAM);
MySPI_ExchangeByte(AddressWrite >> 16);
MySPI_ExchangeByte(AddressWrite >> 8);
MySPI_ExchangeByte(AddressWrite);
for(i=0;i<count;i++)
{
MySPI_ExchangeByte(*(DataWrite+i));
}
MySPI_Stop();
W25Q64_WaitBusy();
}
void W25Q64_ReadData(uint32_t AddressRead, uint8_t *DataRead,uint16_t count)
{
uint32_t i;
MySPI_Start();
MySPI_ExchangeByte(W25Q64_READ_DATA);
MySPI_ExchangeByte(AddressRead >> 16);
MySPI_ExchangeByte(AddressRead >> 8);
MySPI_ExchangeByte(AddressRead);
for(i=0;i<count;i++)
{
*(DataRead+i) = MySPI_ExchangeByte(W25Q64_DUMMY_BYTE);
}
MySPI_Stop();
}
2.3 W25Q64.h
其中定义了一些常用的命令如下:
#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_UNIQUE_ID 0x4B
#define W25Q64_JEDEC_ID 0x9F
#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
#define W25Q64_DUMMY_BYTE 0xFF
2.4测试程序
测试将一个数组的数据写入到W25Q64,然后用另一个数组读出来,将读出来的数据打印到串口。
还需按下图使能串口1,在CubeXM左侧选择Connectivity,然后选择USART1,再将模式设为Asynchronous模式,下面的参数设置部分默认就行,不用修改,再次点击右上角的GENERATE CODE生成代码,生成后在Keil里点击“是”即可。
要使用printf函数在串口输出的话,还要重定向printf函数,即在usart.c中加入以下两个函数(记得加入头文件#include<stdio.h>,不然会报错):
/* USER CODE BEGIN 1 */
int fputc(int ch, FILE *f)
{
HAL_UART_Transmit(&huart1, (uint8_t*)&ch, 1, 10);
return ch;
}
int fgetc(FILE *f)
{
uint8_t ch = 0;
HAL_UART_Receive(&huart1, (uint8_t*)&ch, 1, 10);
return (int)ch;
}
/* USER CODE END 1 */
重新向printf函数以后,需要点击魔术棒,在Target中勾选Use MicroLIB,具体步骤见下图,不然程序运行的时候会卡在启动文件里。
再在main.c中写测试部分:
/* USER CODE BEGIN 2 */
uint8_t i;
uint8_t MID = 0x00;
uint16_t DID = 0x00;
uint8_t ArrayWrite[] = "Hello World!";
uint8_t ArrayRead[12];
MySPI_W_SS(1);
MySPI_W_SCK(0);
W25Q64_ReadID(&MID,&DID);
printf("ID = 0x%x%x\r\n",MID,DID);
W25Q64_SectorErase(0x000000);
W25Q64_PageProgram(0x000000,ArrayWrite,12);
W25Q64_ReadData(0x000000,ArrayRead,12);
for(i=0;i<12;i++)
{
printf("%c",ArrayRead[i]);
}
printf("\r\n");
W25Q64_SectorErase(0x000000);
W25Q64_ReadData(0x000000,ArrayRead,12);
for(i=0;i<12;i++)
{
printf("%x",ArrayRead[i]);
}
printf("\r\n");
/* USER CODE END 2 */
打开一个串口助手,设置对应的COM口,波特率默认115200,8位数据位,1位停止位,没有校验和流控,设置好后打开串口会打印以下信息:
W25Q64的ID为0xef4017,擦除扇区后,对应的扇区数据位均为1.
写这篇博客主要是为了以后忘记的时候能回来看一遍,快速回忆一下,如果有材料侵权,请私信本人删除,谢谢!