目录
什么是SPI
SPI 是英语Serial Peripheral interface的缩写,顾名思义就是串行外围设备接口。是Motorola(摩托罗拉)首先在其MC68HCXX系列处理器上定义的。
SPI,是一种高速 的,全双工 ,同步的通信总线,并且在芯片的管脚上只占用四根线,节约了芯片的管脚,同时为PCB的布局上节省空间,提供方便,主要应用在 EEPROM,FLASH,实时时钟,AD转换器,还有数字信号处理器和数字信号解码器之间。
SPI分为主、从两种模式,一个SPI通讯系统需要包含一个(且只能是一个)主设备,一个或多个从设备。提供时钟的为主设备(Master),接收时钟的设备为从设备(Slave),SPI接口的读写操作,都是由主设备发起。当存在多个从设备时,通过各自的片选信号进行管理。
四根通信线:SCK(Serial Clock)时钟线、MOSI(Master Output Slave Input)主机输出从机输入、MISO(Master Input Slave Output)主机输入从机输出、SS(Slave Select)从机选择线。
有的设备也叫这四根线为SCLK, SDI, SDO, CS
CS/SS:从设备片选信号,由主设备控制。它的功能是用来作为"片选引脚",也就是选择指定的从设备,让主设备可以单独地与特定从设备通讯,避免数据线上的冲突。SPI通信协议规定每个从机都有一根片选信号线,所以这个线不止一根。
对于IIC来说,由于IIC是开漏输出的结构,属于弱上拉,这就使得由低到高时,上升沿时间较长,这就限制了最大通信速度。
SPI是全双工且SPI没有定义速度限制,一般的实现通常能达到甚至超过10 Mbps。
相较于IIC,SPI也是没有应答机制的,发送和接收都不会检查对方是否存在。
SPI硬件电路
所有SPI设备的SCK、MOSI、MISO分别连在一起。
主机另外引出多条SS控制线,分别接到各从机的SS引脚。
输出 引脚配置为推挽 输出,推挽输出的上拉和下拉能力都很强,所以上拉或者下拉的速度就很快,这就保证了他可以快速的完成电平的转换,这就保证了更高的传输速度。输入 引脚配置为浮空或上拉输入。

可以看到,所以设备的MISO都是连接在一起的,所以此时如果多个从机发数据就会造成信号的冲突,所以需要一些办法规避这些冲突。于是SPI协议就规定当从机没有被选中的时候,MISO引脚要表现为高阻态,高阻态就相当于引脚断开,不输出任何电平。只有被选中了(SS为低电平)才能选为推挽输出。
没被选中和被选中了,这些状态都是在从机中自动变化的,我们配置的是主机的程序,所以我们不需要关心。
SPI收发数据
SPI基本的收发电路就是下面这样的一个移位的模型
主机和从机都有一个八位的移位寄存器,主机上的时钟线每来一个时钟,数据左移一位。主机移除的数据移到从机的右边,从机移除的数据移到主机的右边,形成一个环。在时钟的上升沿,数据移出,移到引脚上,其实就是放在了数据输出寄存器上。时钟的下降沿,数据移入,写入到移位寄存器。 八个时钟后,主机和从机的数据就完成了交换。SPI的数据收发就是基于字节交换这个基本单元来进行的。我们只接收不发送就可以随便发一个数据,将从机的数据置换过来就好了。或者只发送不接收,那就接收到的数据不读就好了。
SPI时序
起始条件:SS从高电平切换到低电平
终止条件:SS从低电平切换到高电平
下降沿是通信的开始,上升沿是通信的结束。
时序基本单元:
SPI通信有4种不同的操作模式,不同的从设备可能在出厂时就是配置为某种模式,这是不能改变的;但我们的通信双方必须是工作在同一模式下,所以我们可以对我们的主设备的SPI模式进行配置,通过CPOL(时钟极性)和CPHA(时钟相位)来控制我们主设备的通信模式。这就有了四种通信模式。
软件模拟SPI的话,我们只需要遵守从机的模式就行了,不同的从机支持一种或者多种模式,选择任意一种即可,使用的时候需要注意时序,我们给不同的时序,从机可以识别到,因为不同的模式时序是不一样的,时序对,才能正确的控制从机。
时钟极性(CPOL)定义了时钟空闲状态电平:
- CPOL=0,表示当SCLK=0时处于空闲态,所以有效状态就是SCLK处于高电平时
- CPOL=1,表示当SCLK=1时处于空闲态,所以有效状态就是SCLK处于低电平时
时钟相位(CPHA)定义数据的采集时间:
- CPHA=0,在时钟的第一个跳变沿(上升沿或下降沿)进行数据采样。在第2个边沿发送数据
- CPHA=1,在时钟的第二个跳变沿(上升沿或下降沿)进行数据采样。在第1个边沿发送数据
需要注意的是CPHA只是决定第几个边沿采样,并不决定是上升沿还是下降沿采样。
模式0

