打印
[STM32F0]

【经验分享】STM32F0+SPI通讯

[复制链接]
484|4
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
SPI ,全称:Serial Peripheral Interface,即串行外围设备接口。是一种高速的,全双工,同步的通信总线,并且在芯片的管脚上只占用四根线,节约了芯片的管脚,同时为PCB的布局上节省空间,提供方便,主要应用在 EEPROM,FLASH,实时时钟,AD转换器,还有数字信号处理器和数字信号解码器之间。

SPI通讯设备之间的常用连接方式如下图:



SPI通讯使用3条总线和一个片选线,3条总线分别为SCK、MOSI、MISO,片选线为SS,它们的作用介绍如下:

SS(Slave Select):从设备选择信号线,常称为片选信号线,也称为NSS、CS。当有多个SPI从设备与SPI主机相连时,设备的其它信号线SCK、MOSI及MISO同时并联到相同的SPI总线上,即无论有多少个从设备,都共同只使用这3条总线;而每个从设备都有独立的这一条NSS信号线,本信号线独占主机的一个引脚,即有多少个从设备,就有多少条片选信号线。I2C协议中通过设备地址来寻址、选中总线上的某个设备并与其进行通讯;而SPI协议中没有设备地址,它使用NSS信号线来寻址,当主机要选择从设备时,把该从设备的NSS信号线设置为低电平,该从设备即被选中,即片选有效,接着主机开始与被选中的从设备进行SPI通讯。所以SPI通讯以NSS线置低电平为开始信号,以NSS线被拉高作为结束信号。
SCK(Serial Clock):时钟信号线,用于通讯数据同步。它由通讯主机产生,决定了通讯的速率,不同的设备支持的最高时钟频率不一样,如STM32的SPI时钟频率最大为fpclk/2,两个设备之间通讯时,通讯速率受限于低速设备。
MOSI(Master Output, Slave Input):主设备输出/从设备输入引脚。主机的数据从这条信号线输出,从机由这条信号线读入主机发送的数据,即在这条线上数据的方向为主机到从机。
MISO(Master Input,,Slave Output):主设备输入/从设备输出引脚。主机从这条信号线读入数据,从机的数据由这条信号线输出到主机,即在这条线上数据的方向为从机到主机。
SPI基本通讯过程:



这是一个主机的通讯时序。NSS、SCK、MOSI信号都由主机控制产生,而 MISO 的信号由从机产生,主机通过该信号线读取从机的数据。MOSI与 MISO的信号只在 NSS 为低电平的时候才有效,在 SCK的每个时钟周期 MOSI和 MISO传输一位数据。各信号分解如下:

通讯的起始和停止信号
在上图中的①标号处,NSS 信号线由高变低,是 SPI通讯的起始信号。NSS是每个从机各自独占的信号线,当从机在自己的 NSS 线检测到起始信号后,就知道自己被主机选中了,开始准备与主机通讯。在图中的⑥标号处,NSS信号由低变高,是 SPI通讯的停止信号,表示本次通讯结束,从机的选中状态被取消。

数据有效性
SPI使用 MOSI及 MISO信号线来传输数据,使用 SCK信号线进行数据同步。MOSI及 MISO数据线在 SCK的每个时钟周期传输一位数据,且数据输入输出是同时进行的。数据传输时,MSB先行或 LSB先行并没有作硬性规定,但要保证两个 SPI通讯设备之间使用同样的协定。

观察图中的②③④⑤标号处,MOSI及 MISO的数据在 SCK的上升沿期间变化输出,在 SCK的下降沿时被采样。即在 SCK的下降沿时刻,MOSI及 MISO的数据有效,高电平时表示数据“1”,为低电平时表示数据“0”。在其它时刻,数据无效,MOSI及 MISO为下一次表示数据做准备。

SPI每次数据传输可以 8 位或 16 位为单位,每次传输的单位数不受限制。

时钟信号的相位和极性:
SPIx_CR1寄存器的CPOL和CPHA位能够组合成四种可能的时序关系。CPOL:时钟极性选择,为0时SPI总线空闲为低电平,为1时SPI总线空闲为高电平。CPHA:时钟相位选择,为0时在SCK第一个跳变沿采样,为1时在SCK第二个跳变沿采样。根据CPOL和CPHA的不同组合可以分为四种工作方式,工作方式如下:

