STM32工作笔记0068---SPI同步通信Flash读写实验

技术交流QQ群【JAVA,C++,Python,.NET,BigData,AI】:170933152 

复习一下

要知道SPI,每接收到一个数据,实际上是也发送了一个数据,然后

还要知道:

主机可以设置片选,通过片选连接从机,然后

设置对应的一个从机拉低,这个时候,这个对应的从机就会工作,其他的从机拉高,这个时候

其他的从机就会不工作,这样的话,就是可以实现一个SPI控制多个设备的目标.

这里要注意,第3点,主机要发送数据给从机的同时,实际上这个时候,也会从从机收到数据.

这里,如果只进行写操作的话,那么主机只需要关注写就可以了,不用关注读数据,从机发过来的数据直接忽略就可以了.

如果主机要读数据的话,那么由于驱动,是由主机进行驱动的,所以这里主机要接收数据,就要发送一个空字节来,引发从机的

传输.其实就是产生一个时钟.

这里这个时钟和相位,上一讲已经说了,这里的

CPOL是控制空闲状态的电平,CPOL为1的时候,空闲状态的电平是高电平

CPOL为0的时候,空闲状态的电平是低电平

CPHA=0的时候是从第一个边沿来采集,CPHA=1的时候是从第二个边沿来采集.

SPI对应的引脚.

文档中也有可以看到战舰和精英版的引脚对应关系.

注意这里的片选可以由软件设置,实际上就是设置了一个高低电平

下面是mini版的,引脚连接情况.

看一下这些函数.

void SPI_I2S_DeInit(SPI_TypeDef* SPIx);

void SPI_Init(SPI_TypeDef* SPIx, SPI_InitTypeDef* SPI_InitStruct);

//这个用来设置是主机还是从机,数据帧是8位还是16位,

//然后LSB在前还是在后

void SPI_Cmd(SPI_TypeDef* SPIx, FunctionalState NewState);//还有SPI的使能,要使能哪个SPI

void SPI_I2S_ITConfig(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT, FunctionalState NewState);//要开启中断.

void SPI_I2S_DMACmd(SPI_TypeDef* SPIx, uint16_t SPI_I2S_DMAReq, FunctionalStateNewState);

//还可以通过DMA来传输数据.

void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data);//是发送数据

uint16_t SPI_I2S_ReceiveData(SPI_TypeDef* SPIx);//接收数据

void SPI_DataSizeConfig(SPI_TypeDef* SPIx, uint16_t SPI_DataSize);//8位还是16位.

//下面是4个状态.

FlagStatus SPI_I2S_GetFlagStatus(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG);

void SPI_I2S_ClearFlag(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG);

ITStatus SPI_I2S_GetITStatus(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT);

void SPI_I2S_ClearITPendingBit(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT);

这个函数,前面一讲也说了.

配置相关引脚的复用功能,使能SPIx时钟

   设置对应的spi串口的,对应的IO口,这里选择哪个IO口什么的

    void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct);

初始化SPIx,设置SPIx工作模式

  设置是主机,还是从机,用什么极性什么的

    void SPI_Init(SPI_TypeDef* SPIx, SPI_InitTypeDef* SPI_InitStruct);

使能SPIx

    使能SPI口

    void SPI_Cmd(SPI_TypeDef* SPIx, FunctionalState NewState);

    经过前面的3步就可以使用SPI口了.

SPI传输数据

    void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data);

    uint16_t SPI_I2S_ReceiveData(SPI_TypeDef* SPIx) ;

查看SPI传输状态

   SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_RXNE);

这里会先讲解SPI的代码.

然后还会去讲解这个W25Q128这个芯片的指令的用法.

这个W25Q128就是个flash存储器.

然后打开代码看看,

再FWLib中首先要添加进来spi.c这个文件

然后

这里SPI2_Init这个是初始化SPI2的函数.这个函数是咱们自己写的

这里我们就用的SPI2.

注意,这里咱们自己写的函数SPI2_Init()这个初始化函数.

然后这个函数要注意和系统提供的初始化函数,不一样

stm32f10x_spi.h这个是系统的文件,这里提供的函数名字是

SPI_Init()这个跟咱们写的是不一样的.为了区分,因为咱们用的SPI2所以这里

叫SPI2_Init();

这里面的初始化:


//以下是SPI模块的初始化代码,配置成主机模式,访问SD Card/W25Q64/NRF24L01
//SPI口初始化
//这里针是对SPI3的初始化