模式0在第一个边沿就要移入数据,所以在SCK第一个边沿之前就要提前移出数据。所以在SS的下降沿就要移出数据了。
模式0也是最常用的模式。
模式1

上图中,在SS未被选中的时候,MISO用一条中间的线来表示高阻态。
模式2

与模式0的SCK相反
模式3

与模式1的SCK相反
SPI第一个时钟周期发送的信息根据芯片的不同也会不同,有的是指令(比如写使能),如果指令长度大于一个字节,那么也可能多个周期都是指令。
IIC的有效数据流第一个字节是寄存器地址,之后是读写的数据,使用的是读写寄存器的模型。
而SPI通常采用指令码+读写数据的模型。SPI起始后,第一个发送给从机的数据,一般叫做指令码。在从机中对应的会定义一个指令集,当我们需要发送什么指令时,就可以在起始后的第一个字节发送指令集里面的数据。这样就能指导从机完成相应的功能了。不同的指令可以由不同的数据个数。
W25Q64简介
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
硬件电路
WP:写保护,配合内部的寄存器配置可以实现硬件的写保护。写保护低电平有效,WP接低电平,保护住不让写。
HOLD:数据保持,低电平有效。如果在进行正常读写时,突然产生中断,然后想用SPI通信线去操控其他器件,这时如果把CS置回高电平,时序就终止了,但如果又不想终止总线,又想操作其他器件,这就可以HOLD引脚置低电平,这样芯片就HOLD住了。芯片释放总线,但是时序不会终止,他会记住当前的状态。当操作完其他器件可以回过来,HOLD置回高电平,然后继续HOLD之前的时序。
对于W25Q64内部的存储是将整个空间划分为很块,64KB为一块,一块划分为若干扇区,一个扇区4KB,一个扇区划分为若干页,一页256字节。
Flash操作注意事项
Flash为了保证掉电不丢失,存储容量足够大,成本足够低,所以Flash存储器回在其他地方,比如操作的便捷性等做出一些让步。所以Flash的写入和读取并不像RAM那样简单直接。
写入操作时:
- 写入操作前,必须先进行写使能
- 每个数据位只能由1改写为0,不能由0改写为1
- 写入数据前必须先擦除,擦除后,所有数据位变为1
- 擦除必须按最小擦除单元进行
- 连续写入多字节时,最多写入一页的数据,超过页尾位置的数据,会回到页首覆盖写入。写入操作是先写入到RAM(缓存区)中,W25Q64的RAM大小是256字节,就是一页的大小,所以写操作不能超过页的大小。而且写入操作必须是在页首的地址才行,如果是页中间,那就是在RAM中间开始写的,此时写到RAM底部,再向下写,就会回到RAM顶部开始写。此时写完,再拷贝到Flash中,就会造成数据错乱。
- 写入(包括擦除)操作结束后,芯片进入忙状态,不响应新的读写操作。因为要将缓冲区的数据写入到FLASH。
读取操作时:
- 直接调用读取时序,无需使能,无需额外操作,没有页的限制,读取操作结束后不会进入忙状态,但不能在忙状态时读取。
SPI代码实现
c
/*
将W25Q64的
CS端接PA4
DO端接PA6
CLK接PA5
DI端接PA7
*/
void MySPI_W_CS(uint8_t bit_value)
{
GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)bit_value);
}
void MySPI_W_SCK(uint8_t bit_value)
{
GPIO_WriteBit(GPIOA, GPIO_Pin_5, (BitAction)bit_value);
}
void MySPI_W_MOSI(uint8_t bit_value)
{
GPIO_WriteBit(GPIOA, GPIO_Pin_7, (BitAction)bit_value);
}
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_Init(GPIOA, &GPIO_InitStructure);
//修改引脚默认电平
MySPI_W_SCK(0);//默认选择模式0
MySPI_W_CS(1);//默认不选中
}
//W25Q64支持模式0和模式3
void MySPI_Start()//使用模式0
{
MySPI_W_CS(0);
}
void MySPI_Stop()
{
MySPI_W_CS(1);
}
//使用模式0的时序
uint8_t MySPI_SwapByte(uint8_t ByteSend)//返回值为读取一个数据。ByteSend为交换的时候要给的数据
{
for(int i = 0; i < 8; i++)
{
MySPI_W_MOSI(ByteSend & 0x80);
ByteSend <<= 1;//左移1位,最低位补0
MySPI_W_SCK(1);
if(MySPI_R_MISO() == 1)
{
ByteSend |= 0x01;
}
MySPI_W_SCK(0);
}
return ByteSend;
}
//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;
//}