一.简介
SPI(Serial Peripheral Interface)是由Motorola公司开发的一种通用数据总线
四根通信线:SCK(Serial Clock串行时钟线)、MOSI(Master Output Slave Input主机输出从机输入或者叫做DO数据输出)、MISO(Master Input Slave Output主机输入从机输出DI数据输入)、SS(Slave Select从机选择(NSS、CS))
同步,全双工(收发可以同时)
支持总线挂载多设备(一主多从)
SCK:时钟线
MOSI和MISO分别用于发送和接收的两条线路,MOSI是主机输出从机输入,MISO是主机从从机接收数据的线路
SS:从机选择线,有几个主机就开几个SS选择线
引脚选择
以W25Q64为例
W25Q64是从机,从机的DI对应到主机STM32应该是MOSI,从机的DO对应到STM32应该是MISO
二.硬件电路
所有的MISO连在一起,所有的MOSI连在一起。
SS在一个时刻只能选择一个从机将其置0,要不然会产生冲突。
三.移位示意图
上升沿 :所有移位寄存器向左移动一位,移出去的放在引脚上
下降沿: 移出去的位放在最低位上
四.SPI时序
四种交换模式
CPHA是决定第几个边沿采用,并不能单独决定上升沿还是下降沿
SCK要在第一个边沿到来之前就要先将数据移出去,等到第一个边沿到来的时候就可以直接移入数据。可以将SS的下降沿作为SCK的一部分,在SS下降沿到来的时候就可以将MOSI和MISO的数据移出去。
一开始SS没有选择具体的设备,所以从机设备要设置成高阻态,就对应从机的MISO电平处于中间位置(高阻态)。在SS置为高的时候,又要回到高阻态 。
在第一个边沿到来的时候,主机和从机同时移除最高位数据,在第二个边沿到来后移入数据。
指令时序
SPI发送
SPI指定地址写
SPI指定地址读
五.软件时序代码
#include "stm32f10x.h" // Device header
void MySPI_W_SS(uint8_t BitValue)//从机选择
{
GPIO_WriteBit(GPIOA,GPIO_Pin_4,(BitAction)BitValue);
}
void MySPI_W_MOSI(uint8_t BitValue)
{
GPIO_WriteBit(GPIOA,GPIO_Pin_7,(BitAction)BitValue);//MOSI
}
void MYSPI_W_SCK(uint8_t BitValue)
{
GPIO_WriteBit(GPIOA,GPIO_Pin_5,(BitAction)BitValue);//SCK
}
uint8_t MYSPI_R_MISO()//MISO
{
return GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_6);
}
void MySPI_Init()
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF_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;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
//SS置高电平
MySPI_W_SS(1);
MYSPI_W_SCK(0);//使用模式0
}
void MySPI_Start()
{
MySPI_W_SS(0);
}
void MySPI_Stop()
{
MySPI_W_SS(1);
}
uint8_t MySPI_SwapByte(uint8_t ByteSend)//将主机接收到的数据,用ByteSend置换出来
{
uint8_t ByteReceive=0x00;
for(int i=0;i<8;i++)
{
MySPI_W_MOSI(ByteSend&(0x80>>i));//发送的是ByteSend的最高位
MYSPI_W_SCK(1);
if(MYSPI_R_MISO()==1)
{
ByteReceive|=(0x80>>i);//将最高位存在ByteReceive里面
}
MYSPI_W_SCK(0);
}
return ByteReceive;
}
六.驱动W25Q64 Flash存储器
1.简介
W25Qxx系列是一种低成本、小型化、使用简单的非易失性存储器,常应用于数据存储、字库存储、固件程序存储等场景
存储介质:Nor Flash(闪存)
时钟频率:80MHz / 160MHz (Dual SPI) / 320MHz (Quad SPI)
存储容量(24位地址):
W25Q40: 4Mbit / 512KByte W25Q80: 8Mbit / 1MByte W25Q16: 16Mbit / 2MByte W25Q32: 32Mbit / 4MByte W25Q64: 64Mbit / 8MByte W25Q128: 128Mbit / 16MByte W25Q256: 256Mbit / 32MByte
(易失性存储器:SRAM,DRAM,非易失性存储器:Flash,E2PROM主要是掉电丢不丢失)
W25Q64不能跨页写入,如果写入的数据跨页了,就从本页第一个重新写入,而不是跨页写入。如果确实有个很大的数组需要连续写入,只能自己从软件上,分批次进行写入,计算数组需要跨多少页,然后该擦除的擦除,分批次一页一写。
2.操作注意事项
Flash是一个掉电不丢失的设备,为了保证掉电不丢失这个特性,所以会做出一些妥协让步。不像RAM那样简单直接,想在哪里写就在哪里写,想写多少就写多少,并且RAM是可以覆盖写入的。
写入操作时:
·写入操作前,必须先进行写使能
·每个数据位只能由1改写为0,不能由0改写为1
·写入数据前必须先擦除,擦除后,所有数据位变为1(弥补第二条缺陷)
·擦除必须按最小擦除单元进行
·连续写入多字节时,最多写入一页的数据,超过页尾位置的数据,会回到页首覆盖写入
·写入操作结束后,芯片进入忙状态,不响应新的读写操作
读取操作时:
·直接调用读取时序,无需使能,无需额外操作,没有页的限制,读取操作结束后不会进入忙状态,但不能在忙状态时读取
3.SPI代码
#include "stm32f10x.h" // Device header
#define MySPI_W_SS(x) do{x?GPIO_SetBits(GPIOA,GPIO_Pin_4):GPIO_ResetBits(GPIOA,GPIO_Pin_4);}while(0)
#define MySPI_W_SCK(x) do{x?GPIO_SetBits(GPIOA,GPIO_Pin_5):GPIO_ResetBits(GPIOA,GPIO_Pin_5);}while(0)
#define MySPI_W_MOSI(x) do{x?GPIO_SetBits(GPIOA,GPIO_Pin_7):GPIO_ResetBits(GPIOA,GPIO_Pin_7);}while(0)
uint8_t MySPI_R_MISO()
{
return GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_6);
}
void MySPI_Init()
{
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;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
MySPI_W_SS(1);
MySPI_W_SCK(0);
}
void MySPI_Start()
{
MySPI_W_SS(0);
}
void MySPI_Stop()
{
MySPI_W_SS(1);
}
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{
uint8_t ByteReceive=0x00;
for(uint8_t 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;
}
4.W25Q64代码
#include "stm32f10x.h" // Device header
#include "MySPI.h"
#include "W25Q64_Ins.h"
void W25Q64_Init()
{
MySPI_Init();
}
void W25Q64_ReadID(uint8_t *MID,uint16_t *DID)//MID厂商ID,DID设备ID
{
MySPI_Start();
MySPI_SwapByte(W25Q64_JEDEC_ID);
*MID=MySPI_SwapByte(W25Q64_DUMMY_BYTE);
*DID=MySPI_SwapByte(W25Q64_DUMMY_BYTE);
*DID<<=8;
*DID|=MySPI_SwapByte(W25Q64_DUMMY_BYTE);
MySPI_Stop();
}
void W25Q64_WriteEnable()
{
MySPI_Start();
MySPI_SwapByte(W25Q64_WRITE_ENABLE);//发送写使能
MySPI_Stop();
}
void W25Q64_WaitBusy()
{
uint32_t Timeout=100000;
MySPI_Start();
MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1);//读状态寄存器1
while((MySPI_SwapByte(W25Q64_DUMMY_BYTE)&0x01)==1)
{
Timeout--;
if(Timeout==0)break;//超时退出
}//用掩码取出最低位,如果是等于0x01代表忙
MySPI_Stop();
}
//页编程,发送指令码+3个字节的地址+数据,这里的写数组的参数是输入参数,写的数据保存在数组
void W25Q64_PageProgram(uint32_t Address,uint8_t *DataArray,uint16_t count)
{
W25Q64_WriteEnable();//写入操作前,必须先进行写使能
MySPI_Start();
MySPI_SwapByte(W25Q64_PAGE_PROGRAM);//页编程
MySPI_SwapByte(Address>>16);//取0x123456的0x12 1111 1111 1111 1111 1111 1111
MySPI_SwapByte(Address>>8);
MySPI_SwapByte(Address);
for(uint16_t i=0;i<count;i++)
{
MySPI_SwapByte(DataArray[i]);
}
MySPI_Stop();
W25Q64_WaitBusy();//写入操作结束后,芯片进入忙状态
}
//扇区擦除 发送指令码+3个字节的地址
void W25Q64_SectorErase(uint32_t Address)
{
W25Q64_WriteEnable();//写入操作前,必须先进行写使能
MySPI_Start();
MySPI_SwapByte(W25Q64_SECTOR_ERASE_4KB);
MySPI_SwapByte(Address>>16);
MySPI_SwapByte(Address>>8);
MySPI_SwapByte(Address);
MySPI_Stop();
W25Q64_WaitBusy();//写入操作结束后,芯片进入忙状态
}
//这里的数组是输出,将读出的数据放在数组中
void W25Q64_ReadData(uint32_t Address,uint8_t *DataArray,uint32_t 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();
}