[size=0.83em]
20190817081810498.png (115.15 KB, 下载次数: 3)
下载附件
[color=rgb(153, 153, 153) !important]2021-11-19 23:23 上传





当CPHA=0、CPOL=1时,MISO引脚上的数据在第一个时钟沿跳变之前已经上线了,而为了保证正确传输,MOSI引脚的最高位必须与时钟的第一个边沿同步,在SPI传输过程中,首先将数据上线,然后在同步时钟信号的下降沿时,SPI的接收方捕捉位信号,在时钟信号的一个周期结束时(上升沿),下一位数据信号上线,再重复上述过程,直到一个字节的8位信号传输结束。当CPHA=0、CPOL=0时,与前者唯一不同之处只是在同步时钟信号的上升沿时捕捉位信号,下降沿时下一位数据上线。



当CPHA=1、CPOL=1时,MISO引脚和MOSI引脚上的数据的最高位必须与时钟的第一个边沿同步,在SPI传输过程中,在同步时钟信号周期开始时(下降沿)数据上线,然后在同步时钟信号的上升沿时,SPI的接收方捕捉位信号,在时钟信号的一个周期结束时(下降沿),下一位数据信号上线,再重复上述过程,直到一个字节的8位信号传输结束。当CPHA=1、CPOL=0时,与前者唯一不同之处只是在同步时钟信号的下降沿时捕捉位信号,上升沿时下一位数据上线。

使用特权

评论回复
沙发
两只袜子|  楼主 | 2023-2-13 10:55 | 只看该作者
SPI内部框图如下所示:



①通讯引脚

SPI的所有硬件架构都是从上图中①部分MOSI、MISO、SCK以及NSS展开的。STM32芯片有多个SPI外设,它们的SPI通讯信号引出到不同的GPIO引脚,使用时必须配置到这些指定的引脚。

②数据控制逻辑

SPI的 MOSI及 MISO 都连接到数据移位寄存器上,数据移位寄存器的内容来源于接收缓冲区及发送缓冲区以及 MISO、MOSI线。当向外发送数据的时候,数据移位寄存器以“发送缓冲区”为数据源,把数据一位一位地通过数据线发送出去;当从外部接收数据的时候,数据移位寄存器把数据线采样到的数据一位一位地存储到“接收缓冲区”中。通过写 SPI的“数据寄存器 DR”把数据填充到发送缓冲区中,通过 “数据寄存器 DR”,可以获取接收缓冲区中的内容。

③时钟控制逻辑

SCK线的时钟信号,由波特率发生器根据“控制寄存器 CR1”中的 BR[0:2]位控制,该位是对 fpclk 时钟的分频因子,对 fpclk 的分频结果就是 SCK引脚的输出时钟频率。

④整体控制逻辑

整体控制逻辑负责协调整个SPI外设,控制逻辑的工作模式根据我们配置的“控制寄存器(CR1/CR2)”的参数改变,基本的控制参数包括只接受模式、CRC使能、CRC校验位长度、时钟极性、时钟相位等等。除此之外,控制逻辑还根据要求负责控制NSS信号线。实际应用中,我们一般不使用STM32 SPI外设的标准NSS信号线,而是更简单地使用普通的GPIO,软件控制它的电平输出,从而产生通讯起始和停止信号。

SPI通讯模式:
1、全双工通信:

默认情况下,SPI配置为全双工通信,在这种配置下,主寄存器和从寄存器的移位寄存器使用MOSI和MISO引脚之间的两条单项线连接。在SPI通信过程中,数据在主时钟提供的SCK时钟边缘上同步移动。主机通过MOSI线将要发送的数据传输给从机,并通过MISO线从从机接收数据。当数据帧传输完成(所有位都被移动),主从之间的信息传输完成。

2、半双工通信:

在这种配置中,使用一条交叉连接线将主寄存器和从寄存器的移位寄存器连接在一起。在此通信过程中,数据在SCK时钟边缘上的移位寄存器之间同步移动,移动方向由主从双方互选。可在SPIx_CR1寄存器中配置。在此配置中,主机的MISO引脚或从机的MOSI引脚可以被其它应用当作GPIO使用。

