我们规划SPI和前面I2C的差不多,先建一个MySPI的模块,在这个模块里主要包含通信引脚封装、初始化以及SPI通信的三个拼图,起始、终止和交换一个字节,这是SPI通信层的内容,然后基于SPI层,我们再建一个W25Q64的模块,在这个模块里调用底层SPI的拼图来拼接各种指令和功能的完整时序,比如写使能,擦除,页编程,读数据等,所以这一块叫做W25Q64的硬件驱动层,最后在主函数里,我们调用驱动层的函数来完成我们想要实现的功能。

对于主机来说,时钟,主机输出和片选都是输出引脚,所以这三个引脚是推完输出模式,然后剩下一个主机输入是输入引脚,所以这一个引脚是浮空或上拉,我们选择上拉输入

 

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;//输出引脚初始化
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
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);
void MySPI_W_SS(uint8_t BitValue)//从机片选函数
{
GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue);
}
void MySPI_W_SCK(uint8_t BitValue)// 时钟输出
{
GPIO_WriteBit(GPIOA, GPIO_Pin_5, (BitAction)BitValue);
}
void MySPI_W_MOSI(uint8_t BitValue)//输出数据
{
GPIO_WriteBit(GPIOA, GPIO_Pin_7, (BitAction)BitValue);
}
uint8_t MySPI_R_MISO(void)//接收数据
{
return GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6);
}
还要在初始化函数中再加上两句代码:
MySPI_W_SS(1);//初始化置高电平默认不选中从机
MySPI_W_SCK(0);//计划使用SPI模式0,所以默认低电平
//MOSI没有明确规定可以不管,MISO是输入引脚,不用设置电平,这样初始化的默认电平就置好了。
接下来写SPI通信的三个时序基本单元,首先是起始信号和终止信号:
void MySPI_Start(void)
{
MySPI_W_SS(0);//因为SPI通信起始信号比较简单,SS线置低电平就行了
}
void MySPI_Stop(void)
{
MySPI_W_SS(1);
}
接下来是发送数据,也就是交换一个字节
图中描述的是,SS下降沿和数据移除是同时发生的,包括后面SCK下降沿和数据移除也是同时的,但这并不代表我们程序上要同时执行两条代码,是有先后顺序的,是先,SS下降沿或SCK下降沿,再移除数据,这个下降沿是触发数据移除这个动作的条件,对硬件SPI来说,由于使用了硬件的移位寄存器电路,所以这两个动作几乎是同时发生的,而对于软件SPI来说,由于程序是一条一条执行的,不可能同时完成两个动作,我们直接把它看成先后执行的逻辑。整体的流程就是,先SS下降沿,再移出数据,再SCK上升沿,再移入数据,再SCK下降沿,再移出数据。
看时序中就是,SS下降沿之后,第一步,主机和从机同时移出数据,就是主机移出数据最高位放到MOSI上,从机移出它的最高位放到MISO上,从机的我们不用管,所以第一步是,写MOSI发送的是ByteSend的最高位
MySPI_W_MOSI(ByteSend & 0x80);//取要发送的字节的最高位
//第二步 ,SCK上升沿,上升之后主机和从机同时移入数据,从机会自动把B7读走,从机的移入就不归我们管,主机只需要读取MISO的数据就可以了。
MySPI_W_SCK(1);//从机会自动把MOSI上的数据读走
if (MySPI_R_MISO() == 1){ByteReceive |= 0x80;}//是1就或上最高位,是0就不变还是0,这样把最高位存入ByteReceive
//接着就是SCK产生下降沿,主机和从机移出下一位
MySPI_W_SCK(0);//产生下降沿
MySPI_W_MOSI(ByteSend & 0x40);//把次高位放到MOSI上
接下来把上面代码套上for循环就好了
uint8_t MySPI_SwapByte(uint8_t ByteSend)//ByteSend是我们传进的参数要通过交换一个字节的时序发送出去
//返回值是ByteReceive,是通过交换一个字节接收到的数据
{
uint8_t ByteReceive = 0x00;
for (uint8_t i = 0; i < 8; i++)
{
MySPI_W_MOSI(ByteSend & (0x80 >> i));//取要发送的字节的最高位
MySPI_W_SCK(1);//从机会自动把MOSI上的数据读走
if (MySPI_R_MISO() == 1){ByteReceive |= (0x80 >> i);}//是1就或上最高位,是0就不变还是0,这样把最高位存入ByteReceive
//接着就是SCK产生下降沿,主机和从机移出下一位
MySPI_W_SCK(0);//产生下降沿
}
return ByteReceive;
}
以上方法是使用掩码依次提取数据的每一位,好处就是不会改变参数本身,之后还想要使用ByteSend还能继续用
以下还有一种方法:
uint8_t MySPI_Swapbyte(uint8_t ByteSend)
{
for (uint8_t i = 0; i < 8; i++)
{
MySPI_W_MOSI(ByteSend & 0x80);
ByteSend <<= 1;
MySPI_W_SCK(1);
if (MySPI_R_MISO() == 1){ByteSend |= 0x01;}
MySPI_W_SCK(0);
}
return ByteSend;
}
我们是使用移位数据本身来进行的操作,好处就是效率更高,但是ByteSend数据在移位过程中改变了,函数使用后,原始传入的参数就没有了。后续我们使用第一种方法。到这里SPI的通信层代码就写完了,接下来我们继续写下一个模块,在SPI通信层之上,我们要建立W25Q64的驱动层
#include “MySPI.h”
void W25Q64_Init(void)
{
MySPI_Init();//由于W25Q64也不需要再初始化其他东西了,所以初始化这里我们只需要调用这个函数就行了
}
之后我们可以来实现业务代码了,也就是拼接完整时序,需要参考手册,主要参考的部分就是这个指令集表格
比如我们先实现这个获取ID号的时序,先把ID读出来看对不对以此来验证底层的SPI协议写的有没有问题
从表格可以看到,读取ID号的时序就是,起始,先交换发送指令9F,随后连续交换接收3个字节,停止,那我们怎么知道哪个是交换发送哪个是交换接收呢,后面有写到圆括号括起来的就是我们要交换接收的数据,所以后面三个字节都是需要我们交换接收的。第一个字节是厂商ID表示了是哪个厂家生产的,后两个字节是设备ID其中设备ID高8位,表示存储器类型,低8位表示容量,
void W25Q64_Read(uint8_t *MID, uint16_t *DID)//由于我们计划这个函数是有两个返回值的,所以我们还是使用指针来实现多返回值
//*MID:输出8位的厂商ID,另一个是*DID:输出16位的设备ID
{
MySPI_Start();
MySPI_SwapByte(0x9F);//返回值是交换接收的,但这里接收的数据没有意义,所以返回值就不要了,9F代表读ID的指令
//从机收到了读ID号的指令后,就会严格按照手册里约定的规则来,下一次交换就会把ID号返回给主机了,所以要再来一次
*MID = MySPI_SwapByte(0xFF);//第一个置换回来的是厂商ID,我们存在MID指向的变量里
*DID = MySPI_SwapByte(0xFF);//再交换一次,收到的就是设备ID的高8位了
*DID <<= 8;//把第一次读到的数据运到DID的高8位去
*DID |= MySPI_SwapByte(0xFF);//或上设备ID的低8位
MySPI_Stop();
}
我们先测试一下到目前为止的代码有没有出错:
#include “stm32f10x.h”                  // Device header
#include “Delay.h”
#include “OLED.h”
#include “W25Q64.h”
uint8_t MID;
uint16_t DID;
int main(void)
{
OLED_Init();
W25Q64_Init();
W25Q64_Read(&MID, &DID);
OLED_ShowHexNum(1, 1, MID, 2);
OLED_ShowHexNum(1, 8, DID, 4);
while (1)
{
}
}
可以看到厂商ID为EF,设备ID是4017,接下来我们要把指令集里标黄色的这些指令时序都实现出来,每个指令都对应一个指令码,如果总是在程序中直接像前面的函数一样写一个数字,意义就不太明显,可读性不高,所以我们要把每个指令码也用宏定义替换一下,指令比较多,我们还是单独建一个头文件存放一下。

