STM32--SPI原理及应用

1.什么是SPI

SPI,Serial Peripheral interface,串行外围设备接口。是Motorola(摩托罗拉)首先在其MC68HCXX系列处理器上定义的。

2.SPI基本特性

SPI,是一种高速全双工的通信总线。广泛地应用在ADC、LCD等设备与MCU间,适用于对通信速率要求较高的场合。

支持的最高SCL的时钟频率为fpclk/2,如:STM32F103的fpclk/2=72/2=36MHZ

支持四种数据采样模式

数据帧长度可以设置8位与16位,每次传输的单位数不受限制

可设置数据高位先行(MSB)低位先行(LSB)

没有从机地址

没有应答设计

支持同步全双工通信、双线单向通信、单线模式

3.SPI物理层

3.1SPI的线路连接

3.1.1一对一线路

3.1.2一对多线路

SPI通讯使用3条总线及片选线,3条总线分别为SCK、MOSI、MISO ,片选线为SS

**SCK(Serial Clock):**串行时钟线

用于通讯数据同步。它由通讯主机产生,决定了通讯的速率,不同的设备支持的最高时钟频率不一样, 如STM32的SPI时钟频率最大为fpclk/2,两个设备之间通讯时,通讯速率受限于低速设备。

MOSI(Master Output Slave Input):主机输出、从机输入

主机 的数据从这条信号线输出从机 由这条信号线读入主机发送的数据,即这条线上数据的方向为主机到从机。

MISO(Master Input Slave Output):主机输入、从机输出

主机 从这条信号线读入 数据, 从机 的数据由这条信号线输出到主机,即在这条线上数据的方向为从机到主机。

SS(Slave Select)片选信号线

SS常称为片选信号线 ,也称为NSS、CS,以下用NSS表示。当有多个SPI从设备与SPI主机相连时, 设备的其它信号线SCK、MOSI及MISO同时并联到相同的SPI总线上,即无论有多少个从设备,都共同只使用这3条总线而每个从设备都有独立的这一条NSS信号线,本信号线独占主机的一个引脚,即有多少个从设备,就有多少条片选信号线 。 I2C协议中通过设备地址来寻址、选中总线上的某个设备并与其进行通讯;而SPI协议中没有设备地址,它使用NSS信号线来寻址, 当主机要选择从设备时,把该从设备的NSS信号线设置为低电平,该从设备即被选中,即片选有效, 接着主机开始与被选中的从设备进行SPI通讯。所以SPI通讯以NSS线置低电平为开始信号,以NSS线被拉高作为结束信号。

在一主多从的模式下,GPIO模拟SS是最佳选择

3.2 SPI外设框图

线路1是SPI主机发送数据的流程:发送的数据→发送缓冲区(TDR)→移位寄存器→MOSI

线路2是SPI主机接收数据的流程:接收的数据→MISO→移位寄存器→接收缓冲区(RDR)

1.LSBFIRST位:控制是低位先行还是高位先行。0:MSB高位先行 1:LSB低位先行

2.**SPE(SPI Enable)**SPI使能,就是SPI_Cmd函数配置的位

3.**BR(Baud Rate)**配置波特率,就是SCK的时钟频率

4.MSTR:配置主从模式(1:主,2:从)

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

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

7.TXE/RXNE:(TXE、RXNE)分别是发送寄存器空、接收寄存器非空,这两个事件均可申请进入USART中断

发送寄存器空:表明发送寄存器空闲,可通知MCU向TDR写入数据;

接收寄存器非空:表明已经接收到了数据,进入中断后可通知MCU读取RDR里的数据

8.TXEIE、RXNEIE分别是发送中断使能、接收中断使能

9.波特率发生器:主要用来产生SCL时钟的,内部主要是一个分频器,输入时钟是PCLK,72Mhz或36Mhz,经过分频之后,输出到SCK引脚(生成的时钟与移位寄存器一致,每产生一个时钟,移入、移出1bit)。CR1寄存器中的三个位BR0、1、2用来控制分频系数

数据控制器:相当于一个管理员,控制着所有电路的运行