void SPI2_Init(void)
{
	GPIO_InitTypeDef GPIO_InitStructure;
	SPI_InitTypeDef SPI_InitStructure;
    
    //1.这里首先要使能PORTB时钟使能,然后还要使能SPI
    //SPI2的时钟使能
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //PORTB时钟使能
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI3, ENABLE);  //SPI2时钟使能

    //2.然后这里因为SPI2用的是PB13,PB14,PB15
    //所以写上对应的引脚,然后输入输出模式,然后再去初始化
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //PB13/14/15复用推挽输出
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStructure); //初始化GPIOB


}

这里设置的是复用推挽输出.

至于怎么输出的话,咱们需要:参照这个画面

这里,先初始化这3个引脚,这里默认上拉,这3个引脚,也就是设置为高电平

SetBits

然后这里,再去初始化SPI口

	SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;	 //设置SPI单向或者双向的数据模式:SPI设置为双线双向全双工
	SPI_InitStructure.SPI_Mode = SPI_Mode_Master;						 //设置SPI工作模式:设置为主SPI
	SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;					 //设置SPI的数据大小:SPI发送接收8位帧结构
	SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;							 //串行同步时钟的空闲状态为高电平
	SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;						 //串行同步时钟的第二个跳变沿(上升或下降)数据被采样
	SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;							 //NSS信号由硬件(NSS管脚)还是软件(使用SSI位)管理:内部NSS信号有SSI位控制
	SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_256; //定义波特率预分频的值:波特率预分频值为256
	SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;					 //指定数据传输从MSB位还是LSB位开始:数据传输从MSB位开始
	SPI_InitStructure.SPI_CRCPolynomial = 7;							 //CRC值计算的多项式
	SPI_Init(SPI3, &SPI_InitStructure);									 //根据SPI_InitStruct中指定的参数初始化外设SPIx寄存器

可以看到设置的口,去定义可以看到

#define SPI_Direction_2Lines_FullDuplex ((uint16_t)0x0000)  //2线全双工
#define SPI_Direction_2Lines_RxOnly     ((uint16_t)0x0400)  //2线接收
#define SPI_Direction_1Line_Rx          ((uint16_t)0x8000)  //1线接收
#define SPI_Direction_1Line_Tx          ((uint16_t)0xC000)  //1线发送
#define IS_SPI_DIRECTION_MODE(MODE) (((MODE) == SPI_Direction_2Lines_FullDuplex) || \
                                     ((MODE) == SPI_Direction_2Lines_RxOnly) || \
                                     ((MODE) == SPI_Direction_1Line_Rx) || \
                                     ((MODE) == SPI_Direction_1Line_Tx))

这里设置为2线全双工.

然后:

	SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;	 //设置SPI单向或者双向的数据模式:SPI设置为双线双向全双工
    //1.这里设置是主机模式还是从机模式.
	SPI_InitStructure.SPI_Mode = SPI_Mode_Master;						 //设置SPI工作模式:设置为主SPI
    //2.然后这里确认数据帧是8位的还是16位的数据帧
	SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;					 //设置SPI的数据大小:SPI发送接收8位帧结构
    //3.这里设置CPOL就是空闲状态是高电平还是低电平也就是极性
	SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;							 //串行同步时钟的空闲状态为高电平
    //4.这个设置相位,也就是CPH1=1的时候
    //这里也就是2Edge,也就是从第二个边沿开始采样.
    //这里其实就是上升沿的时候去采集.
	SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;						 //串行同步时钟的第二个跳变沿(上升或下降)数据被采样
    //5.然后这里的片选,片选这里我们用选择用软件来控制
	SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;							 //NSS信号由硬件(NSS管脚)还是软件(使用SSI位)管理:内部NSS信号有SSI位控制
    //6.然后波特率的话,我们设置为256,这个是最低的一个预分频的值.
    //咱们知道这个SPI2的时钟来至于APB2,那么这里
    //这里这个预分频系数有,4,8,16,32,..这个预分频系数越大,那么
    //SPI2的时钟频率就越小.
	SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_256; //定义波特率预分频的值:波特率预分频值为256
    //7.这个起始位是MSB还是LSB,这个上一讲已经讲过了.
	SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;					 //指定数据传输从MSB位还是LSB位开始:数据传输从MSB位开始
    //8.然后这个CRC计算,这个是校验相关的.
	SPI_InitStructure.SPI_CRCPolynomial = 7;							 //CRC值计算的多项式
    //9.这样设置好参数就可以初始化SPI2了
	SPI_Init(SPI3, &SPI_InitStructure);									 //根据SPI_InitStruct中指定的参数初始化外设SPIx寄存器

初始化SPI2口以后,然后就可以去使能SPI外设了.

使能以后就可以去发送数据了.

