教你手写SPI与FLASH通讯(看完这篇你就会手动写啦,保姆级讲解)---- 2020.3.13

写这篇文章足足肝了我一天时间!!!,不过还算是有点收获,希望这篇文章能够帮助你!!!

关于SPI协议原理方面的文章

嵌入式stm32 复习(工作用)— SPI协议原理知识 2020.3.12
添加链接描述

先上完整SPI与FLASH通讯部分代码!!!

SPI.c部分

#include "spi2.h"

/**
 * @brief  SPI硬件初始化
 * @param  None
 * @retval None
 */
void SPI2_Init(void) {

			//1.时钟
			RCC->APB2ENR |= 1 << 3;//GPIOB时钟
			RCC->APB1ENR |= 1 << 14;//SPI2时钟

			//2.GPIO
			GPIOB->CRH &= ~(0x0F << 20);
			GPIOB->CRH |= 0x0B << 20;//PB13(CLK),推免输出,复用

			GPIOB->CRH &= ~(0x0F << 24);
			GPIOB->CRH |= 0x08 << 24;//PB14(MISO),上拉或下拉输入

			GPIOB->CRH &= ~(0x0F << 28);
			GPIOB->CRH |= 0x0B << 28;//PB15(MOSI),推免输出,复用

			//3.SPI
			//可以使用快速赋值的方式,初始化某一个寄存器
			SPI2->CR1 = 0;
			//3.1 配置波特率
			SPI2->CR1 &= ~(0x07 << 3);//选择Fpcl/2时钟频率
			//3.2 选择时钟极性
			SPI2->CR1 &= ~(1 << 1);//选择时钟空闲低电平
			SPI2->CR1 &= ~(1 << 0);//选择第一个时钟边沿
			//3.3 选择数据传输长度
			SPI2->CR1 &= ~(1 << 11);//8bit数据长度
			//3.4 配置 LSBFIRST 帧格式
			SPI2->CR1 &= ~(1 << 7);//MSB开始,高位在前
			//3.5 配置MSTR
			SPI2->CR1 |= 1 << 2;//配置SPI为主设备
			//3.6 配置硬件管理CS(NSS)
			SPI2->CR2 |= 1 << 2;

			//4.使能 SPE
			SPI2->CR1 |= 1 << 6;//使能SPI

}
/**
 * @brief  SPI2数据读和写方法
 * @param  data:要写的数据
 * @retval u8:读取的数据
 */
u8 SPI2_WR_Byte(u8 data) {

	//1.先判断TxE是否为1
	while (!(SPI2->SR & 0x02))
	;
	SPI2->DR = data;
	//2.判断RXNE是否为1
	while (!(SPI2->SR & 0x01))
	;
	return SPI2->DR;

}

SPI.h部分

#ifndef INC_SPI2_H_
#define INC_SPI2_H_

#include "ext.h"

void SPI2_Init(void);

u8 SPI2_WR_Byte(u8 data);
#endif /* INC_SPI2_H_ */

w25qxx.c部分

#include "w25qxx.h"
#include "spi2.h"
#include "stdlib.h"
#include "string.h"

#define W25QXX_CS	PBout(12)	//W25QXX片选信号线

/**
 * @brief  Flash 的初始化
 * @param  None
 * @retval None
 */
void W25QXX_Init(void) {
	//关于库函数的GPIO配置,这里课上不再重复讲解
	GPIOB->CRH &= ~(0x0F << 16);
	GPIOB->CRH |= 0x03 << 16;	//PB12(CS/NSS),推免输出

	W25QXX_CS = 1;	//CS片选拉高

	SPI2_Init();	//初始化SPI2
}

/**
 * @brief  测试读取W25QXX的Manufacturer / Device ID
 * @param  None
 * @retval Manufacturer / Device ID
 */
