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特点对比.png

SPI的数据交换

在SCLK时钟周期的驱动下, MOSI和MISO同时进行,如下图所示,可以看作一个虚拟的环形拓扑结构。主机和从机都有一个移位寄存器,主机移位寄存器数据经过MOSI将数据写入从机的移位寄存器,此时从机移位寄存器的数据也通过MISO传给了主机,实现了两个移位寄存器的数据交换。无论主机还是从机,发送和接收都是同时进行的,如同一个“环”。 需要注意的是,数据的写入顺序是从高地址到低地址。

如果主机只对从机进行写操作,主机只需忽略接收的从机数据即可;如果主机要读取从机数据,需要主机发送一个空数据来引发从机发送数据。
image.png

传输模式

SPI有四种传输模式,主要差别在于CPOL和CPHA的不同。

CPOL( Clock Polarity,时钟极性),表示SCK在空闲时为高电平还是低电平。 当CPOL=0,SCK空闲时为低电平,当CPOL=1, SCK空闲时为高电平。

CPHA( Clock Phase,时钟相位), 表示SCK在第几个时钟边缘采样数据。 当CPHA=0,在SCK第一个边沿采样数据(即在第二个边沿输出),当CPHA=1, 在SCK第二个边沿采样数据(即在第一个边沿输出数据)。

比如:CPHA=0时, 表示在时钟第一个时钟边沿采样数据。 当CPOL=1,即空闲时为高电平,从高电平变为低电平,第一个时钟边沿(下降沿) 即进行采样。 当CPOL=0,即空闲时为低电平,从低电平变为高电平,第一个时钟边沿(上升沿)即进行采样。
image.png

数据传输流程

首先主机和从机都选择同一传输模式。然后主机片选拉低,选中从机。接着在时钟的驱动下, MOSI发送数据,同时MISO读取接收数据。最后完成传输,取消片选。

模拟SPI

 /*
* 函数名: void SPI_WriteByte(uint8_t data)
* 输入参数: data -> 要写的数据
* 输出参数:无  
* 返回值:无
* 函数作用:模拟 SPI 写一个字节
*/ 
void SPI_WriteByte(uint8_t data) {        //SPI写1 Byte,循环8次,每次发送1 Bit;
    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 读一个字节
*/    
uint8_t SPI_ReadByte(void) {         //SPI读1 Byte,循环8次,每次接收1 Bit;
    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;
}

十二楼主
1 声望0 粉丝

下一篇 »
看门狗