【STM32】软件SPI读写W25Q64芯片

目录

W25Q64模块

W25Q64芯片简介

硬件电路

W25Q64框图

Flash操作注意事项

状态寄存器

​编辑

[指令集 INSTRUCTIONS​编辑](#指令集 INSTRUCTIONS编辑)

​编辑

SPI读写W25Q64代码

硬件接线图

MySPI.c

MySPI.h

W25Q64

W25Q64.c

W25Q64.h

W25Q64_Ins.h

main.c

测试


SPI通信(W25Q64芯片简介,使用SPI读写W25Q64存储器芯片)

SPI通信文章:【STM32】SPI通信

http://t.csdnimg.cn/ZKzWt

http://t.csdnimg.cn/BE3Gq


W25Q64模块

W25Q64芯片简介

  • W25Qxx系列是一种低成本 、小型化、使用简单的非易失性存储器 ,常应用于数据存储字库存储固件程序存储等场景
  • 存储介质:Nor Flash(闪存)
  • 时钟频率:80MHz / 160MHz (Dual SPI) / 320MHz (Quad SPI)
  • 存储容量(24位地址 ):
    W25Q40: 4Mbit / 512KByte
    W25Q80: 8Mbit / 1MByte
    W25Q16: 16Mbit / 2MByte
    W25Q32: 32Mbit / 4MByte
    W25Q64: 64Mbit / 8MByte
    W25Q128: 128Mbit / 16MByte
    W25Q256: 256Mbit / 32MByte

在51单片机中学过一款比较经典的存储芯片AT24C02(I2C通信协议),蓝桥杯嵌入式板子上用的也是AT24C02(I2C通信协议),容量一般是kb级别的

W25QXX引脚接线比较简单,接VCC、GND,其他引脚都接GPIO口即可,先用软件SPI

易失性存储器: 一般是SRAM、DRAM

非易失性存储器:一般是E2PROM、Flash,(掉电不丢失,数据存储)

固件程序存储:直接把程序文件下载到外挂芯片里,执行程序时,读取外挂芯片的程序,这就是XIP,就地执行;比如说电脑的BIOS固件,就可以存储在这种非易失性存储器里

  • 时钟线的最大频率是80MHz,相比于STM32F103C8T6,是非常快的,写程序翻转引脚的时候,就不需要加延时了,即使不延时,引脚的翻转频率也达不到80MHz
  • 双重SPI等效的频率:160MHz (Dual SPI) (MISO和MOSI都可以同时发送和接收)(一个时钟信号,发送或接收2位数据)
  • 四重SPI等效的频率:320MHz (Quad SPI)(MISO+MOSI+WP+HOLD引脚,一共四个引脚)(一个时钟信号,发送或接收4位数据)(四位并行)
  1. 24位地址,最大寻址空间是2的24次方 = 16777216bit
  2. 16777216bit / 1024 = 16384kbit
  3. 16384kb / 1024 = 16Mbit
  4. 所以24位地址寻址的最大寻址空间是16Mb(2MB)

硬件电路

左边的图是W25Q64模块的原理图,右上角是芯片引脚定义,右下角这个表,就是每个引脚定义的功能

  • 注意VCC不能接入5V,应该接入3.3V电压
  • WP写保护,低电平有效,WP低电平不能写入,高电平可以写入
  • HOLD数据保持,低电平有效,给HOLD低电平~将时序保存下来,等操作完其他器件,回来给HOLD高电平,继续HOLD之前的时序~~~

W25Q64框图

块 ~ 扇区 ~ 页

一整个存储空间,划分为若干块,对于每个块,又划分为若干扇区,每个扇区划分为很多页,每页256字节

  • 8MB空间,划分成128个块,每块64KB
  • 每一块(64KB)有16个扇区,每个扇区4KB
  • 一页是256字节,一个扇区4KB=4096B,4096/256=16页,一个扇区16页

Flash操作注意事项

写入操作时:

  • 写入操作前,必须 先进行写使能(防止误操作)
  • 每个数据位只能由1改写为0不能由0改写为1
  • 写入数据前必须先擦除,擦除后,所有数据位变为1(弥补了上一条)
  • 擦除必须按最小擦除单元进行(扇区)
  • 连续写入多字节时,最多写入一页的数据 ,超过页尾位置的数据,会回到页首覆盖写入
  • 写入操作结束后,芯片进入忙状态,不响应新的读写操作

读取操作时:

  • 直接调用读取时序 ,无需使能,无需额外操作,没有页的限制,读取操作结束后不会进入忙状态,但不能在忙状态时读取

状态寄存器

BUSY位

BUSY is a read only bit in the status register (S0) that is set to a 1 state when the device is executing a Page Program, Sector Erase, Block Erase, Chip Erase or Write Status Register instruction. During this time the device will ignore further instructions except for the Read Status Register and Erase Suspend instruction (see tW, tPP, tSE, tBE, and tCE in AC Characteristics). When the program, erase or write status register instruction has completed, the BUSY bit will be cleared to a 0 state indicating the device is ready for further instructions.

翻译一下:
BUSY 是状态寄存器(S0)中的一个只读位当设备执行页程序、扇区擦除、块擦除、芯片擦除或写状态寄存器指令时,它被设置为1状态。 在此期间,设备将忽略除读状态寄存器和擦除暂停指令之外的其他指令(参见交流特性中的tW, tPP, tSE, be和tCE)。当程序、擦除或写状态寄存器指令完成时,BUSY位将被清除为0状态,表明设备已准备好接受进一步的指令。

Write Enable Latch(WEL)写使能锁存位

Write Enable Latch (WEL) is a read only bit in the status register (S1) that is set to a 1 after executing a Write Enable Instruction. The WEL status bit is cleared to a 0 when the device is write disabled. A write disable state occurs upon power-up or after any of the following instructions: Write Disable, Page Program, Sector Erase, Block Erase, Chip Erase and Write Status Register.

翻译一下:
写使能锁存(WEL) 是状态寄存器(S1)中的一个只读位在执行写使能指令后被设置为1。当设备写失能时,WEL状态位被清除为0。写失能 状态发生在上电 或以下任何指令之后:写禁用、页程序、扇区擦除、块擦除、芯片擦除和写状态寄存器。

WEL位寄存器总结: 先写使能,再执行写入数据操作后,不需要手动写失能,因为写入后,顺便就写失能了,所以在进行任何写入操作前,都需要进行写使能,一次写使能,只能运行一条指令

指令集 INSTRUCTIONS

厂商ID = EF

设备ID,用AB或者90读取,设备ID=16,如果用9F读取,设备ID=4017

|------|------|
| 写使能 | 0x06 |
| 写失能 | 0x04 |
| 读指令 | 0x05 |
| 页编程 | 0x02 |
| 扇区擦除 | 0x20 |
| 读取ID | 0x9F |
| 读取数据 | 0x03 |
[常用的指令集]


SPI读写W25Q64代码

硬件接线图

  • Vcc ---3.3V
  • CS 片选信号---PA4
  • DO 从机输出---PA6
  • GND
  • CLK 时钟信号---PA5
  • DI 从机输入---PA7

按照硬件SPI接线,这样可以软件SPI和硬件SPI任意切换

程序框架:

  • 先写一个MySPI的底层库 ---SPI 通信层
  • 基于SPI 建一个W25Q64 硬件驱动层代码
  • 主函数调用硬件驱动层代码

PA6是主机输入,配置成上拉输入,其他三个引脚配置成推挽输出即可

然后封装GPIO输出函数,代码在下面,有加注释

交换一个字节(读写一个字节),W25Q64系列,支持模式0和模式3

模式0执行逻辑;先ss下降沿,移出数据;再sck上升沿,移入数据;再sck下降沿,移出数据

(0x80 >> i) 用于挑出数据的某一位或者某几位,用来屏蔽其他的无关位,这种类型的数据,叫做掩码,掩码模型:不会改变数据本身。

SPI通信层代码

MySPI.c

#include "MySPI.h"

/*引脚配置层*/

//封装 置引脚的高低电平的函数都封装起来,换个名称
/**
  * 函    数:SPI写SS引脚电平
  * 参    数:BitValue 协议层传入的当前需要写入SS的电平,范围0~1
  * 返 回 值:无
  * 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SS为低电平,当BitValue为1时,需要置SS为高电平
  */
void  MySPI_W_SS(uint8_t BitValue)	//CS引脚(SS引脚)PA4
{
	GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue);	//根据BitValue,设置SS引脚的电平
}

