目录
SPI通信
- SPI是由Motorola公司开发的一种通用数据总线;
- 有四根通信总线:SCL(Serial Clock)、MOSI(Master Output Slove Input)、MISO(Master Input Slave Input)、SS(Slave Slecet);
- 同步、全双工;
- 支持总线挂载多个设备(一主多从)
硬件电路
- 所有SPI设备的SCK、MOSI、MISO分别连接在一起;
- 主机另外引出多条SS控制线,分别接到各个从机的SS引脚;
- 输出引脚配置为推挽输出模式,输入引脚配置为浮空或上拉输入模式

SPI通信的原理
SPI通信的原理是主机和从机之间交换数据。

如上图所示,主机和从机都有一个移位寄存器,例如主机发送数据给从机:当一个时序到来时,主机将一个bit位数据移出至MOSI总线上,从机将数据移至MISO总线上,当下一个时序到来时,主机将MISO上的数据移入移位寄存器中,从机将MOSI上的数据移入移位寄存器中,如此重复八次,完成一个字节数据的传输。(SPI为高位先行)
SPI时序
起始条件
SPI起始条件为:SS从高电平转换为低电平。

终止条件
SPI终止条件为:SS从低电平转换为高电平。

交换一个字节(模式0)
- CPOL=0:空闲状态时,SCK为低电平
- CPHA=0:SCK的第一个边沿移入数据,第二个边沿移出数据;
- 此模式一般在SS拉低电平时就会将第一个数据移出

交换一个字节(模式1)
- CPOL=0:空闲状态时,SCK为低电平
- CPHA=1:SCK的第一个边沿移出数据,第二个边沿移入数据;

PS:模式3和模式3都是在这两个模式下改变而来的,按照(CPOL CPHA)组合起来的二进制数的大小来决定时模式几的。
指定地址读数据
SPI通信时序的组合:先发送要执行的指令+指定的地址(一般不只8位,由要通信的从机芯片决定)+要发送的数据(如果是读取就不用写这部分)

上图是在指定地址读的时序图。
W25Q64简介
W25QXX系列是一种低成本、小型化、使用简单的非易失性存储器,常应用于数据存储、字库存储、固件程序存储等场景。(存储容量与具体型号相关,均为24为地址)
硬件电路

W25Q64框图

