“实现SPI协议与GPIO模拟SPI的美化方案”

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与I2C特点对比

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第二个边沿采样数据。
SPI时序

数据传输流程

首先主机和从机都选择同一传输模式。然后主机片选拉低,选中从机。接着在时钟的驱动下, 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;
}  
物联沃分享整理
物联沃-IOTWORD物联网 » “实现SPI协议与GPIO模拟SPI的美化方案”

发表评论