移位寄存器:右边的数据低位,一位一位的从MOSI移出去,MISO的数据一位一位的移入到左边的数据高位。图上表示的是低位先行。

SPI从模式下的数据收发:

这一块主要做主从模式引脚变换的,SPI外设可以做主机也可做从机,做主机时这个交叉就不用,如果该外设做从机MOSI 就应该作为输入。

注:

接收、发送缓冲区分别是接收、发送数据寄存器RDR、TDR,这里和串口一样TDR与RDR占用同一个地址,统一叫DR

发送流程:发送数据先写入TDR,在转到移位寄存器发送,发送的同时接收数据,接收到的数据转到RDR,再从RDR读取数据,数据寄存器与移位寄存器配合,可以实现无延迟的连续传输。

与IIC的区别,SPI是全双工,发送与接收时同步进行的,所以SPI的数据寄存器发送与接收是分离的,而移位寄存器,发送与接收是可以共用的。因为IIC 是板双工,所以,它的数据寄存器与移位寄存器的发送与接收都是共用的串口数据寄存器与移位寄存器的发送与接收都是分离的。

3.3 SPI电路如何工作

SPI的数据收发都是基于字节交换,这个基本单元来进行的

主机中波特率发生器提供时钟源,其产生的时钟,会驱动主机与从机的移位寄存器进行移位。因此每来一个时钟,移位寄存器都会向左/右进行移位。

SPI交换数据的步骤:

**1、**波特率发生器时钟的上升沿,所有寄存器向左移动一位,移出去的位放到引脚上:

**2、**波特率发生器时钟的下降沿,引脚上的位,采样输入到移位寄存器的最低位。数据放在通信线上实际上是放到了输出寄存器上。移位采样移位采样.........

**3、**最终结果,主机里的10101010数据转运到从机里,从机里的01010101数据转运到了主机里,这就实现了主机与从机的一个字节的数据交换,SPI的运行过程就是这样。SPI的数据收发都是基于字节交换进行的。

主从机如何发送接收数据?

1.当主机需要发送一个字节,同时接收一个字节时,就可以执行这个字节交换

2.如果指向发送数据,不想接收数据,仍然调用交换字节 的时序,发送同时接收**,只是接收到的数据,我们不做处理**

3.如果只想接收不想发送,还是调用交换字节 的时序,发送同时接收,我们随便发送一个数据(00或ff)把从机的数据置换过来

总结:SPI数据的通信实际上就是:交换字节

4.SPI协议层

4.1SPI时基单元

4.2SPI通信时序

4.2.1 SPI的起始信号与终止信号

1.SPI起始信号NSS线置低电平

NSS是每个从机各自独占的信号线, 当从机在自己的NSS线检测到起始信号后,就知道自己被主机选中了,开始准备与主机通讯。

6.SPI终止信号NSS线置高电平

NSS信号由低变高, 是SPI通讯的停止信号,表示本次通讯结束,从机的选中状态被取消。

4.2.2 数据有效性

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

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

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

4.2.3CPOL/CPHA及通讯模式

SPI一共有四种通讯模式, 它们的主要区别是总线空闲时SCK的时钟状态以及数据采样时刻。为方便说明,在此引入"时钟极性CPOL"和"时钟相位CPHA"的概念。

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

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

1.CPHA=0的情况

无论CPOL=0或1,数据都会在SCL奇数边沿采样,在SCL偶数边沿更改数据 2.CPHA=0的情况

无论CPOL=0或1,数据都会在SCL奇数边沿更改数据,在SCL偶数边沿采样

总结:如果在SCL上升沿更改数据,那么就在SCL的下降沿读取数据

SPI四种数据采样模式总结

6. SPI代码

6.1 SPI结构体详解

cpp 复制代码
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;

1.SPI_Direction

本成员设置SPI的通讯方向,可设置为双线全双工(SPIDirection 2LinesFullDuplex),双线只接收(SPI Direction2Lines RxOnly), 单线只接收(SPIDirection 1LineRx)、单线只发送模式(SPI Direction1LineTx)。

2.SPI_Mode

