SPI介绍

SPI协议,用来传输数据的一种标准化协议。

SPI包括这些独特的特点:

  • 主模式和从模式

  • 双向模式

  • 从模式选择输出

  • 模式故障错误标志与CPU中断能力

  • 双缓冲数据寄存器

  • 具有可编程极性和相位的串行时钟

  • 在等待模式下对SPI操作的控制

  • 引脚描述:

    ​ MOSI:此引脚用于在配置为主主模块时从SPI模块中传输数据,并在配置为从主模块时接收数据。(主出从入)

    ​ MISO:在配置为SPI模块时从SPI模块中传输数据,在配置为主模块时接收数据。(主入从出)

    ​ SS:(低有效)用于将选择信号从SPI模块输出到另一个外设,当其配置为主控时进行数据传输,当SPI配置为从控时作为输入来接收从选择信号。
    ​ 该引脚相当于片选。

    ​ SCK:此引脚用于输出SPI传输数据或接收从属时钟的时钟。

    时序分析

    首先要了解两个概念:CPHA(Clock Phase,时钟相位)和CPOL(Clock Polarity,时钟极性)

    1. 时钟极性:是指 SPI 通讯设备处于空闲状态时,SCK 信号线的电平信号(即 SPI 通讯开始前、 NSS 线为高电平时 SCK 的状态)。CPOL=0 时, SCK 在空闲状态时为低电平,CPOL=1 时,则相反。

    2. 时钟相位:是指数据的采样的时刻,当 CPHA=0 时,MOSI 或 MISO 数据线上的信号将会在 SCK 时钟线的”奇数边沿”被采样。当 CPHA=1 时,数据线在 SCK 的“偶数边沿”采样。

    因此SPI关于时钟的配置,就有了以下4种情况:

    模式 CLK
    CPOL = 0;CPHA = 0 SCK 在空闲状态时为低电平,数据线上的信号在 SCK 时钟线的奇数边沿被采样
    CPOL = 1;CPHA = 0 SCK 在空闲状态时为高电平,数据线上的信号在 SCK 时钟线的奇数边沿被采样
    CPOL = 0;CPHA = 1 SCK 在空闲状态时为低电平,数据线上的信号在 SCK 时钟线的偶数边沿被采样
    CPOL = 1;CPHA = 1 SCK 在空闲状态时为高电平,数据线上的信号在 SCK 时钟线的偶数边沿被采样

    时序1:CPHA = 0

    从时序图可以看出,当 CPHA = 0 的时候,无论CPOL等于多少,在SAMPLE那一栏即采样项(橙色方框处),都是在奇数边沿进行采样,即采样边沿仅受CPHA的影响。并且采样开始前要先拉低SS,即进行片选。

    时序2:CPHA = 1

    从时序图可以看出,当 CPHA = 1 的时候,无论CPOL等于多少,在SAMPLE那一栏即采样项(橙色方框处),都是在偶数边沿进行采样,即采样边沿仅受CPHA的影响。并且采样开始前要先拉低SS,即进行片选。

    综上所述,可以发现SPI的协议自由度是比IIC要高一些的,给了开发者更多的自由搭配的空间。

    比如在写OLED的spi的时候,可能就不用加上MISO引脚,但是要额外搭配DC(Data/Command)引脚,区别发送的是数据还是命令。

    接下来我们分别看看硬件SPI和软件模拟SPI

    STM32的硬件SPI

    接下来我们看看STM32中的硬件SPI,这里以STM32F103RCT6为例。

    功能框图

    1. MOSI、MISO、SCK、NSS与前文说过的一样,四根引脚。
    2. 波特率发生器:由框图可以看出,波特率发生器链接的是SCK,那么可想而知,这是用来产生时钟信号的,既然用来产生时钟信号,那么肯定和STM32的时钟有关,并且也能够进行分频之类的操作。并且框图也指出,寄存器SPI_CR1的BR[2,0] 位指向波特率发生器,由数据手册得知,该位是对 fpclk时钟的分频因子,对 fpclk的分频结果就是 SCK 引脚的输出时钟频率。其中的 fpclk频率是指 SPI 所在的 APB 总线频率。
      计算结果如下:
    BR[0:2] 分频结果(SCK 频率)
    000 fpclk/2
    001 fpclk/4
    010 fpclk/8
    011 fpclk/16
    100 fpclk/32
    101 fpclk/64
    110 fpclk/128
    111 fpclk/256
    1. 数据控制单元:该部分包含接收缓冲区、发送缓冲区、数据移位寄存器。
      发送数据的时候,数据移位寄存器将发送缓冲区内的数据一位一位发出去;接收数据的时候,数据移位寄存器则把把接收缓冲区内的数据一位一位读进来。并且每个数据帧长度可以通过“控制寄存器 CR1”的“DFF 位”配置成 8 位及 16 位模式。配置“LSBFIRST 位”可选择高位先行(MSB)还是低位先行(LSB)。

      对于数据寄存器(DR),通过写 SPI的数据寄存器可以把数据填入发送缓冲区,通过读SPI的数据寄存器可以获取接收缓冲区中的内容。

    2. 剩下的部分则是整体配置SPI相关的控制部分。SPI的运行模式,则随着我们这部分的配置的不同而不同。除了基本的SPI相关参数的配置,还包括SPI的中断信号、DMA请求、NSS信号线配置等。

    从选择(NSS)脚管理,即SS引脚,进行片选

    有2种NSS模式:

    ● 软件NSS模式:可以通过设置SPI_CR1寄存器的SSM位来使能这种模式。内部NSS信号电平可以通过写SPI_CR1的SSI位来驱动,就可以将NSS引脚用作别的功能。

    ● 硬件NSS模式,分两种情况:

    ─ NSS输出被使能:当STM32作为主机,并且NSS输出已经通过SPI_CR2寄存器的SSOE位使能,这时NSS引脚被拉低,所有NSS引脚与这个主SPI的NSS引脚相连并配置为硬件NSS的SPI设备,将自动变成从机。当一个SPI设备需要发送广播数据,它必须拉低NSS信号,以通知所有其它设备它是主机;如果它不能拉低NSS,这意味着总线上有另外一个主设备在通信,这时将产生一个硬件失败错误(Hard Fault)。

    ─ NSS输出被关闭:允许操作于多主机环境。

    通讯过程

    (来自野火的《零死角玩转STM32》)

    主模式收发流程及事件说明如下:

    (1) 控制 NSS 信号线,产生起始信号(图中没有画出),即先将NSS拉低;

    (2) 把要发送的数据写入到“数据寄存器 DR”中,该数据会被存储到发送缓冲区;

    (3) 通讯开始,SCK 时钟开始运行。MOSI 把发送缓冲区中的数据一位一位地传输出去;MISO 则把数据一位一位地存储进接收缓冲区中;

    (4) 当发送完一帧数据的时候,“状态寄存器 SR”中的“TXE 标志位”会被置 1,表示传输完一帧,发送缓冲区已空;类似地,当接收完一帧数据的时候,“RXNE标志位”会被置 1,表示传输完一帧,接收缓冲区非空;这里的两个标志位要由软件清零。

    (5) 等待到 “TXE 标志位” 为 1 时,若还要继续发送数据,则再次往 “数据寄存器DR” 写入数据即可;等待到 “RXNE标志位” 为 1 时,通过读取“数据寄存器DR”可以获取接收缓冲区中的内容。假如我们使能了 TXE 或 RXNE 中断,TXE 或 RXNE 置 1 时会产生 SPI 中断信号,进入同一个中断服务函数,到 SPI 中断服务程序后,可通过检查寄存器位来了解是哪一个事件,再分别进行处理。也可以使用 DMA 方式来收发“数据寄存器 DR”中的数据。

    初始化结构体

    typedef struct
    {
        uint16_t SPI_Direction; 			/*设置 SPI 的单双向模式 */
        uint16_t SPI_Mode; 					/*设置 SPI 的主/从机端模式 */
        uint16_t SPI_DataSize; 				/*设置 SPI 的数据帧长度,可选 8/16 位 */
        uint16_t SPI_CPOL; 					/*设置时钟极性 CPOL,可选高/低电平*/
        uint16_t SPI_CPHA; 					/*设置时钟相位,可选奇/偶数边沿采样 */
        uint16_t SPI_NSS; 					/*设置 NSS 引脚由 SPI 硬件控制还是软件控制*/
        uint16_t SPI_BaudRatePrescaler; 	/*设置时钟分频因子,fpclk/分频数=fSCK */
        uint16_t SPI_FirstBit; 				/*设置 MSB/LSB 先行 */
        uint16_t SPI_CRCPolynomial; 		/*设置 CRC 校验的表达式 */
    } SPI_InitTypeDef;
    

    在与其他SPI从机搭配使用时,有时还要参考从机的数据手册;这类从机一般分为两类:

    1. 仅收发数据,比如FLASH芯片中的W25Q64;
    2. 收发数据和指令,这类从机额外使用DC引脚来区别SPI上发送的数据是单纯的数据还是指令,比如OLED等。

    软件SPI

    软件SPI就是用普通IO口模拟SPI的时序和通讯方法。这里我用SPI通讯方式的LCD搭配STM32CubeMX来做介绍

    GPIO配置

    其中DC用来区分写数据还是写指令

    BLK调节LCD背光

    CS即片选

    SCK即时钟

    SDA即MOSI信号线

    需要注意的是,这里的SCK配置的是上拉,即使SCK时钟在空闲时为高电平,即CPOL = 1

    void MX_GPIO_Init(void)
    {
      GPIO_InitTypeDef GPIO_InitStruct = {0};
    
      /* GPIO Ports Clock Enable */
      __HAL_RCC_GPIOC_CLK_ENABLE();
      __HAL_RCC_GPIOH_CLK_ENABLE();
      __HAL_RCC_GPIOA_CLK_ENABLE();
      __HAL_RCC_GPIOB_CLK_ENABLE();
    
      /*Configure GPIO pin Output Level */
      HAL_GPIO_WritePin(BLK_GPIO_Port, BLK_Pin, GPIO_PIN_SET);
    
      /*Configure GPIO pin Output Level */
      HAL_GPIO_WritePin(GPIOA, DC_Pin|CS_Pin|SPI_SCK_Pin|SPI_SDA_Pin
                              |RES_Pin, GPIO_PIN_SET);
    
      /*Configure GPIO pin : PtPin */
      GPIO_InitStruct.Pin = BLK_Pin;
      GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
      GPIO_InitStruct.Pull = GPIO_NOPULL;
      GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
      HAL_GPIO_Init(BLK_GPIO_Port, &GPIO_InitStruct);
    
      /*Configure GPIO pins : PAPin PAPin PAPin PAPin
                               PAPin */
      GPIO_InitStruct.Pin = DC_Pin|CS_Pin|SPI_SCK_Pin|SPI_SDA_Pin
                              |RES_Pin;
      GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
      GPIO_InitStruct.Pull = GPIO_PULLUP;
      GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
      HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
    }
    

    模拟时序

    很典型的一个写8bit数据的函数,入口数据dat,首先选中LCD,进入循环,对dat拆出高位bit分析是1还是0,再决定MOSI输出的是1还是0,并且这个过程是在一个时钟脉冲内完成的。一次循环结束后,dat左移一位,将低一位的bit推向高位,为下一次循环做好准备。在8次循环过后,就完成了一个8bit数据的发送。其实LCD的使用中,SPI相关的只有以下函数,其他的都是在此之上进行的扩展。(没有明显区分采样时刻是奇数还是偶数边沿)

    void LCD_Writ_Bus(u8 dat) 
    {	
    	u8 i;
    	LCD_CS_Clr();
    	for(i=0;i<8;i++)
    	{			  
    		LCD_SCLK_Clr();
    		if(dat&0x80)
    		{
    		   LCD_MOSI_1();
    		}
    		else
    		{
    		   LCD_MOSI_0();
    		}
    		LCD_SCLK_Set();
    		dat<<=1;
    	}	
      LCD_CS_Set();	
    }
    

    当然读取一个数据也可由上面推导出来,但是,LCD上并没有MISO引脚,所以各位看看就好。用不上。

    uint8_t LCD_Read_Bus(void)
    {
        uint8_t i;
        uint8_t value = 0;
        
    	LCD_CS_Clr();
    	for(i=0;i<8;i++)
    	{			  
    		LCD_SCLK_Clr();
            value <<= 1;
            if(LCD_MISO_READ() == 1)
            {
                value = value + 1;
            }
    		LCD_SCLK_Set();
    	}	
        LCD_CS_Set();	
        return value;
    }
    

    比如写一个16位的数据

    void LCD_WR_DATA(u16 dat)
    {
    	LCD_Writ_Bus(dat>>8);
    	LCD_Writ_Bus(dat);
    }
    

    比如LCD写命令

    void LCD_WR_REG(u8 dat)
    {
    	LCD_DC_Clr();//写命令
    	LCD_Writ_Bus(dat);
    	LCD_DC_Set();//写数据
    }
    //因为大多数情况下是写数据,所以这里配置完写命令后要及时切换回写数据
    

    总结

    单单从SPI的基本协议来看,SPI可能比IIC更简单一些,只是分出了四种模式,但是单纯的SPI根据不同的从机要对协议进行不一样的扩展,这就提升了编程的难度,但是也加大了协议本身的自由度。速率方面的话,同一个芯片的硬件SPI的速率是绝对远超软件SPI的。

    物联沃分享整理
    物联沃-IOTWORD物联网 » STM32 SPI通信深入剖析

    发表评论