/**
  * 函    数:SPI写SCK引脚电平
  * 参    数:BitValue 协议层传入的当前需要写入SCK的电平,范围0~1
  * 返 回 值:无
  * 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SCK为低电平,当BitValue为1时,需要置SCK为高电平
  */
void  MySPI_W_SCK(uint8_t BitValue)	//SCK引脚(PA5)
{
	GPIO_WriteBit(GPIOA, GPIO_Pin_5, (BitAction)BitValue);	//根据BitValue,设置SCK引脚的电平
}

/**
  * 函    数:SPI写MOSI引脚电平
  * 参    数:BitValue 协议层传入的当前需要写入MOSI的电平,范围0~0xFF
  * 返 回 值:无
  * 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置MOSI为低电平,当BitValue非0时,需要置MOSI为高电平
  */
void  MySPI_W_MOSI(uint8_t BitValue)	//MOSI引脚(PA7)
{
	GPIO_WriteBit(GPIOA, GPIO_Pin_7, (BitAction)BitValue);	//根据BitValue,设置MOSI引脚的电平,BitValue要实现非0即1的特性
}

/**
  * 函    数:I2C读MISO引脚电平
  * 参    数:无
  * 返 回 值:协议层需要得到的当前MISO的电平,范围0~1
  * 注意事项:此函数需要用户实现内容,当前MISO为低电平时,返回0,当前MISO为高电平时,返回1
  */
