SPI定义
SPI(Serial Peripheral Interface, 串口外设接口),它用于MCU与各种外围设备以串行方式进行通信,速度最高可达25MHz以上。
SPI接口主要应用在EEPROM、 FLASH、实时时钟、网络控制器、 OLED显示驱动器、 AD转换器,数字信号处理器、数字信号解码器等设备之间。
SPI通常由四条线组成,一条主设备输出与从设备输入( Master Output Slave Input, MOSI),一条主设备输入与从设备输出( Master Input Slave Output, MISO),一条时钟信号( Serial Clock, SCLK),一条从设备使能选择( Chip Select, CS)。与I²C类似,协议比较简单,也可以使用GPIO模拟SPI时序。
SPI的数据交换
在SCLK时钟周期的驱动下,MOSI和MISO同时进行,如下图所示,可以看作一个虚拟的环形拓扑结构。主机和从机都有一个移位寄存器,主机移位寄存器数据经过MOSI将数据写入从机的移位寄存器,此时从机移位寄存器的数据也通过MISO传给了主机,实现了两个移位寄存器的数据交换。无论主机还是从机,发送和接收都是同时进行的,如同一个“环”。 需要注意的是,数据的写入顺序是从高地址到低地址。
如果主机只对从机进行写操作,主机只需忽略接收的从机数据即可;如果主机要读取从机数据,需要主机发送一个空数据来引发从机发送数据。
传输模式
SPI有四种传输模式,主要差别在于CPOL和CPHA的不同。
CPOL( Clock Polarity,时钟极性) 表示SCK在空闲时为高电平还是低电平。 当CPOL=0, SCK空闲时为
低电平, 当CPOL=1, SCK空闲时为高电平。
CPHA( Clock Phase,时钟相位) 表示SCK在第几个时钟边缘采样数据。 当CPHA=0, 在SCK第一个边
沿采样数据,当CPHA=1, 在SCK第二个边沿采样数据。
数据传输流程
首先主机和从机都选择同一传输模式。然后主机片选拉低,选中从机。接着在时钟的驱动下, MOSI发送数据,同时MISO读取接收数据。最后完成传输,取消片选。
模拟SPI
/*
* 函数名: void SPI_WriteByte(uint8_t data)
* 输入参数: data -> 要写的数据
* 输出参数:无
* 返回值:无
* 函数作用:模拟 SPI 写一个字节
*/ SPI写1 Byte,循环8次,每次发送1 Bit;
void SPI_WriteByte(uint8_t data) {
uint8_t i = 0;
uint8_t temp = 0;
for(i=0; i<8; i++) {
temp = ((data&0x80)==0x80)? 1:0; //将data最高位保存到temp;
data = data<<1; //data左移一位,将次高位变为最高位,用于下次取最高位;
SPI_CLK(0); //CPOL=0 //拉低时钟,即空闲时钟为低电平, CPOL=0;
SPI_MOSI(temp); //根据temp值,设置MOSI引脚的电平;
SPI_Delay(); //简单延时,可以定时器或延时函数实现
SPI_CLK(1); //CPHA=0 //拉高时钟, W25Q64只支持SPI模式0或1,即会在时钟上升沿采样MOSI数据;
SPI_Delay();
}
SPI_CLK(0); //最后SPI发送完后,拉低时钟,进入空闲状态;
}
/*
* 函数名: uint8_t SPI_ReadByte(void)
* 输入参数:
* 输出参数:无
* 返回值:读到的数据
* 函数作用:模拟 SPI 读一个字节
*/ SPI读1 Byte,循环8次,每次接收1 Bit;
uint8_t SPI_ReadByte(void) {
uint8_t i = 0;
uint8_t read_data = 0xFF;
for(i=0; i<8; i++) {
read_data = read_data << 1; //“腾空” read_data最低位,8次循环后,read_data将高位在前;
SPI_CLK(0); //拉低时钟,即空闲时钟为低电平;
SPI_Delay();
SPI_CLK(1);
SPI_Delay();
if(SPI_MISO()==1) {
read_data = read_data + 1;
}
}
SPI_CLK(0); //最后SPI读取完后,拉低时钟,进入空闲状态
return read_data;
}
前面提到SPI传输可以看作一个虚拟的环形拓扑结构,即输入和输出同时进行。在前面“ SPI_WriteByte()”函数里,发送了1 Byte,也应该接收1 Byte,只是代码中忽略了接收引脚MISO的状态; 在前面“ SPI_ReadByte()”函数里,接收了1 Byte,也应该发送1 Byte,只是代码中忽略了发送引脚MOSI的内容。有些场景, SPI需要同时读写,因此还需要编写SPI同时读写函数。
/*
* 函数名: uint8_t SPI_WriteReadByte(uint8_t data)
* 输入参数: data -> 要写的一个字节数据
* 输出参数:无
* 返回值:读到的数据
* 函数作用:模拟 SPI 读写一个字节
*/SPI读和写1 Byte,循环8次,每次发送和接收1 Bit;
uint8_t SPI_WriteReadByte(uint8_t data) {
uint8_t i = 0;
uint8_t temp = 0;
uint8_t read_data = 0xFF;
for(i=0;i<8;i++) {
temp = ((data&0x80)==0x80)? 1:0; //将data最高位保存到temp;
data = data<<1; //data左移一位,将次高位变为最高位,用于下次取最高位;
read_data = read_data<<1; //“腾空” read_data最低位,8次循环后,read_data将高位在前;
SPI_CLK(0);
SPI_MOSI(temp);
SPI_Delay();
SPI_CLK(1);
SPI_Delay();
if(SPI_MISO()==1) { //读取MISO上的数据,保存到当前read_data最低位;
read_data = read_data + 1;
}
}
SPI_CLK(0);
return read_data;
}