u16 W25QXX_GetMDID(void) {

	u16 tmp = 0;

	//1.CS拉低,表示开始
	W25QXX_CS = 0;

	//2.数据操作
	SPI2_WR_Byte(0x90);	//发设备指令 0x90

	SPI2_WR_Byte(0x00);	//连续发送三次0x00
	SPI2_WR_Byte(0x00);
	SPI2_WR_Byte(0x00);

	tmp |= SPI2_WR_Byte(0x00) << 8;	//始终时0xEF
	tmp |= SPI2_WR_Byte(0x00);	//Device ID

	//3.CS拉高,表示结束
	W25QXX_CS = 1;
	return tmp;
}
/**
 * @brief  W25QXX 读取状态寄存器1
 * @param  None
 * @retval u8: BUSY:是否忙碌,WEL:是否可写操作
 */
u8 W25QXX_ReadReg1(void) {

	u8 tmp = 0;
	W25QXX_CS = 0;	//1.CS拉低,表示开始
	SPI2_WR_Byte(0x05);	//读Reg1命令
	tmp = SPI2_WR_Byte(0xFF);
	W25QXX_CS = 1;	//3.CS拉高,表示结束

	return tmp;
}

/**
 * @brief  W25QXX 等待忙碌状态
 * @param  None
 * @retval None
 */
void W25QXX_WaiteBusy(void) {
	while (W25QXX_ReadReg1() & 0x01)
		;
}

/**
 * @brief  W25QXX读取len个数据长度(快读方式)
 * @param  addr:地址,pData:数据指针;len:多少个字节
 * @retval None
 */
void W25QXX_ReadMutiBytes(u32 addr, u8* pData, u16 len) {
	u16 i = 0;

	//1.CS拉低,表示开始
	W25QXX_CS = 0;

	//2.数据操作
	SPI2_WR_Byte(0x0B);	//发读数据命令(Fast Read)

	//连续发送三次0x00
	SPI2_WR_Byte((u8) (addr >> 16));
	SPI2_WR_Byte((u8) (addr >> 8));
	SPI2_WR_Byte((u8) (addr >> 0));

	SPI2_WR_Byte(0xFF);	//假读

	for (i = 0; i < len; i++) {
		pData[i] = SPI2_WR_Byte(0xFF);	//读数据
	}

	//3.CS拉高,表示结束
	W25QXX_CS = 1;
}

/**
 * @brief  W25QXX 写使能
 * @param  None
 * @retval None
 */
void W25QXX_WriteEnble(void) {
	W25QXX_CS = 0;	//1.CS拉低,表示开始
	SPI2_WR_Byte(0x06);	//Write Enable
	W25QXX_CS = 1;	//3.CS拉高,表示结束
}

/**
 * @brief  W25QXX 写不使能
 * @param  None
 * @retval None
 */
void W25QXX_WriteDisable(void) {
	W25QXX_CS = 0;	//1.CS拉低,表示开始
	SPI2_WR_Byte(0x04);	//Write Disable
	W25QXX_CS = 1;	//3.CS拉高,表示结束
}

/**
 * @brief  W25QXX 擦除整个芯片
 * @param  None
 * @retval None
 */
void W25QXX_ChipErase(void) {

	W25QXX_WriteEnble();	//必须先发写使能

	W25QXX_CS = 0;	//1.CS拉低,表示开始
	SPI2_WR_Byte(0xC7);	//擦除整个芯片
	W25QXX_CS = 1;	//3.CS拉高,表示结束
	W25QXX_WaiteBusy();
}

/**
 * @brief  W25QXX 擦除Sector
 * @param  None
 * @retval None
 */
void W25QXX_SectorErase(u32 addr) {
	W25QXX_WaiteBusy();

	W25QXX_WriteEnble();	//必须先发写使能
	W25QXX_CS = 0;	//1.CS拉低,表示开始
	SPI2_WR_Byte(0x20);	//Sector Erase
	//发地址
	SPI2_WR_Byte((addr >> 16) & 0xFF);
	SPI2_WR_Byte((addr >> 8) & 0xFF);
	SPI2_WR_Byte((addr >> 0) & 0xFF);
	W25QXX_CS = 1;	//3.CS拉高,表示结束

	W25QXX_WaiteBusy();
}

/**
 * @brief  W25QXX 擦除Block (32KB)
 * @param  None
 * @retval None
 */