uint8_t  MySPI_R_MISO(void)	//MISO引脚(PA6 输入引脚)
{
	return GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6);	//读取MISO电平并返回
}


// SPI速度非常快,操作完引脚,就不需要加延时了

/*
	输出引脚配置为推挽输出,输入引脚配置为浮空或上拉输入
	对主机来说,时钟、主机输出、片选都是输出引脚---推挽输出
	主机输入MISO---输出引脚---选择上拉输入	
	从机(W25Q64)的DO输出,是主机输入---PA6
*/
/**
  * 函    数:SPI初始化
  * 参    数:无
  * 返 回 值:无
  * 注意事项:此函数需要用户实现内容,实现SS、SCK、MOSI和MISO引脚的初始化
  */
void MySPI_Init(void)
{
	/*开启时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);	//开启GPIOA的时钟                    
	
	/*GPIO初始化*/
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;	//推挽输出
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_7;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);				//将PA4、PA5和PA7引脚初始化为推挽输出
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;		//上拉输入
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 ;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);				//将PA6引脚初始化为上拉输入
	
	/*设置默认电平*/
	MySPI_W_SS(1); 	// SS置高, 默认不选中从机
	MySPI_W_SCK(0);	// 计划使用模式0, 默认低电平
	// MOSI 没有明确规定,MISO是输入引脚,不用输出电平状态
}


/*协议层*/


/**
  * 函    数:SPI起始
  * 参    数:无
  * 返 回 值:无
  */
void MySPI_Start(void)
{
	MySPI_W_SS(0);	//拉低SS,开始时序
}

/**
  * 函    数:SPI终止
  * 参    数:无
  * 返 回 值:无
  */
void MySPI_Stop(void)
{
	MySPI_W_SS(1);	//拉高SS,终止时序
}



//交换一个字节(读写一个字节),W25Q64系列,支持模式0和模式3
//这里选择模式0
//ByteSend是传进来的参数,通过交换一个字节发送出去,接受
/**
  * 函    数:SPI交换传输一个字节,使用SPI模式0
  * 参    数:ByteSend 要发送的一个字节
  * 返 回 值:接收的一个字节
  */
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{	//模式0
	uint8_t i, Byte_Receive = 0x00;	//这个变量一定要初始化,不然就出错了!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
	//执行逻辑;先ss下降沿,移出数据,再sck上升沿,移入数据,再sck下降沿,移出数据
	//ss下降沿之后,主机移出数据最高位放到MOSI上,从机移出最高位放到MISO上
	
	// (0x80 >> i) 用于挑出数据的某一位或者某几位,用来屏蔽其他的无关位,这种类型的数据,叫做掩码
	for (i = 0; i < 8; i++)
	{
		//第一步:写MOSI
			MySPI_W_MOSI(ByteSend & (0x80 >> i));	
		//第二步:SCK上升沿,主机和从机同时移入数据
			MySPI_W_SCK(1);	// 这个上升沿,从机自动读取MOSI的数据,
			if (MySPI_R_MISO() == 1) {Byte_Receive |= (0x80 >> i);}	//主机读取从机放到MISO上的数据(最高位)
		//第三步:SCK产生下降沿
			MySPI_W_SCK(0);
	}
	return Byte_Receive;
}