这里

    SPI_Cmd(SPI3, ENABLE); //使能SPI外设

    SPI3_ReadWriteByte(0xff); //启动传输

//这个实际上写了8个1过去,这个叫启动传输,许多人不理解.

这个是因为前面说过,这里使能SPI以后,这里要向SPI串行寄存器中写入一个字节8个位来发起一次传输.

所以这里就是写入了1111 1111 8个位.

用来启动传输.

这个函数,是用来配置速度的,这个通过设置预分频系数可以设置速度.

因为预分频系数越小,这个波特率越大,传输速度越快.

然后再看一下这个函数,可以看到,这个函数,先去检测,发送缓存空标志位是否是0,如果是0的话,说明没有,没有发送数据,

也就是没有读取到,数据,那么连续200次没有读取到数据,就返回个0

然后如果读取到数据了,那么说明有发送的数据,这里可以发送数据,调用SendData

然后,再去检测,接收缓存非空标志位是不是0,如果,是0 的话,说明是空的,

非空标志位是1的时候,是非空的,是0的时候说明是空的.

空的话,那么就继续检测,连续检测200次以后,如果还是空,就返回0

不为空的话,就去接收数据,然后返回接收到的数据.

可以看到这个发送和接收数据的方法,实际上都是操作的DR寄存器.这个数据寄存器.

然后可以看看下面这个图也是这样,只要发送了数据,就会接收数据,因为这个

是环形的.主从机.

那么这里我们要用这个

SPI2去操作什么呢,这里我们用SPI2去操作这个

W25Q128,对于战舰版和精英版.

对于mini版,是操作W25Q64这个flash.

然后这个是这个flash的介绍,上一讲也说过了.

比如这里是W25Q128芯片的数据手册,可以看到里面也有这个图

记录block和扇区的.

sector是扇区,一个扇区是4kb

这里要注意,我们知道这个每个扇区是4k个字节,那么,这里我们在写某个地址之前要保证

这个地址是0XFF,也就是,如果不是0XFF的话,那么就先要擦除这个整个扇区,然后才能再去写数据.

那么我们是怎么保证,我们去擦除扇区,然后又对原来的数据,不要产生影响呢,

是这样的:

这里我们首先要去开辟一个4k的缓存区,比如这里我要

先去声明一个4k的数组,这样也行.

然后

是这样的,我们先定义一个4K字节的缓存区,这个缓存区可以是个数组,然后,在擦除这个sector扇区之前

首先把这个扇区里面的数据先放到这个4k的缓存中去,然后,再往这个缓存中对应的地址,去写入

本次要写入的数据,然后再一次把这个缓存中的数据,写入到这个已经擦除的扇区中去.

然后去看一下这个SPI Flash的操作.

这个文档是一些指令.

这些指令就是提供了怎么操作这个SPI Flash的一些指令.

操作Flash的的这个文件是在

W25QXX.H这个文件中被定义的.

 

上面还定义了一些指令表,这个指令表,实际上是对应了文档中的指令.

比如这里的Write Enable这个写使能对应的指令就是06h,然后

这里还定义了一些,比如这里,对应不同容量的W25Q80...等,有不同的id,所以这里对

不同的容量的flash芯片都做了一个定义.

然后这个W25QXX_CS这个,前面咱们说PB12是战舰版的片选,那么这里就是定义的这个片选信号,

这里设置为1就是取消片选,0的时候咱们前面说就是拉低,也就是片选.

然后来看一下这个Flash的初始化,可以看到这里,首先去使能了这个GPIOB,为什么要使能这个GPIOB 呢?

是因为,前面咱们说用到了这个PB12这个IO口作为片选接口了,所以这里要使能GPIOB,然后

这里有一句:

GPIO_SetBits(GPIOB,GPIO_Pin_12),也就是给PB12设置了高电平,也就是说取消片选的意思,

这句话的意思和

W25QXX_CS=1;实际上是一样的作用.都是做个初始化,把对应的IO口,先取消片选,也就是不让他工作

要让他工作的时候,再把电平拉低就可以了.

然后再去设置SPI2的速度,也就是预分频系数设置成最小的.因为这里我们要读写数据

36 /2 = 18 APB2的时钟是36m然后/2 是18mhz的频率.

所以要求速度要快一些.

可以看到最后调用的这个W25QXX_ReadID()这样一个函数,这个函数

可以用来确定W25QXX_ReadID()也就是确定flash的类型.

然后接下来看一下,操作flash的各个函数.

先看一下这个W25QXX_ReadSR这个函数.