3、单工通信:

SPI可以通过将SPI设置为只发送模式或只接收模式,以单工模式进行通信。在这种配置中,只有一根线用于主机和从机之间的传输。剩下不使用的MISO或MOSI引脚,可以作为GPIO使用。

NSS片选信号介绍:
在从机模式下,NSS作为标准芯片选择输入,并允许从机与主机通信。在主模式下,NSS可以用作输入或者输出。作为输入,它可以防止多机总线冲突,作为输出,它可以驱动一个从机的选择信号。

输出模式:SPI的输出模式可在SPIx_CR2寄存器的SSOE位进行配置。当SSOE为1时,并且SPI处于主模式下控制时,NSS输出低电平。因此当其他SPI设备的NSS引脚与它相连,必然接收到低电平,则片选成功。
输入模式:
1)软件输入:通过配置SPIx_CR1寄存器的SSM位来使能软件模式。NSS分为内部管脚和外部管脚,当NSS配置为软件输入时,NSS的外部引脚可以另作他用(例如:GPIO驱动外部设备CS输出低电平),NSS的内部引脚高低电平可以通过SPIx_CR1寄存器的SSI位来配置。STM32规定要将设备保持主机模式,NSS内部引脚必须输出高电平(SSI=1)。如果STM32作为从机使用,NSS内部引脚必须为0(SSI=0)。

2)硬件输入:对于主机,我们的NSS可以直接接到高电平,对于从机,NSS接低就可以了。

SPI通讯过程:
STM32使用SPI外设通讯时,在通讯的不同阶段它会对“状态寄存器SR”的不同数据位写入参数,我们通过读取这些寄存器标志来了解通讯状态。主机全双工通信如下图所示:



控制NSS信号线,产生起始信号。在传输开始之前,必须为从设备准备足够的时间去准备数据。
把要发送的数据写入到“数据寄存器DR”中,该数据会存储到发送缓冲区。
通讯开始,SCK时钟开始运行。MOSI把发送缓冲区中的数据一位一位地传输出去;MISO则把数据一位一位地存储进接收缓冲区中。
在主控系统中,如果通信(时钟信号)是连续的,BSY在帧与帧之间保持高电平状态。
当发送完一帧数据的时候,“状态寄存器SR”中的“TXE标志位”会被置1,表示传输完一帧,发送缓冲区已空;类似地,当接收完一帧数据的时候,“RXNE”标志位会被置1,表示传输完一帧,接收缓冲区非空。
等待“TXE标志位”为1时,若还要继续发送数据,则再次往“数据寄存器DR”写入数据即可;等待“RXNE标志位”为1时,通过读取“数据寄存器DR”可以获取接收缓冲区中的内容。
假如我们使能了DMA或中断时,TXE或RXNE置1时会产生SPI中断信号,进入同一个中断服务函数,到SPI中断服务程序后,可通过检查寄存器位来了解是哪一个事件,再分别进行处理。也可以使用DMA方式来收发“数据寄存器DR”中的数据。

使用特权

评论回复
板凳
两只袜子|  楼主 | 2023-2-13 10:55 | 只看该作者
bsp_spi.c程序如下:
#include "bsp_spi.h"

