【STM32】软件SPI读写W25Q64芯片

目录

W25Q64模块

W25Q64芯片简介

硬件电路

W25Q64框图

Flash操作注意事项

状态寄存器

​编辑

指令集 INSTRUCTIONS​编辑

​编辑

SPI读写W25Q64代码

硬件接线图

MySPI.c

MySPI.h

W25Q64

W25Q64.c

W25Q64.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
读取ID0x9F
读取数据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

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. 不能跨页写入

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/393510.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【数据结构与算法】递归、回溯、八皇后 一文打尽!

&#x1f389;&#x1f389;欢迎光临&#x1f389;&#x1f389; &#x1f3c5;我是苏泽&#xff0c;一位对技术充满热情的探索者和分享者。&#x1f680;&#x1f680; &#x1f31f;特别推荐给大家我的最新专栏《数据结构与算法&#xff1a;初学者入门指南》&#x1f4d8;&am…

山西电力市场日前价格预测【2024-02-19】

日前价格预测 预测说明&#xff1a; 如上图所示&#xff0c;预测明日&#xff08;2024-02-19&#xff09;山西电力市场全天平均日前电价为377.52元/MWh。其中&#xff0c;最高日前电价为557.55元/MWh&#xff0c;预计出现在08:15。最低日前电价为247.93元/MWh&#xff0c;预计…

个人简历补充

个人简历补充 1.对工作的认识2.八股文和知识面3.框架/架构角度深扒3.1 前端3.1.1 mPaaS&#xff08;移动领域&#xff09;3.1.2 普通前端项目框架3.1.3 微前端 3.2 后端 持续更新 1.对工作的认识 2.八股文和知识面 前端&#xff08;基础知识 / 开发能力 / 总结输出能力&#xf…

2.18总结

这两天在加强对最短路径的学习&#xff0c;除了Floyed比较简单之外&#xff0c;dijkstra、bell-mandford和SPFA学起来感觉有些难&#xff0c;洛谷上的模板题卡了半天都没写出来&#xff0c;最后只能通过看题解来帮助自己完成。 SPFA 算法实现&#xff1a; 最短路径算法对比比较…

嵌入式学习 C++ Day5、6

嵌入式学习 C Day5、6 一、思维导图 二、作业 1.以下是一个简单的比喻&#xff0c;将多态概念与生活中的实际情况相联系&#xff1a; 比喻&#xff1a;动物园的讲解员和动物表演 想象一下你去了一家动物园&#xff0c;看到了许多不同种类的动物&#xff0c;如狮子、大象、猴…

驶向未来:3D可视化模型重塑我们的道路认知

在科技的浪潮中&#xff0c;每一个革新都是对人类未来生活的深度洞察。而今&#xff0c;当可视化这一技术走进我们的视野&#xff0c;它不仅是一场视觉盛宴&#xff0c;更是一次对未来出行方式的全新探索。 一、从平面到立体&#xff0c;解锁道路新视角 你是否曾站在十字路口&…

[Flink04] Flink部署实践

Flink部署支持三种模式&#xff1a;本地部署、Standalone部署、Flink on Yarn部署。 独立&#xff08;Standalone&#xff09;模式由Flink自身提供资源&#xff0c;无需其他框架&#xff0c;这种方式降低了和其他第三方资源框架的耦合性&#xff0c;独立性非常强。但Flink 是大…

老兵(11)

百度文心一格&#xff0c;大约是一年前上线并免费向用户开放的。其实也不是免费&#xff0c;而是“电量”比较好获得&#xff0c;白送的就16/每天&#xff0c;如果只是好奇玩玩的话也算够吧。 当时就很开心&#xff0c;因为一直想着把一些文案图像化&#xff0c;做成漫画的形式…

【教程】Linux使用aria2c多线程满速下载

转载请注明出处&#xff1a;小锋学长生活大爆炸[xfxuezhang.cn] 安装aria2c&#xff1a; sudo apt-get install aria2多线程下载&#xff1a; aria2c -x 16 -s 16 <url> 比如&#xff1a; aria2c -x 16 -s 16 http://images.cocodataset.org/zips/test2017.zip

轨道交通信号增强与覆盖解决方案——经济高效,灵活应用于各类轨道交通场景!

方案背景 我国是世界上轨道交通里程最长的国家&#xff0c;轨道交通也为我们的日常出行带来极大的便利。伴随着无线通信技术的快速发展将我们带入电子时代&#xff0c;出行的过程中对无线通信的依赖程度越来越高&#xff0c;无论是车站还是车内都需要强大、高质量的解决方案以…