/*江科大注释版本

uint8_t MySPI_SwapByte(uint8_t ByteSend)
{
	uint8_t i, ByteReceive = 0x00;					//定义接收的数据,并赋初值0x00,此处必须赋初值0x00,后面会用到
	
	for (i = 0; i < 8; i ++)						//循环8次,依次交换每一位数据
	{
		MySPI_W_MOSI(ByteSend & (0x80 >> i));		//使用掩码的方式取出ByteSend的指定一位数据并写入到MOSI线
		MySPI_W_SCK(1);								//拉高SCK,上升沿移出数据
		if (MySPI_R_MISO() == 1){ByteReceive |= (0x80 >> i);}	//读取MISO数据,并存储到Byte变量
																//当MISO为1时,置变量指定位为1,当MISO为0时,不做处理,指定位为默认的初值0
		MySPI_W_SCK(0);								//拉低SCK,下降沿移入数据
	}
	
	return ByteReceive;								//返回接收到的一个字节数据
}

*/



/*	移位模型:移位寄存器的原理
	掩码模型:不会改变数据本身
	现在不用掩码方式,用移位模型 稍微修改代码
	移位模型:不用定义Byte_Receive变量,直接修改传入参数ByteSend,最后返回这个参数
				不安全,后续想使用ByteSend参数,就没法用了,但是效率比掩码的方法高
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{	//模式0
	
	//执行逻辑;先ss下降沿,移出数据,再sck上升沿,移入数据,再sck下降沿,移出数据
	//ss下降沿之后,主机移出数据最高位放到MOSI上,从机移出最高位放到MISO上
	
	// (0x80 >> i) 用于挑出数据的某一位或者某几位,用来屏蔽其他的无关位,这种类型的数据,叫做掩码
	for (i = 0; i < 8; i++)
	{
		//第一步:写MOSI,移出数据
			MySPI_W_MOSI(ByteSend & 0x80);	//放入最高位
			ByteSend <<= 1;		//ByteSend左移一位,低位补0,空出来了,下面可以直接接收了
		//第二步:SCK上升沿,主机和从机同时移入数据
			MySPI_W_SCK(1);	// 这个上升沿,从机自动读取MOSI的数据,
			if (MySPI_R_MISO() == 1) {ByteSend |= 0x01;}	//把收到的数据放到ByteSend的最低位
		//第三步:SCK产生下降沿
			MySPI_W_SCK(0);
		//下一次循环,还是取ByteSend,在左移1位,取出MISO数据,放到最低位,依次循环8次,交换接收到的数据,就存在ByteSend里了
	}
	return ByteSend;
}
*/



/*SPI 模式1 2 3 的修改方法*/
/*
//在模式模式0基础上 修改成模式1
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{	
	uint8_t i, Byte_Receive;
	//执行逻辑;先ss下降沿,sck上升沿,移出数据,再sck下降沿,移入数据  ---  修改程序的相位即可	
	
	for (i = 0; i < 8; i++)
	{
		//第一步:SCK上升沿,主机和从机同时移入数据
			MySPI_W_SCK(1);	// 这个上升沿,从机自动读取MOSI的数据
		//第二步:写MOSI
			MySPI_W_MOSI(ByteSend & (0x80 >> i));	//移出数据
		//第三步:SCK产生下降沿
			MySPI_W_SCK(0);
		//第四步:移入数据
			if (MySPI_R_MISO() == 1) {Byte_Receive |= (0x80 >> i);}	
		
	}
	return Byte_Receive;
}*/