//读取W25QXX的状态寄存器
//BIT7  6   5   4   3   2   1   0
//SPR   RV  TB BP2 BP1 BP0 WEL BUSY
//SPR:默认0,状态寄存器保护位,配合WP使用
//TB,BP2,BP1,BP0:FLASH区域写保护设置
//WEL:写使能锁定
//BUSY:忙标记位(1,忙;0,空闲)
//默认:0x00
u8 W25QXX_ReadSR(void)
{
    //1.首先byte=0,用来接收读取的字节
	u8 byte = 0;
    //2.然后这里W25QXX_CS,片选要拉低,因为要用片选了.
	W25QXX_CS = 0;							//使能器件
    //3.然后发送发送一个指令,这个指令就是对应了W25X芯片的指令
	SPI3_ReadWriteByte(W25X_ReadStatusReg); //发送读取状态寄存器命令
    //4.然后去读取一个自己的数据.
	byte = SPI3_ReadWriteByte(0Xff);		//读取一个字节
	W25QXX_CS = 1;							//取消片选
	return byte;
}

可以看到这个W25Q芯片的指令,对应的是W25Q芯片的这个指令表.

可以去看看W25Q芯片的指令手册:

可以看到这里,先去写入这个05H的指令,然后就可以去读取这个状态寄存器.

可以看到这里首先咱们先写入了一个05H这样一个指令,要知道写的同时,其实也可以读取一个

指令.只不过这里我们没有读,只是在下一个周期才去读.

可以看到,这里


//读取W25QXX的状态寄存器
//BIT7  6   5   4   3   2   1   0
//SPR   RV  TB BP2 BP1 BP0 WEL BUSY
//SPR:默认0,状态寄存器保护位,配合WP使用
//TB,BP2,BP1,BP0:FLASH区域写保护设置
//WEL:写使能锁定
//BUSY:忙标记位(1,忙;0,空闲)
//默认:0x00
u8 W25QXX_ReadSR(void)
{
	u8 byte = 0;
	W25QXX_CS = 0;							//使能器件
    //1.这里去写入05h这个命令实际上也读取到了一个字节
    //因为咱们说flash,写入的时候就会读取到一个字节
	SPI3_ReadWriteByte(W25X_ReadStatusReg); //发送读取状态寄存器命令
    //2.然后这里发送一个空字节,目的是为了触发读取,因为
    //咱们知道他有两个寄存器,组成了一个环形的结构.
    //这样的方式就读取到了状态寄存器的值.
	byte = SPI3_ReadWriteByte(0Xff);		//读取一个字节
	W25QXX_CS = 1;							//取消片选
	return byte;
}

可以看到,还有写状态寄存器

写状态寄存器是这个:

这个指令.

去文档中去看一下这个指令.

可以看到这里先去发送01h指令,然后再去写入数据.

这类可以看到,这里是没有读的,要注意,写的时候,就可以直接去写,虽然也会接收但是,不用理会

但是读取的时候,要先去写一个空字节0xff,来触发读才行.

然后这个Write_Enable()这个函数,

可以看到这个函数调用

06h这个指令.

然后

可以看到这个只需要写一个指令给他就可以了,不需要其他操作.

另外的像W25QXX_ReadID(void)

这个函数

这个很简单,就不说了,然后主要说一下

看一下这个W25QXX_Erase_Sector()

这个擦除扇区的指令.

先去看一下擦除扇区的指令.

是0x20

然后,文档中:

可以看到这里擦除一个扇区,需要首先发送20h这个擦除指令,然后再去选择擦除哪个扇区,这个怎么去选择擦除哪个扇区,实际

上是选择哪个地址指定的.

//擦除一个扇区
//Dst_Addr:扇区地址 根据实际容量设置
//擦除一个山区的最少时间:150ms
void W25QXX_Erase_Sector(u32 Dst_Addr)
{
	//监视falsh擦除情况,测试用
	printf("fe:%x\r\n", Dst_Addr);
	Dst_Addr *= 4096;
    //1.要擦除的话,首先要使能
	W25QXX_Write_Enable(); //SET WEL
    //2.然后要等待flash忙完,这里有个Wait_Busy
	W25QXX_Wait_Busy();
    //3.然后片选这里设置为0,也就是开启片选,拉低片选
	W25QXX_CS = 0;								//使能器件
    //4.然后开始发送擦除扇区指令
	SPI3_ReadWriteByte(W25X_SectorErase);		//发送扇区擦除指令
    //5.然后,开始发送要从哪个地址开始擦除,这个地址是个
    //24位的数据,所以要先发送高位,再发送中位,在发送低位.
	SPI3_ReadWriteByte((u8)((Dst_Addr) >> 16)); //发送24bit地址
	SPI3_ReadWriteByte((u8)((Dst_Addr) >> 8));
	SPI3_ReadWriteByte((u8)Dst_Addr);
    //6.然后写完以后,在取消片选
	W25QXX_CS = 1;		//取消片选
    //7.然后等待扇区擦除完成.
	W25QXX_Wait_Busy(); //等待擦除完成
}