本成员设置SPI工作在主机模式(SPIMode Master)或从机模式(SPIModeSlave ), 这两个模式的最大区别为SPI的SCK信号线的时序, SCK的时序是由通讯中的主机产生的。若被配置为从机模式,STM32的SPI外设将接受外来的SCK信号。

3.SPI_DataSize

本成员可以选择SPI通讯的数据帧大小是为8位(SPIDataSize 8b)还是16位(SPIDataSize16b)。

4.SPICPOL和SPICPHA

这两个成员配置SPI的时钟极性CPOL和时钟相位CPHA,这两个配置影响到SPI的通讯模式, 关于CPOL和CPHA的说明参考前面"通讯模式"小节。

时钟极性CPOL成员,可设置为高电平(SPICPOL High)或低电平(SPICPOLLow )。

时钟相位CPHA 则可以设置为SPICPHA 1Edge(在SCK的奇数边沿采集数据) 或SPICPHA2Edge(在SCK的偶数边沿采集数据) 。

5.SPI_NSS

片选, 本成员配置NSS引脚的使用模式,可以选择为硬件模式(SPINSS Hard )与软件模式(SPINSSSoft ), 在硬件模式中的SPI片选信号由SPI硬件自动产生,而软件模式则需要我们亲自把相应的GPIO端口拉高或置低产生非片选和片选信号。实际中软件模式应用比较多。

6.SPI_BaudRatePrescaler

本成员设置波特率分频因子,分频后的时钟即为SPI的SCK信号线的时钟频率。这个成员参数可设置为fpclk的2、4、6、8、16、32、64、128、256分频。

7.SPI_FirstBit

高位先行或低位先行,所有串行的通讯协议都会有MSB先行(高位数据在前)还是LSB先行(低位数据在前)的问题,而STM32的SPI模块可以通过这个结构体成员,对这个特性编程控制。

8.SPI_CRCPolynomial

这是SPI的CRC校验中的多项式,若我们使用CRC校验时,就使用这个成员的参数(多项式),来计算CRC的值。

6.2SPI结构体初始化配置

cpp 复制代码
/*
一、初始化代码流程:
	1.开启时钟(SPI与GPIO)
	2.初始化GPIO口(外设控制的输出信号--复用推挽输出)
	3.配置SPI外设
	4.开关控制(调用SPI_Cmd()使能)
*/

void MySPI_Init(void)
{
	//第一步:开启GPIO时钟、SPI外设时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1,ENABLE);
	
	//第二步:初始化SPI外设复用的GPIO
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStructure);
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStructure);
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStructure);
	
	//第三步:初始化SPI外设
	
	SPI_InitTypeDef SPI_InitStructure;
	//配置SCK的时钟频率
	SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_128;
	//CPHA与CPOL是配置SPI模式的(模式0~3)
	SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;//第一个边沿采样
	SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low;//默认为低电平
	//CRC校验的多项式
	SPI_InitStructure.SPI_CRCPolynomial = 7;
	//8位or16位的数据帧
	SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;
	//SPI的通信模式
	SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
	//高位or低位先行
	SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;
	//SPI模式,决定当前设备是SPI的主机还是从机
	SPI_InitStructure.SPI_Mode = SPI_Mode_Master;
	//NSS模式(硬件模式or软件模式)
	SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;
	
	SPI_Init(SPI1,&SPI_InitStructure);
	
	//第四步:使能SPI外设
	SPI_Cmd(SPI1,ENABLE);

	MySPI_W_SS(1);//默认不选用从机
}

6.3 SPI读写数据

cpp 复制代码
//SPI外设交换一个字节的流程
//顺序:SS下降沿--移出数据--SCK上升沿--移入数据--SCK下降沿--移出数据...
/*
流程:
  1.等待TXE=1,就是发送数据寄存器(TDR)空,此时 可以向TDR写入发送的数据
  2.调用SPI_I2S_SendData函数,发送TDR寄存器中的数据
  3.等待RXNE=1,就是接收寄存器(RDR)非空,此时 可以读取RDR中的数据
  4.读取RDR寄存器中的数据
*/
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{
	//第一步:等待TXE为1,发送数据寄存器空,表示可以往该寄存器写数据了
	while (SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_TXE) != SET);
	//第二步:写入TDR的数据
	SPI_I2S_SendData(SPI1,ByteSend);//转移到移位寄存器并生成波形都是自动完成的
	//第三步:等待RXNE为1,表示接收数据寄存器非空,也就是说接收数据寄存器有数据
	while (SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_RXNE) != SET);
	//第四步:读取DR
	return SPI_I2S_ReceiveData(SPI1);//从接收寄存器中读取数据
}