/*
//在模式1的基础上,修改成模式3 --- 模式1和模式3的区别:时钟极性不同
//操作:就是所有出现SCK的地方,都取反,初始化里的时钟极性也要翻转!!!
//初始化里的时钟极性也要翻转!!!初始化里的时钟极性也要翻转!!!
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{	
	uint8_t i, Byte_Receive;
	//执行逻辑;先ss下降沿,sck上升沿,移出数据,再sck下降沿,移入数据  ---  修改程序的相位即可	
	
	for (i = 0; i < 8; i++)
	{
		//第一步:SCK下降沿,主机和从机同时移入数据
			MySPI_W_SCK(0);	// 这个下降沿,从机自动读取MOSI的数据
		//第二步:写MOSI
			MySPI_W_MOSI(ByteSend & (0x80 >> i));	//移出数据
		//第三步:SCK产生上升沿
			MySPI_W_SCK(1);
		//第四步:移入数据
			if (MySPI_R_MISO() == 1) {Byte_Receive |= (0x80 >> i);}	
		
	}
	return Byte_Receive;
}*/

/*SPI模式2:在模式0的基础上,翻转所有的SCK极性*/
//这就是SPI的四种模式修改方法

MySPI.h

#ifndef __MYSPI_H__
#define __MYSPI_H__

#include "stm32f10x.h"                  // Device header

void MySPI_Init(void);
void MySPI_Start(void);
void MySPI_Stop(void);
uint8_t MySPI_SwapByte(uint8_t ByteSend);

#endif

W25Q64

W25Q64只支持模式0和模式3

W25Q64_WaitBusy()函数,事前等待&事后等待

事后等待只需要再写入操作前调用;

事前等待在写入操作和读取操作之前都得调用

W25Q64.c

#include "W25Q64.h"

void W25Q64_Init(void)
{
	MySPI_Init();
}

/*读取ID,第一个字节:厂商ID。设备ID:第二个字节:存储器类型;第三个字节:容量*/
/**
  * 函    数:W25Q64读取ID号
  * 参    数:MID 工厂ID,使用输出参数的形式返回
  * 参    数:DID 设备ID,使用输出参数的形式返回
  * 返 回 值:无
  */
void W25Q64_ReadID(uint8_t *MID, uint16_t *DID)
{
	MySPI_Start();
	MySPI_SwapByte(W25Q64_JEDEC_ID);			// 0x9F, 读取ID号码指令,这里的返回值没有意义,就不需要了
	*MID = MySPI_SwapByte(W25Q64_DUMMY_BYTE); 	// 这次交换,把数据给主机,接收的数据是厂商ID变量*MID,发送的数据任意给,一般给0xFF
												//  这里是在通信,通信是有时序的,不同时间调用相同的函数,意义就是不一样的
	*DID = MySPI_SwapByte(W25Q64_DUMMY_BYTE);  	// 设备ID的高八位(第三次交换)
	*DID <<= 8;								   	// 高八位移到左边
	*DID |= MySPI_SwapByte(W25Q64_DUMMY_BYTE); 	// 设备ID的低八位(用或运算,整合数据)
	MySPI_Stop();
}

/**
  * 函    数:W25Q64写使能
  * 参    数:无
  * 返 回 值:无
  */
void W25Q64_WriteEnable(void)
{
	MySPI_Start();							//SPI起始
	MySPI_SwapByte(W25Q64_WRITE_ENABLE); 	//交换发送 写使能的指令
	MySPI_Stop();							//SPI终止
}


// 发送指令码05,发完指令码,读取状态寄存器,查看是否是忙状态,最低位BUSY,1是忙,0是不忙
/**
  * 函    数:W25Q64等待忙
  * 参    数:无
  * 返 回 值:无
  */
void W25Q64_WaitBusy(void) // 等待busy位为0
{
	uint32_t Timeout;
	MySPI_Start();								//SPI起始
	MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1);				//交换发送读状态寄存器1的指令
	Timeout = 100000;							//给定超时计数时间
	while ((MySPI_SwapByte(W25Q64_DUMMY_BYTE) & 0x01) == 0x01)	//循环等待忙标志位
	{
		Timeout --;								//等待时,计数值自减
		if (Timeout == 0)						//自减到0后,等待超时
		{
			/*超时的错误处理代码,可以添加到此处*/
			break;								//跳出等待,不等了
		}
	}
	MySPI_Stop();								//SPI终止
}
/*注意:
		W25Q64_WaitBusy,事前等待&事后等待
		事后等待只需要再写入操作前调用;
		事前等待在写入操作和读取操作之前都得调用
*/