可以看到这里擦除一个扇区一般都是150ms

然后再就是这个PowerDown和这个Wait_Busy

然后主要去看一下这个读数据,跟写数据.

首先先去看这个读数据

可以看到这里

要读数据的话,首先要发出读取数据的这个指令,然后

然后,去确定要读取的起始地址,然后右边Data Out 1就是等待数据读出.

看看代码:

//读取SPI FLASH
//在指定地址开始读取指定长度的数据
//pBuffer:数据存储区
//ReadAddr:开始读取的地址(24bit)
//NumByteToRead:要读取的字节数(最大65535)
void W25QXX_Read(u8 *pBuffer, u32 ReadAddr, u16 NumByteToRead)
{
	u16 i;
    //1.先开启片选,拉低片选
	W25QXX_CS = 0;								//使能器件
    //2.然后这里确定要读取的地址.
	SPI3_ReadWriteByte(W25X_ReadData);			//发送读取命令
	SPI3_ReadWriteByte((u8)((ReadAddr) >> 16)); //发送24bit地址
	SPI3_ReadWriteByte((u8)((ReadAddr) >> 8));
	SPI3_ReadWriteByte((u8)ReadAddr);
    //3.然后开始,根据要读取的缓存大小去读取
	for (i = 0; i < NumByteToRead; i++)
	{
    //4.注意读取的时候实际上是去写入一个空字节,每写入一个空字节
    //实际上就是会触发读取.
		pBuffer[i] = SPI3_ReadWriteByte(0XFF); //循环读数
	}
    //5.下面是片选,用完了以后再取消片选
	W25QXX_CS = 1;
}

然后这里重点看这两个函数,一个是W25QXX_Write_NoCheck()

这个函数

一个是W25QXX_Write()这个写函数

首先来看这个

W25QXX_Write_NoCheck()

可以看到这里有

//无检验写SPI FLASH
//必须确保所写的地址范围内的数据全部为0XFF,否则在非0XFF处写入的数据将失败!
//具有自动换页功能
//在指定地址开始写入指定长度的数据,但是要确保地址不越界!
//pBuffer:数据存储区
//WriteAddr:开始写入的地址(24bit)
//NumByteToWrite:要写入的字节数(最大65535)
//CHECK OK
void W25QXX_Write_NoCheck(u8 *pBuffer, u32 WriteAddr, u16 NumByteToWrite)
{
	u16 pageremain;
	pageremain = 256 - WriteAddr % 256; //单页剩余的字节数
	if (NumByteToWrite <= pageremain)
		pageremain = NumByteToWrite; //不大于256个字节
	while (1)
	{
        //1.可以看到这里实际上是调用的这个页写操作
        //来进行的写入数据,这里的页写实际上就是往
        //sector也就是扇区中去写入数据
		W25QXX_Write_Page(pBuffer, WriteAddr, pageremain);
		if (NumByteToWrite == pageremain)
			break; //写入结束了
		else	   //NumByteToWrite>pageremain
		{
			pBuffer += pageremain;
			WriteAddr += pageremain;

			NumByteToWrite -= pageremain; //减去已经写入了的字节数
			if (NumByteToWrite > 256)
				pageremain = 256; //一次可以写入256个字节
			else
				pageremain = NumByteToWrite; //不够256个字节了
		}
	};
}

看看这个这里的:

W25QXX_Write_Page 这个函数,可以看到这里:

//SPI在一页(0~65535)内写入少于256个字节的数据
//在指定地址开始写入最大256字节的数据
//pBuffer:数据存储区
//WriteAddr:开始写入的地址(24bit)
//NumByteToWrite:要写入的字节数(最大256),该数不应该超过该页的剩余字节数!!!
void W25QXX_Write_Page(u8 *pBuffer, u32 WriteAddr, u16 NumByteToWrite)
{
	u16 i;
	W25QXX_Write_Enable();						 //SET WEL
	W25QXX_CS = 0;								 //使能器件
    //1.去看看这里的
    //W25X_PageProgram,这个命令也是有的,去定义看看
	SPI3_ReadWriteByte(W25X_PageProgram);		 //发送写页命令
	SPI3_ReadWriteByte((u8)((WriteAddr) >> 16)); //发送24bit地址
	SPI3_ReadWriteByte((u8)((WriteAddr) >> 8));
	SPI3_ReadWriteByte((u8)WriteAddr);
	for (i = 0; i < NumByteToWrite; i++)
		SPI3_ReadWriteByte(pBuffer[i]); //循环写数
	W25QXX_CS = 1;						//取消片选
	W25QXX_Wait_Busy();					//等待写入结束
}

