STM32之SPI详细解析

STM32之SPI详细解析,第1张

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 = 0SCK 在空闲状态时为低电平,数据线上的信号在 SCK 时钟线的奇数边沿被采样
CPOL = 1;CPHA = 0SCK 在空闲状态时为高电平,数据线上的信号在 SCK 时钟线的奇数边沿被采样
CPOL = 0;CPHA = 1SCK 在空闲状态时为低电平,数据线上的信号在 SCK 时钟线的偶数边沿被采样
CPOL = 1;CPHA = 1SCK 在空闲状态时为高电平,数据线上的信号在 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 频率)
000fpclk/2
001fpclk/4
010fpclk/8
011fpclk/16
100fpclk/32
101fpclk/64
110fpclk/128
111fpclk/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的。


欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/564535.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-06
下一篇 2022-04-06

发表评论

登录后才能评论

评论列表(0条)

保存