/*手动翻转电平 代码start*/
//从机选择
void MySPI_W_SS(uint8_t BitValue)
{
    //先将GPIOA4引脚链接到SPI从机的SS,这一步的目的是用GPIO模拟SS
    //在SPI的一主多从模式下用GPIO模拟SS是较好的做法
	GPIO_WriteBit(GPIOA,GPIO_Pin_4, (BitAction)BitValue);//用GPIO模拟SS
}

//起始信号
void MySPI_Start(void)
{
	MySPI_W_SS(0);
}

//终止信号
void MySPI_Stop(void)
{
	MySPI_W_SS(1);
}

/*一、
  写与读其实函数内容是一样的,因为SPI的工作方式就是交换一个数据
  如果想读数据,就随便发送一个数据,用来交换从机SPI的数据
  如果想写数据,那就将数据直接发送,交换会的数据直接丢弃
 二、配置时序流程:
	写DR、读DR、获取状态标志位
*/

//读多个字节的数据
#define DUMMY_BYTE	0xFF//定义一个空壳数据 用于与从机交换数据 
void SPI_ReadData(uint8_t *DataArray, uint32_t Count)
{
	uint16_t i;
	MySPI_Start();//起始信号
	for (i = 0; i < Count; i ++)
	{
		DataArray[i] = MySPI_SwapByte(W25Q64_DUMMY_BYTE);//交换数据
	}
	MySPI_Stop();//终止信号
}

//写多个字节的数据
void SPI_WriteData( uint8_t *DataArray, uint16_t Count)
{
	uint16_t i;
	MySPI_Start();
	for (i = 0; i < Count; i ++)
	{
		MySPI_SwapByte(DataArray[i]);
	}
	MySPI_Stop();
}

//main函数
#include "stm32f10x.h"                  // Device header

uint8_t ArrayWrite[] = {0xA1,0xB2,0xC3,0xD4};
uint8_t ArrayRead[4];

int main(void)
{
	MySPI_Init();//初始化SPI结构体之后,SPI就已经通电工作了
	//写入测试 直接调用读写函数就可以了,可以通过USART或者OLED显示验证一下
	SPI_WriteData(ArrayWrite,4);//写
	SPI_ReadData(ArrayRead,4);//读

	while(1)
	{	
		
	}
}
相关推荐
LateBloomer7772 小时前
FreeRTOS——信号量
笔记·stm32·学习·freertos
wenchm3 小时前
细说STM32单片机DMA中断收发RTC实时时间并改善其鲁棒性的另一种方法
stm32·单片机·嵌入式硬件
编码追梦人4 小时前
如何实现单片机的安全启动和安全固件更新
单片机
电子工程师UP学堂4 小时前
电子应用设计方案-16:智能闹钟系统方案设计
单片机·嵌入式硬件
飞凌嵌入式5 小时前
飞凌嵌入式T113-i开发板RISC-V核的实时应用方案
人工智能·嵌入式硬件·嵌入式·risc-v·飞凌嵌入式
blessing。。6 小时前
I2C学习
linux·单片机·嵌入式硬件·嵌入式
嵌新程7 小时前
day03(单片机高级)RTOS
stm32·单片机·嵌入式硬件·freertos·rtos·u575
Lin2012307 小时前
STM32 Keil5 attribute 关键字的用法
stm32·单片机·嵌入式硬件
电工小王(全国可飞)8 小时前
STM32 RAM在Memory Map中被分为3个区域
stm32·单片机·嵌入式硬件
maxiumII8 小时前
Diving into the STM32 HAL-----DAC笔记
笔记·stm32·嵌入式硬件