可以去文档中找一下这个02指令.

这个指令的用法也是很简单的,可以看到,先写指令,然后在写地址,然后再去写入数据.

//SPI在一页(0~65535)内写入少于256个字节的数据
//在指定地址开始写入最大256字节的数据
//pBuffer:数据存储区
//WriteAddr:开始写入的地址(24bit)
//NumByteToWrite:要写入的字节数(最大256),该数不应该超过该页的剩余字节数!!!
void W25QXX_Write_Page(u8 *pBuffer, u32 WriteAddr, u16 NumByteToWrite)
{
	u16 i;
    //1.先去使能
	W25QXX_Write_Enable();						 //SET WEL
    //2.再去启动片选
	W25QXX_CS = 0;								 //使能器件
    //3.再发起写页也就是写入扇区指令
	SPI3_ReadWriteByte(W25X_PageProgram);		 //发送写页命令
	SPI3_ReadWriteByte((u8)((WriteAddr) >> 16)); //发送24bit地址
	SPI3_ReadWriteByte((u8)((WriteAddr) >> 8));
	SPI3_ReadWriteByte((u8)WriteAddr);
    //4.然后这里再循环的去写数据.
	for (i = 0; i < NumByteToWrite; i++)
		SPI3_ReadWriteByte(pBuffer[i]); //循环写数
	W25QXX_CS = 1;						//取消片选
	W25QXX_Wait_Busy();					//等待写入结束
}

然后 再去看看这个NoCheck的这个函数,注意这里要知道

这个函数,他不是只可以写入到某个扇区的,他是可以跨扇区的,也就是

只需要指定一个地址就可以了,他就从这个地址开始写,写入的数据,有可能会跨扇区.

//无检验写SPI FLASH
//必须确保所写的地址范围内的数据全部为0XFF,否则在非0XFF处写入的数据将失败!
//具有自动换页功能
//在指定地址开始写入指定长度的数据,但是要确保地址不越界!
//pBuffer:数据存储区
//WriteAddr:开始写入的地址(24bit)
//NumByteToWrite:要写入的字节数(最大65535)
//CHECK OK
void W25QXX_Write_NoCheck(u8 *pBuffer, u32 WriteAddr, u16 NumByteToWrite)
{
	u16 pageremain;
	pageremain = 256 - WriteAddr % 256; //单页剩余的字节数
	if (NumByteToWrite <= pageremain)
		pageremain = NumByteToWrite; //不大于256个字节
	while (1)
	{
		W25QXX_Write_Page(pBuffer, WriteAddr, pageremain);
		if (NumByteToWrite == pageremain)
			break; //写入结束了
		else	   //NumByteToWrite>pageremain
		{
			pBuffer += pageremain;
			WriteAddr += pageremain;

			NumByteToWrite -= pageremain; //减去已经写入了的字节数
			if (NumByteToWrite > 256)
				pageremain = 256; //一次可以写入256个字节
			else
				pageremain = NumByteToWrite; //不够256个字节了
		}
	};
}

然后这里重点看这个函数: 

这里只给大家讲一下思路

W25QXX_Write

这个函数的思路是这样的:

也就是说,每个sector都是4k个字节,那么1k就是1024个字节

那么,4k就是4096个字节,每个字节都是一个地址.

他在写数据的时候,会给出一个起始地址,和一个要写入的数据的长度,那么这个函数会判断

这个写入的起始地址和这个长度,加起来有个结束地址,也就是这个地址段之间,有没有,不是0xff的

如果有不是0xff的,那么对应的扇区就进行删除,当然,删除之前,要先去

把数据保存到buf中,然后把再把buf中的数据,给写入到

对应的地址中去.

这个ppt中写了这个函数的思路

然后对照代码看看