#ifndef __W25Q64_H
#define __W25Q64_H

#define W25Q64_WRITE_ENABLE 0x06
#define W25Q64_WRITE_DISABLE 0x04
#define W25Q64_READ_STATUS_REGISTER_1 0x05
#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
#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

接下来先是读指令的函数:

void W25Q64_WriteEnable(void)
{
MySPI_Start();
MySPI_SwapByte(W25Q64_WRITE_ENABLE);//这里芯片规定的就是SPI起始之后的第一个字节都是指令码
MySPI_Stop();//这个读指令之后不需要跟任何数据,所以直接Stop,这样就完成了。
}

下一条指令是读状态寄存器1,指令码是05,发完指令码就可以接收状态寄存器了,我们读状态寄存器的主要用途就是判断芯片是不是忙状态,我们要读取它的最低位,BUSY看看是不是1,1表示芯片在忙,0表示芯片空闲,另外我们最好要实现一个等待BUSY为0的函数,我们调用这个函数,如果BUSY为1,就进入等待,等函数执行完BUSY就肯定是0了

起始之后,先发送指令码再接受状态寄存器,之后如果时序不停止,还要继续接收的话,芯片就会继续把最新的状态寄存器发过来也就是状态寄存器可以被连续读出,这个连续读出的特性就方便我们执行等待的功能,