【JavaEE】_HTTP响应

目录 1. 首行 2. 报头header 3.空行 4. 正文body 1. 首行 响应首行&#xff1a;版本号状态码状态码描述&#xff1b; HTTP状态码描述了这次响应的结果&#xff08;比如成功、失败&#xff0c;以及失败原因等&#xff09;&#xff1b; 1. HTTP状态码有&#xff1a; &#…

三种vcruntime140.dll丢失解决方法,有效解决vcruntime140.dll文件丢失

vcruntime140.dll作为一个动态链接库文件&#xff0c;具有重要的功能和用途。它是由Microsoft Visual C Redistributable软件包提供的一个重要组件&#xff0c;用于支持运行在Windows操作系统上的应当vcruntime140.dll文件丢失时&#xff0c;将会对计算机系统产生一系列的影响。…

CVE-2022-24652 漏洞复现

CVE-2022-24652 开题 后台管理是thinkphp的&#xff0c;但是工具没检测出漏洞。 登陆后界面如下&#xff0c;上传头像功能值得引起注意 这其实就是CVE-2022-24652&#xff0c;危险类型文件的不加限制上传&#xff0c;是文件上传漏洞。漏洞路由/user/upload/upload 参考文章&a…

如何减少 HTTP 响应的数据大小

资料来源 : 小林coding 小林官方网站 : 小林coding (xiaolincoding.com) 如何减少 HTTP 响应的数据大小? 对于 HTTP 的请求和响应&#xff0c;通常 HTTP 的响应的数据大小会比较大&#xff0c;也就是服务器返回的资源会比较大。 于是&#xff0c;我们可以考虑对响应的资源进…

【蓝桥杯】算法模板题(Floyd算法)

一.弗洛伊德算法 用途&#xff1a;用来求解多源点最短路径问题。 思想&#xff1a;Floyd算法又称为插点法&#xff0c;是一种利用动态规划的思想寻找给定的加权图中多源点之间最短路径的算法。 主要步骤&#xff1a; 1&#xff09;初始化&#xff1a;使用邻接矩阵初始化dis…

并发CPU伪共享及优化

目录 伪共享 解决 伪共享 缓存系统中是以缓存行&#xff08;cache line&#xff09;为单位存储的。缓存行是2的整数幂个连续字节&#xff0c;一般为32-256个字节。最常见的缓存行大小是64个字节。当多线程修改互相独立的变量时&#xff0c;如果这些变量共享同一个缓存行&am…

代码随想录算法训练营第十六天 | 654.最大二叉树、617.合并二叉树、700.二叉搜索树中的搜索、98.验证二叉搜索树

代码随想录算法训练营第十六天 | 654.最大二叉树、617.合并二叉树、700.二叉搜索树中的搜索、98.验证二叉搜索树 文章目录 代码随想录算法训练营第十六天 | 654.最大二叉树、617.合并二叉树、700.二叉搜索树中的搜索、98.验证二叉搜索树1 LeetCode 654.最大二叉树2 LeetCode 61…

04_device_bus_driverLinux内核模块

01_basicLinux内核模块-CSDN博客文章浏览阅读45次。环境IDubuntuMakefilemodules:clean:basic.creturn 0;运行效果。https://blog.csdn.net/m0_37132481/article/details/136157384?csdn_share_tail%7B%22type%22%3A%22blog%22%2C%22rType%22%3A%22article%22%2C%22rId%22%3A%…

c高级 函数+Makefile

一、作业 1.写一个函数&#xff0c;输出当前用户的uid和gid&#xff0c;并使用变量接收结果 #!/bin/bash function fun(){retid -uret1id -gecho $ret $ret1 } retfun echo $ret二、练习回顾 1.分文件编译&#xff08;实现冒泡排序&#xff09; 正确的&#xff1a;将数组的…

<网络安全>《35 网络攻防专业课<第一课 - 网络攻防准备>》

1 主要内容 认识黑客 认识端口 常见术语与命令 网络攻击流程 VMWare虚拟环境靶机搭建 2 认识黑客 2.1 白帽、灰帽和黑帽黑客 白帽黑客是指有能力破坏电脑安全但不具恶意目的黑客。 灰帽黑客是指对于伦理和法律态度不明的黑客。 黑帽黑客经常用于区别于一般&#xff08;正面…