//写SPI FLASH
//在指定地址开始写入指定长度的数据
//该函数带擦除操作!
//pBuffer:数据存储区
//WriteAddr:开始写入的地址(24bit)
//NumByteToWrite:要写入的字节数(最大65535)
u8 W25QXX_BUFFER[4096];
//1.这里这个pBuffer 用来存放要写入的数据
//WriteAddr从这个地址开始,
//要写入多少个NumByteToWrite字节
void W25QXX_Write(u8 *pBuffer, u32 WriteAddr, u16 NumByteToWrite)
{
	u32 secpos;
	u16 secoff;
	u16 secremain;
	u16 i;
	u8 *W25QXX_BUF;
	W25QXX_BUF = W25QXX_BUFFER;
    //2.这个用WriteAddr要写入的地址,/ 4096,因为4096就是一个扇区4k的
    //的大小,所以这样的话,算出来就是,要写入的扇区是哪个扇区
    //
	secpos = WriteAddr / 4096; //扇区地址
    //3.然后,取余数实际上就是获取,要写入的地址
    //在某个扇区里的偏移量.
	secoff = WriteAddr % 4096; //在扇区内的偏移
    //4.然后这里secremain 4096-secoff这个
    //这个就是这个扇区剩余空间的大小了.
	secremain = 4096 - secoff; //扇区剩余空间大小
	//printf("ad:%X,nb:%X\r\n",WriteAddr,NumByteToWrite);//测试用
    //5.这里先判断要写入的这个字节的个数,如果
    //小于这个扇区还剩余的字节个数的话
    //就可以把实际的字节个数直接赋值给secremain 
	if (NumByteToWrite <= secremain)
		secremain = NumByteToWrite; //不大于4096个字节
	while (1)
	{
        //6.这里首先要先计算出这个扇区的地址secpos * 4096,
        //然后从这个地址开始读取4096个字节,把这个扇区的数据都读出来
        //读取到这个W25QXX_BUF中去.
		W25QXX_Read(W25QXX_BUF, secpos * 4096, 4096); //读出整个扇区的内容
        //7.然后这里去循环去判断,看看
        //从偏移量secoff 开始,到数据的长度的地址,这个过程中
        //数据有没有不是空的W25QXX_BUF[secoff + i] != 0XFF
        //如果有的话,那么就说明这个扇区需要擦除
		for (i = 0; i < secremain; i++)				  //校验数据
		{
			if (W25QXX_BUF[secoff + i] != 0XFF)
				break; //需要擦除
		}
        //8.如果i小于secremain,说明跳出了上面的循环了,
        //跳出循环也就说明,这个地址区间内有不是0xff,空数据区的地方
        //也就是需要擦除扇区.
		if (i < secremain) //需要擦除
		{
            //9.然后就去擦除这个扇区
			W25QXX_Erase_Sector(secpos);	//擦除这个扇区
			for (i = 0; i < secremain; i++) //复制
			{
                //10.然后再把传入的要写入扇区的数据
                //写入进去,用循环就可以了.
                //注意这里仅仅是更新了这个BUF,并没有开始往扇区中写入
				W25QXX_BUF[i + secoff] = pBuffer[i];
			}
            //11.更新完BUF以后,就可以把更新后的buf一次性的,写入到整个扇区中了
            //
			W25QXX_Write_NoCheck(W25QXX_BUF, secpos * 4096, 4096); //写入整个扇区
		}
		else
			W25QXX_Write_NoCheck(pBuffer, WriteAddr, secremain); //写已经擦除了的,直接写入扇区剩余区间.
        //12.如果NumByteToWrite =secremain 
        //也就是如果扇区剩余的空间大小,够用的话,也就是
        //两个相等的话,因为如果够用的话,上面把NumByteToWrite 赋值给secremain了
		if (NumByteToWrite == secremain)
           {
           //13.那么写入就结束了.
			break; //写入结束了
		   }
        else	   //写入未结束
		{
            //14.否则的话,说明,要写入的数据,超过这个
            //扇区剩余的空间了
            //这个时候就要设置,写入的扇区地址是下一个扇区
            //secpos++
			secpos++;	//扇区地址增1
            //15.然后从下一个扇区的第0个地址开始写入
			secoff = 0; //偏移位置为0
            //16.然后
            //下一个要写入的位置,要偏移一下
            //pBuffer 接着从pBuffer 没写完的地方继续写
			pBuffer += secremain;		 //指针偏移
            //17.然后要写入的地址也要偏移一下
            //WriteAddr 从flash的下一个扇区接着写.
			WriteAddr += secremain;		 //写地址偏移
            //18.因为已经写了一部分了NumByteToWrite 
            //所以这里,要减去已经写完的字节.
			NumByteToWrite -= secremain; //字节数递减
			if (NumByteToWrite > 4096)
               //19.如果NumByteToWrite 剩余的字节数,还是超过一个扇区了
               //就把剩余的数secremain 直接设置成一个扇区的大小
				secremain = 4096; //下一个扇区还是写不完
			else
               //20.否则的话,就说明下一个扇区,也就是第二个扇区,已经
               //够用了,可以写完了,这样就可以了.
				secremain = NumByteToWrite; //下一个扇区可以写完了
		}
	};
}