void W25QXX_BlockErase32(u32 addr) {

	W25QXX_WriteEnble();	//必须先发写使能

	W25QXX_CS = 0;	//1.CS拉低,表示开始
	SPI2_WR_Byte(0x52);	//Block Erase (32KB)
	SPI2_WR_Byte((addr >> 16) & 0xFF);
	SPI2_WR_Byte((addr >> 8) & 0xFF);
	SPI2_WR_Byte((addr >> 0) & 0xFF);
	W25QXX_CS = 1;	//3.CS拉高,表示结束
	W25QXX_WaiteBusy();
}

/**
 * @brief  W25QXX Block (64KB)
 * @param  None
 * @retval None
 */
void W25QXX_BlockErase64(u32 addr) {

	W25QXX_WriteEnble();	//必须先发写使能

	W25QXX_CS = 0;	//1.CS拉低,表示开始
	SPI2_WR_Byte(0xD8);	//Block Erase (64KB)
	SPI2_WR_Byte((addr >> 16) & 0xFF);
	SPI2_WR_Byte((addr >> 8) & 0xFF);
	SPI2_WR_Byte((addr >> 0) & 0xFF);
	W25QXX_CS = 1;	//3.CS拉高,表示结束
	W25QXX_WaiteBusy();
}

/**
 * @brief  W25QXX Power-down
 * @param  None
 * @retval None
 */
void W25QXX_PowerDown(void) {
	W25QXX_CS = 0;	//1.CS拉低,表示开始
	SPI2_WR_Byte(0xB9);	//Power-down
	W25QXX_CS = 1;	//3.CS拉高,表示结束
}

/**
 * @brief  W25QXX 写Page
 * @param  None
 * @retval None
 */
void W25QXX_WritePage(u32 addr, u8* pData, u16 len) {

	u16 i = 0;

	W25QXX_WriteEnble();	//先WriteEnable

	W25QXX_CS = 0;	//1.CS拉低,表示开始
	SPI2_WR_Byte(0x02);	//Page Program 命令

	SPI2_WR_Byte((addr >> 16) & 0xFF);
	SPI2_WR_Byte((addr >> 8) & 0xFF);
	SPI2_WR_Byte((addr >> 0) & 0xFF);

	for (i = 0; i < len; i++) {
		SPI2_WR_Byte(pData[i]);
	}

	W25QXX_CS = 1;	//3.CS拉高,表示结束
	W25QXX_WaiteBusy();

}

/**
 * @brief  W25QXX 可以在任意位置写任意数据(如果在经过的Page区域,已经是0xFF,则可以直接进行数据操作)
 * @param  addr:数据地址;pData:要写的数据首地址;len:要写数据的有效长度。
 * @retval None
 */
void W25QXX_WritePageEx(u32 addr, u8* pData, u16 len) {

	u8* pBuffer = pData;
	u16 last = 256 - addr % 256;	//获得当前地址所在Page中的位置到Page尾部的剩余大小空间

	if (last >= len)
		last = len;

	while (1) {
		W25QXX_WritePage(addr, pBuffer, last);

		if (last == len)
			break;
		else {
			pBuffer += last;
			addr += last;

			len -= last;
			if (len > 256)
				last = 256;
			else
				last = len;
		}
	}
}

u8 W25QXX_BUF[4096] = { 0 };	//如果频繁访问读写操作,可开辟缓存区,如果外接SRAM,可以使用动态申请内存
/**
 * @brief  W25QXX 写任意数据
 * @param  addr:数据地址;pData:要写的数据首地址;len:要写数据的有效长度。
 * @retval None
 */
void W25QXX_WriteMutiBytes(u32 addr, u8* pData, u16 len) {

	u16 offset = addr % 4096;
	u16 last = 4096 - offset;
	u16 n = addr / 4096;

	u8* buffer = W25QXX_BUF;	//malloc(4096);

	if (last >= len)
		last = len;

	while (1) {
		u16 i = 0;
		//1.先拷贝数据
		W25QXX_ReadMutiBytes(n * 4096, buffer, 4096);

		for (i = 0; i < 4096; ++i) {
			if (buffer[i] != 0xFF)
				break;
		}

		//如果当有扇区不需要擦除时,写入速度明显提升,可以在进行大容量拷贝时,先将整个Flash擦除后再写入
		if (i != 4096) {
			//2.擦除整个扇区
			W25QXX_SectorErase(n * 4096);
		}

		//3.替换数据(使用C标准库的速度和下面循环的速度大致差不多)
		strncpy((char *) (buffer + offset), (const char *) pData, last);

		//等同于下面步骤
//		for (i = 0; i < last; ++i) {
//			buffer[i + offset] = pData[i];
//		}

		//4.回写数据
		W25QXX_WritePageEx(n * 4096, buffer, 4096);

		if (last == len)
			break;		//递归终止条件

		n++;			//每次扇区地址向后偏移1

		pData += last;	//数据指针向后偏移last
		offset = 0;		//从第二次开始offset变成0
		addr += last;	//地址向后偏移last

		if (len > 4096)
			last = 4096;
		else
			last = len;

	}

//	free(buffer);
}