/**
  * 函    数:W25Q64页编程
  * 参    数:Address 页编程的起始地址,范围:0x000000~0x7FFFFF
  * 参    数:DataArray	用于写入数据的数组(指针传递数组)
  * 参    数:Count 要写入数据的数量,范围:0~256
  * 返 回 值:无
  * 注意事项:写入的地址范围不能跨页
  */
void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count)
{
	uint16_t i;
	
	W25Q64_WriteEnable();						//写入操作前,必须先写使能
	
	MySPI_Start();								//SPI起始
	MySPI_SwapByte(W25Q64_PAGE_PROGRAM);		//交换发送页编程的指令
	MySPI_SwapByte(Address >> 16);				//交换发送地址23~16位
	MySPI_SwapByte(Address >> 8);				//交换发送地址15~8位
	MySPI_SwapByte(Address);					//交换发送地址7~0位
	
	for (i = 0; i < Count; i ++)				//循环Count次
	{
		MySPI_SwapByte(DataArray[i]);			//依次在起始地址后写入数据
	}
	
	MySPI_Stop();								//SPI终止
	
	W25Q64_WaitBusy();							//等待忙,事后等待比较保险
}

/**
  * 函    数:W25Q64扇区擦除(4KB)
  * 参    数:Address 指定扇区的地址,范围:0x000000~0x7FFFFF
  * 返 回 值:无
  */
void W25Q64_SectorErase(uint32_t Address)
{
	W25Q64_WriteEnable();						//写使能
	
	MySPI_Start();								//SPI起始
	MySPI_SwapByte(W25Q64_SECTOR_ERASE_4KB);	//交换发送扇区擦除的指令
	MySPI_SwapByte(Address >> 16);				//交换发送地址23~16位
	MySPI_SwapByte(Address >> 8);				//交换发送地址15~8位
	MySPI_SwapByte(Address);					//交换发送地址7~0位
	MySPI_Stop();								//SPI终止
	
	W25Q64_WaitBusy();							//等待忙,不忙就退出这个函数了
}

/**
  * 函    数:W25Q64读取数据
  * 参    数:Address 读取数据的起始地址,范围:0x000000~0x7FFFFF
  * 参    数:DataArray 用于接收读取数据的数组,通过输出参数返回
  * 参    数:Count 要读取数据的数量,范围:0~0x800000
  * 返 回 值:无
  */
void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count)
{
	uint32_t i;
	MySPI_Start();								//SPI起始
	MySPI_SwapByte(W25Q64_READ_DATA);			//交换发送读取数据的指令
	MySPI_SwapByte(Address >> 16);				//交换发送地址23~16位
	MySPI_SwapByte(Address >> 8);				//交换发送地址15~8位
	MySPI_SwapByte(Address);					//交换发送地址7~0位
	
	for (i = 0; i < Count; i ++)				//循环Count次
	{
		DataArray[i] = MySPI_SwapByte(W25Q64_DUMMY_BYTE);	//依次在起始地址后读取数据
	}
	
	MySPI_Stop();								//SPI终止
}

W25Q64.h

#ifndef __W25Q64_H__
#define __W25Q64_H__

#include "stm32f10x.h"                  // Device header
#include "MySPI.h"
#include "W25Q64_Ins.h"     //指令的头文件

void W25Q64_Init(void);
void W25Q64_ReadID(uint8_t *MID, uint16_t *DID);
void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count);
void W25Q64_SectorErase(uint32_t Address);
void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count);

#endif

W25Q64_Ins.h

cpp 复制代码
#ifndef __W25Q64_INS_H
#define __W25Q64_INS_H

#define W25Q64_WRITE_ENABLE							0x06	//写使能
#define W25Q64_WRITE_DISABLE						0x04	//写失能
#define W25Q64_READ_STATUS_REGISTER_1				0x05	//读状态寄存器1
#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	//读取ID号
#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	//无用数据

#endif

main.c

#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "W25Q64.h"

uint8_t MID;
uint16_t DID;

uint8_t ArrayWrite[] = {0x01, 0x02, 0x03, 0x04};	//定义要写入数据的测试数组
//uint8_t ArrayWrite[] = {0x11, 0x22, 0x33, 0x44};	//定义要写入数据的测试数组
uint8_t ArrayRead[4];								//定义要读取数据的测试数组

