软件SPI读写W25Q64

一、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有四种传输模式,如下表所示,主要差别在于CPOLCPHA的不同。

CPOL(Clock Polarity,时钟极性)表示SCK在空闲时为高电平还是低电平。当CPOL=0,SCK空闲时为 低电平,当CPOL=1,SCK空闲时为高电平。

        CPHA(Clock Phase,时钟相位)表示 SCK 在第几个时钟边缘采样数据。当 CPHA=0 ,在
SCK第一个边 沿采样数据,当CPHA=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.

        写这篇博客主要是为了以后忘记的时候能回来看一遍,快速回忆一下,如果有材料侵权,请私信本人删除,谢谢!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值