w25qxx.h部分

#ifndef INC_W25QXX_H_
#define INC_W25QXX_H_

#include "ext.h"


void W25QXX_Init(void);
u16 W25QXX_GetMDID(void);

void W25QXX_ReadMutiBytes(u32 addr, u8* pData, u16 len);
void W25QXX_WriteMutiBytes(u32 addr, u8* pData, u16 len);

void W25QXX_Write(u8* pBuffer, u32 address, u16 len);

void W25QXX_ChipErase(void);

#endif /* INC_W25QXX_H_ */

好!按照老样子,接下来开始详细讲解每行代码的用处,以及为什么这样写!

SPI.c部分
SPI.c初始化部分

//1.时钟
RCC->APB2ENR |= 1 << 3;//GPIOB时钟
RCC->APB1ENR |= 1 << 14;//SPI2时钟

//在这里插入图片描述//因为这里stm32与flash通讯的接口是GPIOB端口,所以需要开启GPIOB的时钟和SPI2的时钟。
//
在这里插入图片描述
//在这里插入图片描述//
在这里插入图片描述
//由前几篇的IIC协议文章中设置一样,所以这里就不再赘述。

//2.GPIO
	GPIOB->CRH &= ~(0x0F << 20);
	GPIOB->CRH |= 0x0B << 20;//PB13(CLK),推免输出,复用

//
在这里插入图片描述
//因为SCK端口是PB13,所以位于端口配置高寄存器。
//由上一篇文章的SPI协议可以得知:SCK是由波特率产生器产生的的,是产生固定时钟周期的。,所以这个端口需要设置成复用推免输出模式,则设置这四位为1011,运算如下:
//但是首先先将这四位清零,再赋值。
在这里插入图片描述

GPIOB->CRH &= ~(0x0F << 24);
GPIOB->CRH |= 0x08 << 24;//PB14(MISO),上拉或下拉输入

//由上篇文章可以得知:MISO:只能由从设备向主设备发数据,所以这里将MISO引脚设置成上拉或者下拉输入。
//
在这里插入图片描述

GPIOB->CRH &= ~(0x0F << 28);
GPIOB->CRH |= 0x0B << 28;//PB15(MOSI),推免输出,复用

//由上篇文章可以得知:MOSI:只能由主设备向从设备发数据,所以这里将MOSI引脚设置成推免输出,复用。
//
在这里插入图片描述

//3.SPI
//3.1 配置波特率
SPI2->CR1 &= ~(0x07 << 3);//选择Fpcl/2时钟频率

//
在这里插入图片描述//
在这里插入图片描述//从上图可以得知,波特率控制是由fPCLK决定的。
//所以我们就得知道fPCLK是多大频率。
//
在这里插入图片描述//所以这里fPCLK为36MHz,所以SPI2的最大波特率是18MHz。这里设置最大,即BR[2:0]这三位全是0,那么设置的时候先全置1,然后再取反。
//
在这里插入图片描述

//3.2 选择时钟极性
SPI2->CR1 &= ~(1 << 1);//选择时钟空闲低电平
SPI2->CR1 &= ~(1 << 0);//选择第一个时钟边沿

//由上一篇文章我们可以知道有两种时钟空闲状态。
//高电平空闲:是从低电平到高电平,发送或者接收数据。
//低电平空闲:是从高电平到低电平,发送或者接收数据。
//那么我们这里采用的是低电平空闲,也就相当于电平从低电平变成高电平时,才会传输数据。
//
在这里插入图片描述//在这里插入图片描述//从图可以得知,数据采样从第二个时钟边沿开始。