然后再去看一下这个main.c,main函数

#include "led.h"
#include "delay.h"
#include "key.h"
#include "sys.h"
#include "lcd.h"
#include "usart.h"
#include "w25qxx.h"

/************************************************
 ALIENTEK精英STM32开发板实验23
 SPI 实验  
 技术支持:www.openedv.com
 淘宝店铺:http://eboard.taobao.com 
 关注微信公众平台微信号:"正点原子",免费获取STM32资料。
 广州市星翼电子科技有限公司  
 作者:正点原子 @ALIENTEK
************************************************/

//要写入到W25Q64的字符串数组
const u8 TEXT_Buffer[] = {"ELITE STM32 SPI TEST"};
#define SIZE sizeof(TEXT_Buffer)
int main(void)
{
	u8 key;
	u16 i = 0;
	u8 datatemp[SIZE];
	u32 FLASH_SIZE;

	delay_init();									//延时函数初始化
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //设置中断优先级分组为组2:2位抢占优先级,2位响应优先级
	uart_init(115200);								//串口初始化为115200
	LED_Init();										//初始化与LED连接的硬件接口
	LCD_Init();										//初始化LCD
	KEY_Init();										//按键初始化
//1.这里首先就是先去初始化W25QXX_Init flash芯片
	W25QXX_Init();									//W25QXX初始化

	POINT_COLOR = RED; //设置字体为红色
	LCD_ShowString(30, 50, 200, 16, 16, "ELITE STM32");
	LCD_ShowString(30, 70, 200, 16, 16, "SPI TEST");
	LCD_ShowString(30, 90, 200, 16, 16, "ATOM@ALIENTEK");
	LCD_ShowString(30, 110, 200, 16, 16, "2015/1/15");
	LCD_ShowString(30, 130, 200, 16, 16, "KEY1:Write  KEY0:Read"); //显示提示信息
	while (W25QXX_ReadID() != W25Q128)							   //检测不到W25Q128
	{
      //2.然后再判断这里Flash的类型,是不是128的
      //不是的话,就提示W25Q128 Check Failed!
		printf("W25Q128 Check Failed!");
		LCD_ShowString(30, 150, 200, 16, 16, "W25Q128 Check Failed!");
		delay_ms(500);
		LCD_ShowString(30, 150, 200, 16, 16, "Please Check!        ");
		delay_ms(500);
		LED0 = !LED0; //DS0闪烁
	}
    //3.是的话就提示W25Q128 Ready
	LCD_ShowString(30, 150, 200, 16, 16, "W25Q128 Ready!");
	FLASH_SIZE = 128 * 1024 * 1024; //FLASH 大小为16M字节
	POINT_COLOR = BLUE;				//设置字体为蓝色
	while (1)
	{
      //4.这里去扫描按键
		key = KEY_Scan(0);
		if (key == KEY1_PRES) //KEY1按下,写入W25QXX
		{
          //5.如果按下的是KEY1,就去写入TEXT_Buffer的数据到flash
          //
			LCD_Fill(0, 170, 239, 319, WHITE); //清除半屏
			LCD_ShowString(30, 170, 200, 16, 16, "Start Write W25Q128....");
			W25QXX_Write((u8 *)TEXT_Buffer, FLASH_SIZE - 100, SIZE);		 //从倒数第100个地址处开始,写入SIZE长度的数据
			LCD_ShowString(30, 170, 200, 16, 16, "W25Q128 Write Finished!"); //提示传送完成
		}
        //6.如果按下的是KEY0就把写入的数据,再去读取出来.
		if (key == KEY0_PRES) //KEY0按下,读取字符串并显示
		{
			LCD_ShowString(30, 170, 200, 16, 16, "Start Read W25Q128.... ");
			W25QXX_Read(datatemp, FLASH_SIZE - 100, SIZE);				   //从倒数第100个地址处开始,读出SIZE个字节
			LCD_ShowString(30, 170, 200, 16, 16, "The Data Readed Is:  "); //提示传送完成
			LCD_ShowString(30, 190, 200, 16, 16, datatemp);				   //显示读到的字符串
		}
		i++;
		delay_ms(10);
		if (i == 20)
		{
			LED0 = !LED0; //提示系统正在运行
			i = 0;
		}
	}
}

然后把代码编译一下,下载到开发版

可以看到KEY1是写,KEY0是读取,

这里首先按下KEY1,可以看到

提示写完了,然后

按下KEY0再看一下

可以看到读出来的数据,其实就是前面写入的数据.

猜你喜欢

转载自blog.csdn.net/lidew521/article/details/108403131