long spi2_lost;

  • // 初始化SPI对应IO引脚
  • void SPI_GPIO_Init(void)
  • {
  •     GPIO_InitTypeDef  GPIO_InitStruct;
  •     SPI_InitTypeDef SPI_InitStruct;
  •     RCC_AHBPeriphClockCmd(SPI_RCC, ENABLE);
  •     RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2, ENABLE);
  •     // 初始化GPIO引脚
  •     GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF;
  •     GPIO_InitStruct.GPIO_OType =  GPIO_OType_PP;
  •     GPIO_InitStruct.GPIO_Pin =  SPI_CLK_PIN | SPI_MOSI_PIN | SPI_MISO_PIN;
  •     GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_UP;
  •     GPIO_InitStruct.GPIO_Speed = GPIO_Speed_Level_3;
  •     GPIO_Init(SPI_GPIOx, &GPIO_InitStruct);
  •     GPIO_InitStruct.GPIO_Pin = SPI_NSS_PIN;
  •     GPIO_InitStruct.GPIO_Mode = GPIO_Mode_OUT;
  •     GPIO_Init(SPI_GPIOx, &GPIO_InitStruct);
  •     // 配置GPIO复用
  •     GPIO_PinAFConfig(SPI_GPIOx, GPIO_PinSource13, GPIO_AF_0); // PB13:CLK
  •     GPIO_PinAFConfig(SPI_GPIOx, GPIO_PinSource14, GPIO_AF_0); // PB14:MISO
  •     GPIO_PinAFConfig(SPI_GPIOx, GPIO_PinSource15, GPIO_AF_0); // PB15:MOSI
  •     SPI_Cmd(SPI2, DISABLE); // 失能SPI
  •     SPI_InitStruct.SPI_Direction = SPI_Direction_2Lines_FullDuplex; // 选择SPI单向或双向数据模式
  •     SPI_InitStruct.SPI_Mode = SPI_Mode_Master; // 选择SPI主机/从机模式
  •     SPI_InitStruct.SPI_DataSize = SPI_DataSize_8b; // 选择SPI数据宽度
  •     SPI_InitStruct.SPI_CPOL = SPI_CPOL_Low; // 选择时钟极性
  •     SPI_InitStruct.SPI_CPHA = SPI_CPHA_1Edge; // 选择时钟相位
  •     SPI_InitStruct.SPI_NSS = SPI_NSS_Soft; // 选择NSS信号管理方式
  •     SPI_InitStruct.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_256;        // 波特率选择
  •     SPI_InitStruct.SPI_FirstBit = SPI_FirstBit_MSB;        // 选择数据传输开始方向
  •     SPI_InitStruct.SPI_CRCPolynomial = 7;        // CRC计算多项式
  •     SPI_Init(SPI2, &SPI_InitStruct);
  •     SPI_RxFIFOThresholdConfig(SPI2, SPI_RxFIFOThreshold_QF); // 配置FIFO阈值
  •     SPI_Cmd(SPI2, ENABLE); // 使能SPI
  • }
  • // SPI2读写一个字节
  • uint8_t SPI2_ReadWriteByte(uint8_t Data)
  • {
  •     uint8_t retry=0;
  •     while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_TXE) == RESET) { if(retry++>200) { spi2_lost++; return 0xFF;} } // 发送缓存标志位为空
  •     SPI_SendData8(SPI2, Data); // 通过外设SPI2发送一个数据
  •     retry=0;
  •     while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_RXNE) == RESET) { if(retry++>200) { spi2_lost++; return 0xFF;} } //接收缓存标志位不为空
  •     return SPI_ReceiveData8(SPI2); // 通过SPI2返回接收数据
  • }

复制代码


bsp_spi.h程序如下:

  • #ifndef _BSP_SPI_H_
  • #define _BSP_SPI_H_
  • #include "stm32f0xx.h"
  • #define        SPI_RCC                                        RCC_AHBPeriph_GPIOB
  • #define SPI_GPIOx                                GPIOB
  • #define SPI_NSS_PIN                        GPIO_Pin_12
  • #define SPI_CLK_PIN                        GPIO_Pin_13
  • #define SPI_MISO_PIN                GPIO_Pin_14
  • #define SPI_MOSI_PIN                GPIO_Pin_15
  • #define SPI2_CS_ENABLE         GPIO_ResetBits(SPI_GPIOx, SPI_NSS_PIN)
  • #define SPI2_CS_DISABLE GPIO_SetBits(SPI_GPIOx, SPI_NSS_PIN)
  • void SPI_GPIO_Init(void);
  • uint8_t SPI2_ReadWriteByte(uint8_t Data);
  • #endif


复制代码



使用特权

评论回复
地板
chenqianqian| | 2023-2-13 12:02 | 只看该作者
本帖最后由 chenqianqian 于 2023-2-13 12:05 编辑

SPI从单线模式发展到四线模式,速度越來越快了

使用特权

评论回复
5
chenqianqian| | 2023-2-13 12:02 | 只看该作者
本帖最后由 chenqianqian 于 2023-2-13 12:04 编辑

SPI算是应用最广泛的串行通信接口了吧

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

1936

主题

6725

帖子

8

粉丝