//3.3 选择数据传输长度
SPI2->CR1 &= ~(1 << 11);//8bit数据长度

//
在这里插入图片描述//一般我们传输数据时是以8位数据格式进行发送或者接收的,并且是第11位。

//3.4 配置 LSBFIRST 帧格式
SPI2->CR1 &= ~(1 << 7);//MSB开始,高位在前

//在这里插入图片描述
//举个例子,如上图所示,传输数据的时候总是高位在前。
//在这里插入图片描述

//3.5 配置MSTR
SPI2->CR1 |= 1 << 2;//配置SPI为主设备

在这里插入图片描述

//3.6 配置硬件管理CS(NSS)
SPI2->CR2 |= 1 << 2;

在这里插入图片描述在这里插入图片描述

//4.使能 SPE
SPI2->CR1 |= 1 << 6;//使能SPI

//在这里插入图片描述

SPI.c部分
SPI2数据读和写部分

//1.先判断TxE是否为1
while (!(SPI2->SR & 0x02));

//在这里插入图片描述//由上图我们可以知道,只有当TXE标志被置位时,数据才会从发送缓冲区到移位寄存器,所以这里先判断TxE是否为1。
//在这里插入图片描述//在这里插入图片描述//在这里插入图片描述

SPI2->DR = data;

//当TXE置1时,才开始发数据。
//在这里插入图片描述

//2.判断RXNE是否为1
while (!(SPI2->SR & 0x01));

//在这里插入图片描述// 传送移位寄存器里的数据到接收缓冲器,并且RXNE标志被置位,也就相当于RXNE位为1时,传送移位寄存器的数据才能到接收寄存器中。
//在这里插入图片描述
//
在这里插入图片描述//所以这里就相当于主设备向从设备发数据的过程中,主设备同时也在接收从设备发过来的数据,还记不记得我们在关于IIC协议文章讲到的IIC协议通讯是半双工通信,发送或者接收数据只能单方向,但是SPI协议就不是半双工通讯方式了,而是全双工通讯,又因为IIC传输数据只在一根线上传输,但是SPI传输数据是在两条线上传输的,所以这也是为什么SPI传输数据的速度比IIC传输数据快的原因了!!!

w25qxx.c部分
w25qxx(Flash)初始化部分

GPIOB->CRH &= ~(0x0F << 16);
GPIOB->CRH |= 0x03 << 16;	//PB12(CS/NSS),推免输出
W25QXX_CS = 1;	//CS片选拉高
SPI2_Init();	//初始化SPI2

//在这里插入图片描述//因为在每次通讯的时候,刚开始CS片选信号线电平状态总是从高电平变到低电平,所以这也是我们为什么设置CS片选信号不是在SPI初始化里面设置,而是在SPI初始化之前进行设置的,并且将CS片选信号线电平状态置1。
//设置CS片选信号的时候也是先清零然后再赋值。

w25qxx.c部分
w25qxx( 测试读取W25QXX的Manufacturer / Device ID)部分

#define W25QXX_CS	PBout(12)	//W25QXX片选信号线

//因为已知CS片选信号线是PB12,并且是输出模式,这个操作相当于与IIC协议中SCL,SDA信号线的电平状态设置相同,也是采用宏定义的方式。

u16 tmp = 0;
//1.CS拉低,表示开始
W25QXX_CS = 0;

//这里就是将CS片选信号线拉低,这样才能开始传输数据。相当于IIC协议中的起始条件和结束条件。

//2.数据操作
SPI2_WR_Byte(0x90);	//发设备指令 0x90

SPI2_WR_Byte(0x00);	//连续发送三次0x00
SPI2_WR_Byte(0x00);
SPI2_WR_Byte(0x00);

tmp |= SPI2_WR_Byte(0x00) << 8;	//始终时0xEF
tmp |= SPI2_WR_Byte(0x00);	//Device ID

//3.CS拉高,表示结束
W25QXX_CS = 1;

