我们规划SPI和前面I2C的差不多,先建一个MySPI的模块,在这个模块里主要包含通信引脚封装、初始化以及SPI通信的三个拼图,起始、终止和交换一个字节,这是SPI通信层的内容,然后基于SPI层,我们再建一个W25Q64的模块,在这个模块里调用底层SPI的拼图来拼接各种指令和功能的完整时序,比如写使能,擦除,页编程,读数据等,所以这一块叫做W25Q64的硬件驱动层,最后在主函数里,我们调用驱动层的函数来完成我们想要实现的功能。
对于主机来说,时钟,主机输出和片选都是输出引脚,所以这三个引脚是推完输出模式,然后剩下一个主机输入是输入引脚,所以这一个引脚是浮空或上拉,我们选择上拉输入
MySPI_W_SCK(0);//计划使用SPI模式0,所以默认低电平
//MOSI没有明确规定可以不管,MISO是输入引脚,不用设置电平,这样初始化的默认电平就置好了。

if (MySPI_R_MISO() == 1){ByteReceive |= 0x80;}//是1就或上最高位,是0就不变还是0,这样把最高位存入ByteReceive
//接着就是SCK产生下降沿,主机和从机移出下一位
MySPI_W_SCK(0);//产生下降沿
MySPI_W_MOSI(ByteSend & 0x40);//把次高位放到MOSI上
//返回值是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;
}
{
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;
}

//*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();
}

#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时也是不能够进行读取操作的