W25Q64将存储区域分为了128个不同的存储块(0xXXxxxx),再将不同的存储块分为了16个不同的存储扇区(0xXXXxxx),大写为不变部分。
Flash操作注意事项
写入操作时
- 写入操作前,必须先进行写使能;
- 每个数据位只能由1变成0,不能由0变成1;
- 写入数据前必须先擦除,擦除后,所有数据位变为1;
- 擦除必须按照最小擦除单元进行擦除;
- 连续写入多个字节时,最多写入一页数据,超过页尾位置的数据,会回到页首覆盖写入;
- 写入操作结束后,芯片进入忙状态,不响应新的读写操作。
读取操作时
- 直接调用读取操作,无需使能,无需额外操作,没有页限制,读取操作结束后也不会进入忙状态,但是不能在忙状态读取数据。
一、软件SPI
本实验用PA4、PA5、PA6、PA7分别连接SS、CLK、MISO、MOSI。
程序
先对SPI模块进行编写,MySPI.c文件:
#include "stm32f10x.h" // Device header
//函数:将对时钟引脚电平的改变进行封装
void MySPI_CLK (uint8_t BitVal)
{
GPIO_WriteBit (GPIOA ,GPIO_Pin_5 ,(BitAction) BitVal);
}
//函数:将对发送数据过程进行封装
void MySPI_MOSI (uint8_t BitVal)
{
GPIO_WriteBit (GPIOA ,GPIO_Pin_7 ,(BitAction) BitVal);
}
//函数:将对接收内容过程进行封装
uint8_t MySPI_MISO (void)
{
return GPIO_ReadInputDataBit (GPIOA ,GPIO_Pin_6);
}
//函数:将对从机选择过程进行封装
void MySPI_SS (uint8_t BitVal)
{
GPIO_WriteBit (GPIOA ,GPIO_Pin_4 ,(BitAction) BitVal);
}
//函数:对SPI的初始化函数
void MySPI_Init(void)
{
//打开APB2总线上GPIOA的时钟
RCC_APB2PeriphClockCmd (RCC_APB2Periph_GPIOA ,ENABLE);
//对PA4、PA5、PA7进行初始化配置,这里需要配置为推挽输出模式
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode =GPIO_Mode_Out_PP;
GPIO_InitStruct.GPIO_Pin =GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_7 ;
GPIO_InitStruct.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init (GPIOA ,&GPIO_InitStruct );
//对PA6进行初始化配置,这里需要配置为上拉输入模式
GPIO_InitStruct.GPIO_Mode =GPIO_Mode_IPU;
GPIO_InitStruct.GPIO_Pin =GPIO_Pin_6 ;
GPIO_InitStruct.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init (GPIOA ,&GPIO_InitStruct );
//对SPI时序的初始状态进行设置,默认为时钟低电平、从机未选择状态
MySPI_CLK (0);
MySPI_SS(1);
}
//函数:初始状态,对从机进行选择
void MySPI_Start(void)
{
MySPI_SS(0);
}
//函数:结束状态,对从机选择进行取消
void MySPI_Stop(void)
{
MySPI_SS(1);
}
//函数;主机和从从机交换一个字节数据
//函数参数:要发送给从机的数据
//返回值:主机接收到的数据
uint8_t MySPI_Swap(uint8_t send_data)
{
//创建变量用于存储主机接收到的数据
uint8_t receive_data = 0x00;
//创建变量用于存储for循环次数
uint8_t i;
//循环语句:用于循环交换一个字节数据
for(i=0;i<8;i++)
{
//向MOSI总线写入一位数据(发送一位数据)
MySPI_MOSI(send_data & (0x80>>i));
//将时钟拉高
MySPI_CLK (1);
//接收一位数据(用判断+位处理的方式)
if(MySPI_MISO ()){receive_data |= (0x80>>i);}
//接收完一位数据后,将时钟拉低
MySPI_CLK (0);
}
//将接收到的一个字节数据返回
return receive_data;
}
这里用软件模拟的SPI是模式0,空闲状态时钟为低电平,第一个边沿移入数据,第二个边沿移出数据。
MySPI.h文件:
#ifndef _MYSPI_H
#define _MYSPI_H
void MySPI_CLK (uint8_t BitVal);
void MySPI_MOSI (uint8_t BitVal);
uint8_t MySPI_MISO (void);
void MySPI_SS (uint8_t BitVal);
void MySPI_Init(void);
void MySPI_Start(void);
void MySPI_Stop(void);
uint8_t MySPI_Swap(uint8_t send_data);
#endif
再对W25Q64模块进行编写,W25Q64.c文件:
#include "stm32f10x.h" // Device header
#include "MySPI.h"
#include "W25Q64_Ins.h"
//对W25Q64进行初始化
void W25Q64_Init(void)
{
MySPI_Init ();
}
//函数:对W25Q64的ID号进行读取,可以使用这个函数来测试MySPI模块时序是否正确
void Read_ID(uint8_t *MF,uint16_t *ID)
{
//对从机进行选择
MySPI_Start ();
//向W25Q64发送读取ID指令
MySPI_Swap(W25Q64_JEDEC_ID);
//接收MID
*MF = MySPI_Swap(W25Q64_DUMMY_BYTE);
//接收DID高8位
*ID = MySPI_Swap(W25Q64_DUMMY_BYTE);
//将接收到的高8位移位至高位
*ID <<= 8;
//接收DID的低8位
*ID |= MySPI_Swap(W25Q64_DUMMY_BYTE);
//对取消对从机的选择
MySPI_Stop ();
}
//函数:对W25Q64的写使能函数
void Write_Enable(void)
{
//对从机进行选择
MySPI_Start ();
//向W25Q64发送写使能指令
MySPI_Swap (W25Q64_WRITE_ENABLE);
//取消对从机的选择
MySPI_Stop ();
}
//函数:对Flah进行写操作后,会进入忙状态,该状态下不允许有读写操作,因此需要
//封装一个函数来等待忙状态
void Wait_Busy(void)
{
//创建一个变量用于存储等待忙状态的时间
uint32_t Timeout = 100000;
//对从机进行选择
MySPI_Start ();
//向W25Q64发送读取标志位寄存器指令
MySPI_Swap (W25Q64_READ_STATUS_REGISTER_1);
//用while循环去判断忙状态,等待忙状态过去(该状态寄存器最低位为1时是忙状态)
while((MySPI_Swap (W25Q64_DUMMY_BYTE) & 0x01) == 0x01)
{
//为了避免程序进行死循环,定义一个合适的等待时间,到时间跳出等待状态
Timeout --;
if(Timeout == 0 )
{
break ;
}
}
//取消对从机的选择
MySPI_Stop ();
}
//函数:对指定地址页面进行内容写入
//函数参数:指定地址、要写入的内容、要写入内容的个数
void Page_Program(uint32_t address,uint8_t *DataArray,uint16_t count)
{
uint8_t i;
//先进行W25Q64的写使能
Write_Enable ();
//对从机进行选择
MySPI_Start ();
//向W25Q64发送写页面指令
MySPI_Swap (W25Q64_PAGE_PROGRAM);
//紧接着发送指定24位地址
//先发送高8位地址(将24位地址向右移动16位,刚好取到高8位)
MySPI_Swap (address >> 16);
//再发送中8位地址(将24位地址向右移动8位,刚好取到中8位)
MySPI_Swap (address >> 8);
//最后发送低8位地址,直接取到低8位
MySPI_Swap (address);
//用for循环对要发送的数据依次进行发送
for(i=0;i<count;i++)
{
//依次啊发送一个字节数据
MySPI_Swap (DataArray [i]);
}
//取消对从机的选择
MySPI_Stop ();
//写操作后需要等待忙
Wait_Busy ();
}
//函数:擦除函数,必须以最小擦除单元进行擦除
//函数参数:要擦除扇区的首地址
void Sector_Erase(uint32_t address)
{
//向W25Q64进行写使能
Write_Enable ();
//对从机进行选择
MySPI_Start ();
//向W25Q64发送擦除指令
MySPI_Swap (W25Q64_SECTOR_ERASE_4KB);
//对要擦除区域首地址进行发送
MySPI_Swap (address >> 16);
MySPI_Swap (address >> 8);
MySPI_Swap (address);
//取消对从机的选择
MySPI_Stop ();
//写操作后需要等待忙
Wait_Busy ();
}
//函数:读取从机中的数据
//函数参数:要读取数据的首地址,用于存储读取到的数据的数组,读取的数据个数
void Read_Data(uint32_t address,uint8_t *DataArray,uint16_t count)
{
uint16_t i;
//对从机进行选择
MySPI_Start ();
//向W25Q64发送读取指令
MySPI_Swap (W25Q64_READ_DATA);
//发送要读取数据的24位的首地址
MySPI_Swap (address >> 16);
MySPI_Swap (address >> 8);
MySPI_Swap (address);
//用for循环依次读取从机数据
for(i=0;i<count;i++)
{
DataArray[i] = MySPI_Swap (W25Q64_DUMMY_BYTE);
}
//取消对从机的选择
MySPI_Stop ();
}
W25Q64.h文件:
#ifndef _W25Q64_H
#define _W25Q64_H
void W25Q64_Init(void);
void Read_ID(uint8_t *MF,uint16_t *ID);
void Write_Enable(void);
void Wait_Busy(void);
void Page_Program(uint32_t address,uint8_t *DataArray,uint16_t count);
void Sector_Erase(uint32_t address);
void Read_Data(uint32_t address,uint8_t *DataArray,uint16_t count);
#endif
W25Q64_Ins.h文件:
#ifndef __W25Q64_INS_H
#define __W25Q64_INS_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
上面这个文件是对W25Q64的相关指令进行宏定义,便于使用。
最后在主函数中对各模块函数进行调用实现对W25Q64Flash的读写操作,main.c文件:
#include "stm32f10x.h" // Device header
#include "OLED.h"
#include "W25Q64.h"
//创建变量用于存储MID和DID
uint8_t MID;
uint16_t DID;
//创建一个数组用于存储要写入到W25Q64Flah中的数据
uint8_t Data_W[3]={0x09,0x77,0x88};
//创建一个数组用于存储从W25Q64Flah中读取到的数据
uint8_t Data_R[3];
int main(void)
{ //对各个模块进行初始化
OLED_Init();
W25Q64_Init ();
//显示提示字符串
OLED_ShowString (1,1,"MID: DID:");
//读取ID
Read_ID(&MID,&DID);
//显示MID和DID
OLED_ShowHexNum (1,5,MID,2);
OLED_ShowHexNum (1,12,DID,4);
//显示提示字符串
OLED_ShowString (2,1,"Data_W:");
OLED_ShowString (3,1,"Data_R:");
//在进行写入操作前先进行扇区擦除
Sector_Erase (0x000000);
//进行页面写入数据
Page_Program (0x000000,Data_W,3);
//显示写入到W25Q64的数据
OLED_ShowHexNum (2,8,Data_W[0],2);
OLED_ShowHexNum (2,11,Data_W[1],2);
OLED_ShowHexNum (2,14,Data_W[2],2);
//读取从机Flash中的数据
Read_Data (0x000000,Data_R,3);
//显示从Flash中读取的数据
OLED_ShowHexNum (3,8,Data_R[0],2);
OLED_ShowHexNum (3,11,Data_R[1],2);
OLED_ShowHexNum (3,14,Data_R[2],2);
while(1)
{
}
}
最终效果:主机向从机的Flash写入一组数据,并从从机的Flash中将数据读取,对写入数据和读出数据进行对比,验证实验是否成功。
注意事项
- 在进行Flash的写操作前必须进行写使能和扇区擦除;
- 在进行Flash的写操作后必须进行等待忙状态的处理;
- W25Q64的地址为24位,要分三次发送。
二、硬件SPI
SPI外设
- STM32内部集成了硬件SPI收发电路,可以由硬件自动执行时钟生成、数据收发等功能,减轻CPU的负担;
- 可配置8位/16位数据帧、高位先行/低位先行;
- 时钟频率:f / (2、4、8、16、32、64、128、256);
- 支持多主机模型、主从操作;
- 功能可裁剪位半双工/单工通信;
- 支持DMA;
- 兼容I2S协议;
- STM32F103C8T6硬件SPI资源:SPI1、SPI2
SPI外设的基本结构图