//在这里插入图片描述//由上图可知首次CS片选信号线拉低,开始传输数据,然后主设备开始向从设备发送0x90,同时没有接收到数据,然后再发送24位数据,分别是3次8位数据,都是0x00,注意是先发最高位,然后再随机发送一个数据,0x00也行,然后就接收到一个制造商ID地址0xEF数据,再然后就能接收到正确的设备ID了。
//发送和接收数据是同步进行的。
//因为tmp数据是16位的,同时接收制造商ID和设备ID地址,且制造商ID是高8位,设备ID地址是低8位。
//最后再片选拉高!!!

w25qxx.c部分
W25QXX读取len个数据长度(快读方式)部分

u16 i = 0;

//1.CS拉低,表示开始
W25QXX_CS = 0;

//跟往常一样,片选拉低

//2.数据操作
SPI2_WR_Byte(0x0B);	//发读数据命令(Fast Read)

//连续发送三次0x00
SPI2_WR_Byte((u8) (addr >> 16));
SPI2_WR_Byte((u8) (addr >> 8));
SPI2_WR_Byte((u8) (addr >> 0));

//
在这里插入图片描述//由上图可知,先把CS片选信号线拉低,然后开始传输数据,主设备开始向从设备发送0x0B,然后继续发24位数据,此时没有成功接收数据,然后再等待8个时钟周期,最后主设备随机发送数据到从设备,此时就能成功接收数据并且是一直能够接收,直到数据接收完成。
//在这里插入图片描述//然后依次类推,成功发送24位数据到从设备。

SPI2_WR_Byte(0xFF);	//假读

//然后再等待8个时钟周期,相当于发送8位数据,只不过是接收不到数据的。

for (i = 0; i < len; i++) {
	pData[i] = SPI2_WR_Byte(0xFF);	//读数据
}

//然后就可以成功接收数据了,这里就是一个for循环。
//指针就是数组,数组就是指针。

//3.CS拉高,表示结束
W25QXX_CS = 1;

//CS拉高,表示结束

w25qxx.c部分
W25QXX写page部分
在这里插入图片描述//W25Q80/16/32阵列被组织成4,096/8,192/16,384个可编程页,每个页256字节。
//使用Page程序指令一次最多可编程256个字节。可以以16组(扇区擦除)、128组(32KB块擦除)、256组(64KB块擦除)或整个芯片(芯片擦除)的方式擦除页面。
//W25Q80/16/32有256/512/1024个可擦扇区和16/32/64个可擦块。

//在这里插入图片描述//不同型号的w25qxx主要区别就在于页数不同,比如w25q80有16个block,w25q16有32个block,w25q32有64个block。
//Flash内存结构中最小单元是page(页),所以进行主设备写数据到从设备的话,需要进行页操作。

在这里插入图片描述
//页程序指令允许从一个字节到256字节(一页)的数据在先前擦除(FFh)的内存位置被编程。
//写使能指令必须在设备接收擦除指令之前执行(状态寄存器位WEL= 1)。
//如果要对整个256字节的页面进行编程,则最后一个地址字节(8个最不重要的地址位)应该设置为0。
//如果最后一个地址字节不为零,并且时钟的数量超过了剩余的页面长度,则寻址将自动换行到页面的开头。
//在某些情况下,小于256字节(部分页面)可以在不影响同一页面内的其他字节的情况下进行编程。

w25qxx.c部分
W25QXX写使能部分

//既然我们写数据的时候会用到写使能和读取状态等函数,那么我们就在写数据之前先把这几个函数封装好。

W25QXX_CS = 0;	//1.CS拉低,表示开始
SPI2_WR_Byte(0x06);	//Write Enable
W25QXX_CS = 1;	//3.CS拉高,表示结束

//首先我们还是先把CS拉低,表示开始,然后主设备发送一个指令到从设备,代表已经成功写使能,最后CS拉高,表示结束。
//
在这里插入图片描述w25qxx.c部分
W25QXX写不使能部分

W25QXX_CS = 0;	//1.CS拉低,表示开始
SPI2_WR_Byte(0x04);	//Write Disable
W25QXX_CS = 1;	//3.CS拉高,表示结束

//在这里插入图片描述//和写使能原理一样,只是主设备向从设备发送的数据不同罢了。