void W25Q64_WriteBusy(void)
{
uint32_t Timeout;
MySPI_Start();
MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1);
while((MySPI_SwapByte(W25Q64_DUMMY_BYTE) & 0x01) == 0x01)//与上0x01用掩码取出最低位,如果BUSY为1
//就会再次进入循环,再次读出一次状态寄存器,继续判断直到BUSY为0,跳出循环
{
Timeout–;
if (Timeout)
{
break;
}
}
MySPI_Stop();
}

接下来我们写页编程的函数,格式是先发送一个指令吗0x02,再发送3个字节的地址,最后发数据。由DataByte1开始发,最多发到DataByte256,如果继续发,就会覆盖原来的DataByte1。

void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count)//如果我们只想要指定地址写一个字节,直接传字节参数就行
//但我们一般存储的东西比较多,每次都调用存储一个字节效率太低了,所以我们可以传递一个数组DataArray,数组我们可以通过指针传递
//因为一次性写入的数据的数量范围0~256,所以Count要定义为16位,如果之定义8位,只能存0~255
{
MySPI_Start();
MySPI_SwapByte(W25Q64_PAGE_PROGRAM);
MySPI_SwapByte(Address >> 16);//要继续交换发送3个字节的地址,地址是高位字节先发送,所以第一次发的是
MySPI_SwapByte(Address >> 8);//Address3个字节里的最高字节,交换字节函数只能接受8位数据,所以高位会被舍弃
MySPI_SwapByte(Address);
for (uint16_t i = 0; i < Count; i++)
{
MySPI_SwapByte(DataArray[i]);
}
MySPI_Stop();
}

下一个我们来实现擦除的功能,这里只演示扇区的擦除,只需要先发送指令20,再发送3个字节的地址就行了,这样指定地址的所在的整个扇区就会被擦除。

void W25Q64_SectorErase(uint32_t Address)
{
MySPI_Start();
MySPI_SwapByte(W25Q64_SECTOR_ERASE_4KB);
MySPI_SwapByte(Address >> 16);
MySPI_SwapByte(Address >> 8);
MySPI_SwapByte(Address);
MySPI_Stop();
}

最后一个指令,就是ReadData

流程是交换发送指令0x03,再发送3个字节的地址,随后转入接收,就可以依次接收数据了,在之前DO一直都是高阻态在发送完3个字节之后,DO开启输出,此时主机就可以接收有用的数据DataOut1了,读取没有字节限制,可以跨页一直连续

void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count)//这里的数组是输出参数,count范围可以很大
{
MySPI_Start();
MySPI_SwapByte(W25Q64_READ_DATA);
MySPI_SwapByte(Address >> 16);
MySPI_SwapByte(Address >> 8);
MySPI_SwapByte(Address);
for (uint32_t i = 0; i < Count; i++)
{
DataArray[i] = MySPI_SwapByte(W25Q64_DUMMY_BYTE);//在每次调用交换读取之后,存储器内部地址指针自动自增
}
MySPI_Stop();
}

注意Flash的操作注意事项:第一条写入前必须要先进行写使能,在这里,涉及写入操作的时序有扇区擦除和页编程,我们可以直接在函数前面写上一个写使能

W25Q64_WriteEnable();

这个写使能仅对之后跟随的一条时序有效,一条时序之后会自动写失能,所以在每次写之前都加上一条写使能,写完之后就不用再写失能了。

还有一个要做的是:写入操作之后,芯片会进入忙状态,所以我们可以在每次写操作时序结束之后调用一下WaitBUSY,我们可以在每次写入之后都等BUSY清零了再退出,这样最保险。还有就是事前等待,就是函数里在写入之前进行等待BUSY,这样效率会高一些,因为写完之后不等,程序可以执行其他的代码,程序可以执行其他代码的时间,正好可以利用执行其他代码的时间来消耗等待的时间,但是事前操作在写入操作和读取操作之前都要调用,因为芯片BUSY时也是不能够进行读取操作的

#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 ArrayRead[4];
int main(void)
{
OLED_Init();
W25Q64_Init();
OLED_ShowString(1, 1, “MID:   DID”);
OLED_ShowString(2, 1, “W:”);
OLED_ShowString(3, 1, “R:”);
W25Q64_Read(&MID, &DID);
OLED_ShowHexNum(1, 5, MID, 2);
OLED_ShowHexNum(1, 12, DID, 4);
W25Q64_SectorErase(0x000000);//后面三位随便写擦除的都是同一个扇区,只要前面三位不变
W25Q64_PageProgram(0x000000, ArrayWrite, 4);
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)
{
}
}