一、SPI通信
1、SPI(Serial Peripheral Interface)是由Motorola公司开发的一种通用数据总线
2、四根通信线:SCK(Serial Clock)、MOSI(Master Output Slave Input)、MISO(Master Input Slave Output)、SS(Slave Select)
3、同步,全双工
4、支持总线挂载多设备(一主多从)
5、硬件电路
(1)所有SPI设备的SCK、MOSI、MISO分别连在一起
(2)主机另外引出多条SS控制线,分别接到各从机的SS引脚
(3)输出引脚配置为推挽输出,输入引脚配置为浮空或上拉输入
6、移位示意图
7、SPI时序基本单元
(1)起始条件:SS从高电平切换到低电平
(2)终止条件:SS从低电平切换到高电平
(3)交换一个字节(模式0)
CPOL=0:空闲状态时,SCK为低电平
CPHA=0:SCK第一个边沿移入数据,第二个边沿移出数据
CPOL表示时钟极性
CPHA表示时钟相位,决定是第一个时钟采样移入还是第二个时钟采样移入
(4)交换一个字节(模式1)
CPOL=0:空闲状态时,SCK为低电平
CPHA=1:SCK第一个边沿移出数据,第二个边沿移入数据
(5)交换一个字节(模式2)
CPOL=1:空闲状态时,SCK为高电平
CPHA=0:SCK第一个边沿移入数据,第二个边沿移出数据
(6)交换一个字节(模式3)
CPOL=1:空闲状态时,SCK为高电平
CPHA=1:SCK第一个边沿移出数据,第二个边沿移入数据
8、SPI时序
(1)发送指令
向SS指定的设备,发送指令(0x06)
(2)指定地址写
向SS指定的设备,发送写指令(0x02), 随后在指定地址(Address[23:0])下,写入指定数据(Data)
(3)指定地址读
向SS指定的设备,发送读指令(0x03), 随后在指定地址(Address[23:0])下,读取从机数据(Data)
二、W25Q64简介
1、W25Q64简介
(1)W25Qxx系列是一种低成本、小型化、使用简单的非易失性存储器,常应用于数据存储、字库存储、固件程序存储等场景
(2)存储介质:Nor Flash(闪存)
(3)时钟频率:80MHz / 160MHz (Dual SPI) / 320MHz (Quad SPI)
(4)存储容量(24位地址):
W25Q40: 4Mbit / 512KByte
W25Q80: 8Mbit / 1MByte
W25Q16: 16Mbit / 2MByte
W25Q32: 32Mbit / 4MByte
W25Q64: 64Mbit / 8MByte
W25Q128: 128Mbit / 16MByte
W25Q256: 256Mbit / 32MByte
2、硬件电路
3、W25Q64框图
4、Flash操作注意事项
(1)写入操作时:
a.写入操作前,必须先进行写使能
b.每个数据位只能由1改写为0,不能由0改写为1
c.写入数据前必须先擦除,擦除后,所有数据位变为1
d.擦除必须按最小擦除单元进行(最小的擦除单元是一个扇区)
e.连续写入多字节时,最多写入一页的数据,超过页尾位置的数据,会回到页首覆盖写入
f.写入操作结束后,芯片进入忙状态,不响应新的读写操作
(2)读取操作时:
a.直接调用读取时序,无需使能,无需额外操作,没有页的限制,读取操作结束后不会进入忙状态,但不能在忙状态时读取
三、软件SPI读写W25Q64
1、按照以下接线方式连接,并将STLINK插到电脑上
2、多层模块架构
(1)最底层:SPI层
MySPI.c
#include "stm32f10x.h" // Device header
/*
丛机选择
*/
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)
{
GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_6);
}
/*
初始化SPI
*/
void MySPI_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;//推挽输出
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_4 | GPIO_Pin_5 | 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;//MISO:输入
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
MySPI_W_SS(1);//丛机先不选
MySPI_W_SCK(0);//模式0
}
/*
起始条件
*/
void MySPI_Start(void)
{
MySPI_W_SS(0);
}
/*
终止条件
*/
void MySPI_Stop(void)
{
MySPI_W_SS(1);
}
/*
交换一个字节(模式0)掩码
*/
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{
uint8_t i,ByteReceive=0x00;
for(i=0;i<8;i++)
{
MySPI_W_MOSI(ByteSend & (0x80>>i));//使用掩码依次提取数据每一位
MySPI_W_SCK(1);
if(MySPI_R_MISO() == 1){ByteReceive |= (0x80>>i);}
MySPI_W_SCK(0);
}
return ByteReceive;
}
///*
// 交换一个字节(模式0)移位
//*/
//uint8_t MySPI_SwapByte(uint8_t ByteSend)
//{
// uint8_t i;
//
// for(i=0;i<8;i++)
// {
// MySPI_W_MOSI(ByteSend & 0x80);//用移位数据本身来进行操作,效率高,但是ByteSend在移位的过程中改变了
// ByteSend <<= 1;
// MySPI_W_SCK(1);
// if(MySPI_R_MISO() == 1){ByteSend |= 0x01;}
// MySPI_W_SCK(0);
// }
//
// return ByteSend;
//}
MySPI.h
#ifndef __MYSPI_H
#define __MYSPI_H
void MySPI_Init(void);
void MySPI_Start(void);
void MySPI_Stop(void);
uint8_t MySPI_SwapByte(uint8_t ByteSend);
#endif
(2)协议层之上:W25Q64的驱动层
W25Q64.c
#include "stm32f10x.h" // Device header
#include "MySPI.h"
#include "W25Q64_Ins.h"
/*
初始化W25Q64
*/
void W25Q64_Init(void)
{
MySPI_Init();
}
/*
获取ID号
*/
void W25Q64_ReadID(uint8_t *MID,uint16_t *DID)//厂商ID、设备ID
{
MySPI_Start();
MySPI_SwapByte(W25Q64_JEDEC_ID);
*MID = MySPI_SwapByte(W25Q64_DUMMY_BYTE);//厂商ID
*DID = MySPI_SwapByte(W25Q64_DUMMY_BYTE);//设备ID高八位
*DID <<= 8;
*DID |= MySPI_SwapByte(W25Q64_DUMMY_BYTE);//设备ID低八位,用移位的方法得到16位设备ID
MySPI_Stop();
}
/*
写使能
*/
void W25Q64_WriteEnable(void)
{
MySPI_Start();
MySPI_SwapByte(W25Q64_WRITE_ENABLE);
MySPI_Stop();
}
/*
读状态寄存器忙等待
*/
void W25Q64_WaitBusy(void)
{
uint32_t Timeout;
MySPI_Start();
MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1);
Timeout =100000;
while((MySPI_SwapByte(W25Q64_DUMMY_BYTE) & 0x01) == 0x01) //busy为1,进入循环,直到busy为0跳出
{
Timeout--;
if(Timeout ==0)
{
break;
}
}
MySPI_Stop();
}
/*
页编程
*/
void W25Q64_PageProgram(uint32_t Address,uint8_t *DataArray,uint16_t Count)
{
uint16_t i;
W25Q64_WriteEnable();//写入操作前,必须先进行写使能
MySPI_Start();
MySPI_SwapByte(W25Q64_PAGE_PROGRAM);
MySPI_SwapByte(Address >> 16);//高八位
MySPI_SwapByte(Address >> 8);//中八位,高中16位,只能存8位,所以舍弃高八位
MySPI_SwapByte(Address);//低八位,高中低24位,只能存8位,所以舍弃高中16位
for(i=0;i<Count;i++)
{
MySPI_SwapByte(DataArray[i]);//发送写入数据
}
MySPI_Stop();
W25Q64_WaitBusy();//写入操作结束后,芯片进入忙状态,必须进入忙等待
}
/*
擦除扇区
*/
void W25Q64_SectorErase(uint32_t Address)
{
W25Q64_WriteEnable();//写入操作前,必须先进行写使能
MySPI_Start();
MySPI_SwapByte(W25Q64_SECTOR_ERASE_4KB);
MySPI_SwapByte(Address >> 16);//高八位
MySPI_SwapByte(Address >> 8);//中八位,高中16位,只能存8位,所以舍弃高八位
MySPI_SwapByte(Address);//低八位,高中低24位,只能存8位,所以舍弃高中16位
MySPI_Stop();
W25Q64_WaitBusy();//写入操作结束后,芯片进入忙状态,必须进入忙等待
}
/*
读取数据
*/
void W25Q64_ReadData(uint32_t Address,uint8_t *DataArray,uint32_t Count)
{
uint32_t i;
MySPI_Start();
MySPI_SwapByte(W25Q64_READ_DATA);
MySPI_SwapByte(Address >> 16);//高八位
MySPI_SwapByte(Address >> 8);//中八位,高中16位,只能存8位,所以舍弃高八位
MySPI_SwapByte(Address);//低八位,高中低24位,只能存8位,所以舍弃高中16位
for(i=0;i<Count;i++)
{
DataArray[i] = MySPI_SwapByte(W25Q64_DUMMY_BYTE);
}
MySPI_Stop();
}
W25Q64.h
#ifndef __W25Q64_H
#define __W25Q64_H
void W25Q64_Init(void);
void W25Q64_ReadID(uint8_t *MID,uint16_t *DID);
void W25Q64_PageProgram(uint32_t Address,uint8_t *DataArray,uint16_t Count);
void W25Q64_SectorErase(uint32_t Address);
void W25Q64_ReadData(uint32_t Address,uint8_t *DataArray,uint32_t Count);
#endif
(3)应用层:主函数
mian.c
#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_ReadID(&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)
{
}
}
3、实现效果
(1)掉电不丢失:注释掉擦除和写入的代码,断电后重启,读取的数据不变
(2)Flash擦除之后变为FF:注释掉写入的代码,只擦除不写入
(3)Flash只能1写0,不能0写1:
a.先写入AA、BB、CC、DD
b.注释擦除,写入55、66、77、88,尝试不擦除直接写入
(4)写入数据不能跨页:
修改页编辑的地址为0x0000FF,一页的最后一个地址,从最后一个地址开始写,验证是否可以跨页到下一页的0x000100
修改读取数据地址为0x0000FF,也从最后一个开始读
再将读取数据地址修改为0x000000
四、SPI外设简介
1、SPI外设简介
(1)STM32内部集成了硬件SPI收发电路,可以由硬件自动执行时钟生成、数据收发等功能,减轻CPU的负担
(2)可配置8位/16位数据帧、高位先行/低位先行
(3)时钟频率: fPCLK / (2, 4, 8, 16, 32, 64, 128, 256)
(4)支持多主机模型、主或从操作
(5)可精简为半双工/单工通信
(6)支持DMA
(7)兼容I2S协议(数字音频信号传输的专用协议)
(8)STM32F103C8T6 硬件SPI资源:SPI1(挂载在APB2,PCLK是72M)、SPI2(挂载在APB1,PCLK是36M)
2、SPI框图
3、SPI基本结构
4、主模式全双工连续传输
5、非连续传输
6、软件/硬件波形对比
五、硬件SPI读写W25Q64
1、SPI引脚
2、修改底层的MySPI.c文件
把初始化和时序的执行步骤,由软件实现改成硬件实现
(1)SPI库函数的功能
(2)MySPI.c
#include "stm32f10x.h" // Device header
/*
丛机选择(软件模拟)
*/
void MySPI_W_SS(uint8_t BitValue)
{
GPIO_WriteBit(GPIOA,GPIO_Pin_4,(BitAction)BitValue);
}
/*
初始化SPI(硬件)
*/
void MySPI_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//开启时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1,ENABLE);
GPIO_InitTypeDef GPIO_InitStructure; //初始化GPIO
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;//通用推挽输出
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_4;//SS软件控制的输出信号
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF_PP;//复用推挽输出
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_5 | 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);
SPI_InitTypeDef SPI_InitStructure; //初始化SPI外设
SPI_InitStructure.SPI_Mode=SPI_Mode_Master;//主模式
SPI_InitStructure.SPI_Direction=SPI_Direction_2Lines_FullDuplex;//双线全双工
SPI_InitStructure.SPI_DataSize=SPI_DataSize_8b;//配置8位数据帧
SPI_InitStructure.SPI_FirstBit=SPI_FirstBit_MSB;//高位先行
SPI_InitStructure.SPI_BaudRatePrescaler=SPI_BaudRatePrescaler_128;//波特率预分频器
SPI_InitStructure.SPI_CPOL=SPI_CPOL_Low;//模式0
SPI_InitStructure.SPI_CPHA=SPI_CPHA_1Edge;//第一个边沿开始采样,SCK第一个边沿移入数据,CPHA=0
SPI_InitStructure.SPI_NSS=SPI_NSS_Soft;//软件NSS
SPI_InitStructure.SPI_CRCPolynomial=7;
SPI_Init(SPI1,&SPI_InitStructure);
SPI_Cmd(SPI1,ENABLE); //使能
MySPI_W_SS(1);//丛机先不选
}
/*
起始条件(软件模拟)
*/
void MySPI_Start(void)
{
MySPI_W_SS(0);
}
/*
终止条件(软件模拟)
*/
void MySPI_Stop(void)
{
MySPI_W_SS(1);
}
/*
交换一个字节(非连续传输)
*/
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{
while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_TXE) != SET);//等待TXE为1
SPI_I2S_SendData(SPI1,ByteSend);//写发送数据至TDR,一旦写入数据时序就会自动生成
while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_RXNE) != SET);//等待RXNE为1,表示收到一个字节,同时表示发送时序产生完成
return SPI_I2S_ReceiveData(SPI1);//读取RDR接受的数据,就是置换接收的一个字节
}
3、实现效果
只需要修改底层的MySPI.c文件,其他代码和软件实现一样,最终实现的效果也和软件一样,不再重复展示