w25qxx.c部分
W25QXX读取状态寄存器部分
//在这里插入图片描述
//BUSY是状态寄存器(S0)中的一个只读位,它在设备执行a时被设置为1状态
页程序,扇区擦除,块擦除,芯片擦除或写状态寄存器指令
//当程序、擦除或写状态寄存器指令完成后,忙位将被清除为0状态,表示设备已准备好接受进一步的指令。
//在这里插入图片描述

W25QXX_CS = 0;	//1.CS拉低,表示开始
SPI2_WR_Byte(0x05);	//读Reg1命令
tmp = SPI2_WR_Byte(0xFF);
W25QXX_CS = 1;	//3.CS拉高,表示结束

//写使能指令将状态寄存器中的写使能锁存器(WEL)位设置为1

w25qxx.c部分
W25QXX等待忙碌状态部分

while (W25QXX_ReadReg1() & 0x01);

//这里就是如果是忙碌状态未完成,那么busy是1,那么上述操作就是会一直在while循环里,直到忙碌状态完成。

w25qxx.c部分
W25QXX擦除整个芯片

W25QXX_WriteEnble();	//必须先发写使能

//一个写使能指令必须在设备接受芯片擦除指令之前执行(状态)。

W25QXX_CS = 0;	//1.CS拉低,表示开始
SPI2_WR_Byte(0xC7);	//擦除整个芯片
W25QXX_CS = 1;	//3.CS拉高,表示结束

//这里就不再赘述。

W25QXX_WaiteBusy();

//等待擦除命令完成。

w25qxx.c部分
W25QXX擦除扇区

W25QXX_WriteEnble();	//必须先发写使能

//还是在执行擦除命令之前先进行写使能

W25QXX_CS = 0;	//1.CS拉低,表示开始
SPI2_WR_Byte(0x20);	//Sector Erase
//发地址
SPI2_WR_Byte((addr >> 16) & 0xFF);
SPI2_WR_Byte((addr >> 8) & 0xFF);
SPI2_WR_Byte((addr >> 0) & 0xFF);
W25QXX_CS = 1;	//3.CS拉高,表示结束

//这个发地址就相当于是只取8位数据,然后发3遍,总共24位。

W25QXX_WaiteBusy();

//然后等待擦除命令完成

w25qxx.c部分
W25QXX擦除块(32KB)

W25QXX_WriteEnble();	//必须先发写使能

//还是在执行擦除命令之前先进行写使能

W25QXX_CS = 0;	//1.CS拉低,表示开始
SPI2_WR_Byte(0x52);	//Block Erase (32KB)
SPI2_WR_Byte((addr >> 16) & 0xFF);
SPI2_WR_Byte((addr >> 8) & 0xFF);
SPI2_WR_Byte((addr >> 0) & 0xFF);
W25QXX_CS = 1;	//3.CS拉高,表示结束

//这个发地址就相当于是只取8位数据,然后发3遍,总共24位。

W25QXX_WaiteBusy();

//然后等待擦除命令完成

w25qxx.c部分
W25QXX擦除块(64KB)

W25QXX_WriteEnble();	//必须先发写使能

W25QXX_CS = 0;	//1.CS拉低,表示开始
SPI2_WR_Byte(0xD8);	//Block Erase (64KB)
SPI2_WR_Byte((addr >> 16) & 0xFF);
SPI2_WR_Byte((addr >> 8) & 0xFF);
SPI2_WR_Byte((addr >> 0) & 0xFF);
W25QXX_CS = 1;	//3.CS拉高,表示结束
W25QXX_WaiteBusy();

//这里不再赘述。

w25qxx.c部分
W25QXX写Page

void W25QXX_WritePage(u32 addr, u8* pData, u16 len)
//这里的 u16 为什么不是u8类型的呢,主要是u8类型的话,就是0~255,而一页是256个字节,所以这里是u16类型。
//在这里插入图片描述

u16 i = 0;

W25QXX_WriteEnble();	//先WriteEnable

W25QXX_CS = 0;	//1.CS拉低,表示开始
SPI2_WR_Byte(0x02);	//Page Program 命令

