技术交流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再看一下
可以看到读出来的数据,其实就是前面写入的数据.