int main(void)
{
	/*模块初始化*/
	OLED_Init();						//OLED初始化
	W25Q64_Init();						//W25Q64初始化
	
	/*显示静态字符串*/
	OLED_ShowString(1, 1, "MID:   DID:");
	OLED_ShowString(2, 1, "W:");
	OLED_ShowString(3, 1, "R:");
	
	/*显示ID号*/
	W25Q64_ReadID(&MID, &DID);			//获取W25Q64的ID号,指针 返回输出参数
	OLED_ShowHexNum(1, 5, MID, 2);		//显示MID,显示厂商ID
	OLED_ShowHexNum(1, 12, DID, 4);		//显示DID,显示设备ID
	
	/*W25Q64功能函数测试*/
	W25Q64_SectorErase(0x000000);					//扇区擦除(写之前先进行扇区擦除操作)
	W25Q64_PageProgram(0x000000, ArrayWrite, 4);	//将写入数据的测试数组写入到W25Q64中
	
	W25Q64_ReadData(0x000000, ArrayRead, 4);		//读取刚写入的测试数据到读取数据的测试数组中
	
	/*显示数据*/
	OLED_ShowHexNum(2, 3, ArrayWrite[0], 2);		//显示写入数据的测试数组
	OLED_ShowHexNum(2, 6, ArrayWrite[1], 2);
	OLED_ShowHexNum(2, 9, ArrayWrite[2], 2);
	OLED_ShowHexNum(2, 12, ArrayWrite[3], 2);
	
	OLED_ShowHexNum(3, 3, ArrayRead[0], 2);			//显示读取数据的测试数组
	OLED_ShowHexNum(3, 6, ArrayRead[1], 2);
	OLED_ShowHexNum(3, 9, ArrayRead[2], 2);
	OLED_ShowHexNum(3, 12, ArrayRead[3], 2);
	
	while (1)
	{
		
	}
}

测试

  1. 修改写入的数组内容,如果写入读出一直,则正常读写
  2. 验证掉电不丢失,将扇区擦除和页编程的两行代码注释掉,下载、断电、重新上电看显示是否有变化,无变化则验证成功
  3. 验证一下FLASH擦除之后变成FF的特性,在上面的基础上,将擦除的那一行取消注释,下载测试,读取的数据都是FF就验证成功了
  4. 不擦除直接改写,注释掉擦除的那一行代码,取消注释页编程,然后将写入的数据修改一下,下载测试,(比如说先写入AA、BB、CC、DD,后面改成55、66、77、88,则读出数据是00、22、44、88),成功验证FLASH只能1写0,不能0写1
  5. 不能跨页写入

原创笔记,码字不易,欢迎点赞,收藏~

相关推荐
日晨难再20 分钟前
嵌入式:STM32的启动(Startup)文件解析
stm32·单片机·嵌入式硬件
yufengxinpian43 分钟前
集成了高性能ARM Cortex-M0+处理器的一款SimpleLink 2.4 GHz无线模块-RF-BM-2340B1
单片机·嵌入式硬件·音视频·智能硬件
__基本操作__2 小时前
历遍单片机下的IIC设备[ESP--0]
单片机·嵌入式硬件
网易独家音乐人Mike Zhou8 小时前
【卡尔曼滤波】数据预测Prediction观测器的理论推导及应用 C语言、Python实现(Kalman Filter)
c语言·python·单片机·物联网·算法·嵌入式·iot
zy张起灵8 小时前
48v72v-100v转12v 10A大功率转换电源方案CSM3100SK
经验分享·嵌入式硬件·硬件工程
PegasusYu11 小时前
STM32CUBEIDE FreeRTOS操作教程(九):eventgroup事件标志组
stm32·教程·rtos·stm32cubeide·free-rtos·eventgroup·时间标志组
lantiandianzi15 小时前
基于单片机的多功能跑步机控制系统
单片机·嵌入式硬件
文弱书生65615 小时前
输出比较简介
stm32
哔哥哔特商务网15 小时前
高集成的MCU方案已成电机应用趋势?
单片机·嵌入式硬件
跟着杰哥学嵌入式15 小时前
单片机进阶硬件部分_day2_项目实践
单片机·嵌入式硬件