SPI2_WR_Byte((addr >> 16) & 0xFF);
SPI2_WR_Byte((addr >> 8) & 0xFF);
SPI2_WR_Byte((addr >> 0) & 0xFF);

//这里不再赘述。

for (i = 0; i < len; i++) {
	SPI2_WR_Byte(pData[i]);
}

//这里就是把需要写入的数据成功写到从设备当中。

W25QXX_CS = 1;	//3.CS拉高,表示结束
W25QXX_WaiteBusy();

//这里不再赘述。

w25qxx.c部分
W25QXX写Page(可以在任意位置写任意数据)

//在之前也说过了:如果最后一个地址字节不为零,并且时钟的数量超过了剩余的页面长度,则寻址将自动换行到页面的开头

u8* pBuffer = pData;

//先把所写数据的首地址赋值给新的指针变量,存放地址。

u16 last = 256 - addr % 256;	

//获得当前地址所在Page中的位置到Page尾部的剩余大小空间

if (last >= len)
	last = len;

while (1) {
	W25QXX_WritePage(addr, pBuffer, last);

	if (last == len)
		break;
	else {
		pBuffer += last;
		addr += last;

		len -= last;
		if (len > 256)
			last = 256;
		else
			last = len;

//
在这里插入图片描述//在这里插入图片描述//看完上这两张图就看清楚了。

w25qxx.c部分
W25QXX写Page(可以在任意位置写任意数据)如果频繁访问读写操作,可开辟缓存区,如果外接SRAM,可以使用动态申请内存

u8 W25QXX_BUF[4096] = { 0 };	

//如果频繁访问读写操作,可开辟缓存区,如果外接SRAM,可以使用动态申请内存

//一般如果写数据之前没有被擦除,相当与里面的数据不全是1的话,那么就先进行擦除,然后再写数据。

//扇区擦除指令将指定扇区(4k字节)内的所有内存设置为所有1 (FFh)的擦除状态。

//
在这里插入图片描述//在这里插入图片描述

u16 offset = addr % 4096;
u16 last = 4096 - offset;
u16 n = addr / 4096;

//如上图所示,n代表前面有多少个扇区。

u8* buffer = W25QXX_BUF;	//malloc(4096);

//将缓冲区赋值给新的指针变量。

if (last >= len)
	last = len;

//同理,不再赘述。

W25QXX_ReadMutiBytes(n * 4096, buffer, 4096);

//将在扇区中的数据读出来

for (i = 0; i < 4096; ++i) {
	if (buffer[i] != 0xFF)
		break;
}

//如果读出来原来的扇区里面的数据不都是1,则停止,相当于该扇区需要进行擦除操作。

if (i != 4096) {
	//2.擦除整个扇区
	W25QXX_SectorErase(n * 4096);
}

//如果当有扇区不需要擦除时,写入速度明显提升,可以在进行大容量拷贝时,先将整个Flash擦除后再写入

	//3.替换数据(使用C标准库的速度和下面循环的速度大致差不多)
for (i = 0; i < last; ++i) {
	buffer[i + offset] = pData[i];
}

//把需要写的数据放到缓冲区中

//4.回写数据
W25QXX_WritePageEx(n * 4096, buffer, 4096);

//回写数据

if (last == len)
	break;		//递归终止条件

//这里不再赘述。

n++;			

//每次扇区地址向后偏移1

pData += last;	//数据指针向后偏移last
offset = 0;		//从第二次开始offset变成0
addr += last;	//地址向后偏移last

//从第二次开始offset变成0

if (len > 4096)
	last = 4096;
else
	last = len;

//这里就不再赘述了。

结束语

个人认为大家如果细心看完这篇文章,并且结合上一篇文章一起看(在文章的刚开始会将前几篇关于SPI协议原理部分的文章链接发出来),我相信大家会彻底掌握SPI与Flash通讯了!!!如果觉得这篇文章还不错的话,记得点赞 ,支持下!!!

以后我会继续推出关于嵌入式(stm32)的协议方面的讲解,下一讲会推出DMA部分的文章!敬请期待!!!

**我先休息去了~~╭(╯^╰)╮

发布了27 篇原创文章 · 获赞 56 · 访问量 4475

猜你喜欢

转载自blog.csdn.net/qq_40544107/article/details/104833464