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结构体详解
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的通讯方向,可设置为双线全双工(SPIDirection2LinesFullDuplex),双线只接收(SPIDirection2LinesRxOnly), 单线只接收(SPIDirection1LineRx)、单线只发送模式(SPIDirection1LineTx)。
2.SPI_Mode
本成员设置SPI工作在主机模式(SPIModeMaster)或从机模式(SPIModeSlave ), 这两个模式的最大区别为SPI的SCK信号线的时序, SCK的时序是由通讯中的主机产生的。若被配置为从机模式,STM32的SPI外设将接受外来的SCK信号。
3.SPI_DataSize
本成员可以选择SPI通讯的数据帧大小是为8位(SPIDataSize8b)还是16位(SPIDataSize16b)。
4.SPICPOL和SPICPHA
这两个成员配置SPI的时钟极性CPOL和时钟相位CPHA,这两个配置影响到SPI的通讯模式, 关于CPOL和CPHA的说明参考前面“通讯模式”小节。
时钟极性CPOL成员,可设置为高电平(SPICPOLHigh)或低电平(SPICPOLLow )。
时钟相位CPHA 则可以设置为SPICPHA1Edge(在SCK的奇数边沿采集数据) 或SPICPHA2Edge(在SCK的偶数边沿采集数据) 。
5.SPI_NSS
片选,本成员配置NSS引脚的使用模式,可以选择为硬件模式(SPINSSHard )与软件模式(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结构体初始化配置
/*
一、初始化代码流程:
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读写数据
//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)
{
}
}