对单片机SPI外设的初始化配置如上图顺序。
主模式全双工连续传输

此模式下SPI传输数据速度更快,会提前将下一个数据传入发送缓冲器中。
非连续传输

非连续传输,速度相对较慢,但是时序结构简单,不易出错。
程序
先对W25Q64模块进行编写,W25Q64.c文件:
#include "stm32f10x.h" // Device header
#include "W25Q64_Ins.h"
//对于从机的选择还是选择使用软件触发
void SPI_SS(uint8_t Value)
{
GPIO_WriteBit (GPIOA ,GPIO_Pin_4,(BitAction)Value);
}
//对从机进行选择
void MySPI_Start(void)
{
SPI_SS(0);
}
//取消对从机的选择
void MySPI_Stop(void)
{
SPI_SS(1);
}
//函数:对从机进行初始化配置
void W25Q64_Init(void)
{
//打开APB2总线上GPIOA、SPI1时钟
RCC_APB2PeriphClockCmd (RCC_APB2Periph_GPIOA ,ENABLE);
RCC_APB2PeriphClockCmd (RCC_APB2Periph_SPI1 ,ENABLE);
//对PA4进行初始化配置,配置为推挽输出模式
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode =GPIO_Mode_Out_PP;
GPIO_InitStruct.GPIO_Pin =GPIO_Pin_4;
GPIO_InitStruct.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init (GPIOA ,&GPIO_InitStruct );
//对PA5、PA7进行初始化配置,配置为复用推挽输出模式
GPIO_InitStruct.GPIO_Mode =GPIO_Mode_AF_PP;
GPIO_InitStruct.GPIO_Pin =GPIO_Pin_5 | GPIO_Pin_7;
GPIO_InitStruct.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init (GPIOA ,&GPIO_InitStruct );
//对PA6进行初始化配置,配置为上拉输入模式
GPIO_InitStruct.GPIO_Mode =GPIO_Mode_IPU;
GPIO_InitStruct.GPIO_Pin =GPIO_Pin_6;
GPIO_InitStruct.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init (GPIOA ,&GPIO_InitStruct );
//对SPI外设单元进行初始化配置
SPI_InitTypeDef SPI_InitStructure;
//将 STM32选择为主机
SPI_InitStructure.SPI_Mode =SPI_Mode_Master;
//将从机选择置为软件触发
SPI_InitStructure.SPI_NSS =SPI_NSS_Soft ;
//选择SPI模式0
SPI_InitStructure.SPI_CPHA =SPI_CPHA_1Edge ;
SPI_InitStructure.SPI_CPOL =SPI_CPOL_Low;
//选择128分频
SPI_InitStructure.SPI_BaudRatePrescaler =SPI_BaudRatePrescaler_128 ;
//
SPI_InitStructure.SPI_CRCPolynomial =0;
//将传输数据帧选择为8位
SPI_InitStructure.SPI_DataSize =SPI_DataSize_8b;
//选择两线全双工传输模式
SPI_InitStructure.SPI_Direction =SPI_Direction_2Lines_FullDuplex ;
//选择高位先行
SPI_InitStructure.SPI_FirstBit =SPI_FirstBit_MSB;
//初始化SPI
SPI_Init(SPI1 ,&SPI_InitStructure);
//打开SPI1通道
SPI_Cmd(SPI1 ,ENABLE );
}
//将下面这些函数的发送部分用"SPI_I2S_SendData()",接收部分用"SPI_I2S_ReceiveData()"
void Read_ID(uint8_t *MF,uint16_t *ID)
{
MySPI_Start ();
SPI_I2S_SendData(SPI1,W25Q64_JEDEC_ID);
*MF = SPI_I2S_ReceiveData(SPI1);
*ID = SPI_I2S_ReceiveData(SPI1);
*ID <<= 8;
*ID |= SPI_I2S_ReceiveData(SPI1);
MySPI_Stop ();
}
void Write_Enable(void)
{
MySPI_Start ();
SPI_I2S_SendData(SPI1,W25Q64_WRITE_ENABLE);
MySPI_Stop ();
}
void Wait_Busy(void)
{
uint32_t Timeout = 100000;
MySPI_Start ();
SPI_I2S_SendData(SPI1,W25Q64_READ_STATUS_REGISTER_1);
while((SPI_I2S_ReceiveData(SPI1) & 0x01) == 0x01)
{
Timeout --;
if(Timeout == 0 )
{
break ;
}
}
MySPI_Stop ();
}
//在写入数据前对TXE标志位进行判断
void Page_Program(uint32_t address,uint8_t *DataArray,uint16_t count)
{
uint8_t i;
Write_Enable ();
MySPI_Start ();
SPI_I2S_SendData (SPI1,W25Q64_PAGE_PROGRAM);
SPI_I2S_SendData (SPI1,address >> 16);
SPI_I2S_SendData (SPI1,address >> 8);
SPI_I2S_SendData (SPI1,address);
for(i=0;i<count;i++)
{
while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_TXE) == RESET);
SPI_I2S_SendData (SPI1,DataArray [i]);
}
MySPI_Stop ();
Wait_Busy ();
}
void Sector_Erase(uint32_t address)
{
Write_Enable ();
MySPI_Start ();
SPI_I2S_SendData (SPI1,W25Q64_SECTOR_ERASE_4KB);
SPI_I2S_SendData (SPI1,address >> 16);
SPI_I2S_SendData (SPI1,address >> 8);
SPI_I2S_SendData (SPI1,address);
MySPI_Stop ();
Wait_Busy ();
}
//在读取数据前对RXNE标志位进行判断
void Read_Data(uint32_t address,uint8_t *DataArray,uint16_t count)
{
uint16_t i;
MySPI_Start ();
SPI_I2S_SendData (SPI1,W25Q64_READ_DATA);
SPI_I2S_SendData (SPI1,address >> 16);
SPI_I2S_SendData (SPI1,address >> 8);
SPI_I2S_SendData (SPI1,address);
for(i=0;i<count;i++)
{
while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_RXNE) == RESET);
DataArray[i] = SPI_I2S_ReceiveData(SPI1);
}
MySPI_Stop ();
}
W25Q64.h文件:
#ifndef _W25Q64_H
#define _W25Q64_H
void W25Q64_Init(void);
void Read_ID(uint8_t *MF,uint16_t *ID);
void Write_Enable(void);
void Wait_Busy(void);
void Page_Program(uint32_t address,uint8_t *DataArray,uint16_t count);
void Sector_Erase(uint32_t address);
void Read_Data(uint32_t address,uint8_t *DataArray,uint16_t count);
#endif
W25Q64_Ins.h文件:
#ifndef __W25Q64_INS_H
#define __W25Q64_INS_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
最后在主函数中调用各个函数实现数据的写入读取,main.c文件:
#include "stm32f10x.h" // Device header
#include "OLED.h"
#include "W25Q64.h"
uint8_t MID;
uint16_t DID;
uint8_t Data_W[3]={0x09,0x77,0x88};
uint8_t Data_R[3];
int main(void)
{
OLED_Init();
W25Q64_Init ();
OLED_ShowString (1,1,"MID: DID:");
Read_ID(&MID,&DID);
OLED_ShowHexNum (1,5,MID,2);
OLED_ShowHexNum (1,12,DID,4);
OLED_ShowString (2,1,"Data_W:");
OLED_ShowString (3,1,"Data_R:");
Sector_Erase (0x000000);
Page_Program (0x000000,Data_W,3);
OLED_ShowHexNum (2,8,Data_W[0],2);
OLED_ShowHexNum (2,11,Data_W[1],2);
OLED_ShowHexNum (2,14,Data_W[2],2);
Read_Data (0x000000,Data_R,3);
OLED_ShowHexNum (3,8,Data_R[0],2);
OLED_ShowHexNum (3,11,Data_R[1],2);
OLED_ShowHexNum (3,14,Data_R[2],2);
while(1)
{
}
}
最终效果与软件I2C一致。
注意事项
- 对从机的选择用软件触发比较方便;
- 硬件SPI的SS需要配置为推挽输出、MOSI,SCL配置为复用输出模式、MISO配置为上拉输入模式。