STM32学习笔记:SPI通信实验入门指南
上期我们讲完了IIC通信实验,今天我们继续我们的通信专题,来将我们的SPI通信实验,并以与AS5047P编码器进行通信为例。有前面IIC通信实验的基础和对这些通信的理解,这里我们会号理解很多。下面我们将进入今天的正题。
SPI简介
我们先来简单了解一下什么是SPI,SPI 是英语 Serial Peripheral interface 的缩写,顾名思义就是串行外围设备接口。其他的关于SPI的简介就不多说了,我们只需要知道SPI是一种高速的,全双工,同步的通信总线,并且在芯片的管脚上只占用四根线。
SPI四条通信线组成
SPI 接口一般使用 4 条线通信:
- MISO 主设备数据输入,从设备数据输出。
- MOSI 主设备数据输出,从设备数据输入
- SCLK 时钟信号,由主设备产生。
- CS 从设备片选信号,由主设备控制。片选信号在作用顾名思义就是主机选择需要对其进行通信的从机,通过拉低从机相应的片选信号,主机就可以与相应从机进行通信,也因此SPI的单个主机可以同时挂载多个从机。
从图中可以看出,主机和从机都有一个串行移位寄存器,主机通过向它的
SPI
串行寄存器写入一个字节来发起一次传输。寄存器通过
MOSI
信号线将字节传送给从机,从机也将自己的 移位寄存器中的内容通过 MISO
信号线返回给主机。这样,两个移位寄存器中的内容就被交换。 外设的写操作和读操作是同步完成的。如果只进行写操作,主机只需忽略接收到的字节;反之, 若主机要读取从机的一个字节,就必须发送一个空字节来引发从机的传输。
SPI 总线四种工作方式
SPI 模块为了和外设进行数据交换,根据外设工作要求,其输出串行同步时钟极性和相位可以进行配置,总共有四种组合。时钟极性(CPOL)对传输协议没有重大的影响。如果CPOL=0,串行同步时钟的空闲状态为低电平;如果 CPOL=1,串行同步时钟的空闲状态为高电平。时钟相位(CPHA)能够配置用于选择两种不同的传输协议之一进行数据传输。如果CPHA=0,在串行同步时钟的第一个跳变沿(上升或下降)数据被采样;如果 CPHA=1,在串行同步时钟的第二个跳变沿(上升或下降)数据被采样。SPI 主模块和与之通信的外设备时钟相位和极性应该一致。
SPI的初始化
1)配置相关引脚的复用功能,使能 SPI2 时钟
这里我们使用的是 SPI2,其中 SCK.、MISO 和 MOSI 这三条线相应的引脚就是PB13、PB14、PB15,把他们设置都成复用输出。按照下面的手册 MISO 应该设置成浮空或上拉输入,但实际上也可以设置成复用输出。
这里有小伙伴会问了,那CS这条线呢?好问题!其实 CS 这条线如果我们是使用软件 SPI 的话,我们可以用任意个可以输出高低电平的 IO 口都可以充当 CS,但是如果使用硬件 SPI 那就必须使用相应的引脚了。这里我们是用软件 SPI,所以也直接使用PB12作为 CS。CS 的初始化我们放到AS5047P 的初始化函数里面去了。
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE ); //PORTB 时钟使能
RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2, ENABLE ); //SPI2 时钟使能
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //PB13/14/15 复用推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure); //初始化 GPIOB
2)初始化 SPI2,设置 SPI2 工作模式
这一步我们需要使用到的函数是void SPI_Init(),该函数的第一个参数是一个结构体指针,是选择初始化哪一个 SPI 的;第二个参数也是一个结构体指针,这个结构体类型为 SPI_InitTypeDef,我们先来看看这个结构体的定义,我们一个变量一个变量地来分析。
typedef struct
{
uint16_t SPI_Direction;
uint16_t SPI_Mode;
uint16_t SPI_DataSize;
uint16_t SPI_CPOL;
uint16_t SPI_CPHA;
uint16_t SPI_NSS;
uint16_t SPI_BaudRatePrescaler;
uint16_t SPI_FirstBit;
uint16_t SPI_CRCPolynomial;
}SPI_InitTypeDef;
首先,第一个成员变量 SPI_Direction 是用来设置 SPI 的通信方式,可以选择为半双工,全双工,以及串行发和串行收方式。这个结构体每一个成员变量相应的参数定义在了头文件stm32f10x_spi.h中。这里我们选择全双工。
#define SPI_Direction_2Lines_FullDuplex ((uint16_t)0x0000) //全双工
#define SPI_Direction_2Lines_RxOnly ((uint16_t)0x0400) //半双工
#define SPI_Direction_1Line_Rx ((uint16_t)0x8000) //串行收
#define SPI_Direction_1Line_Tx ((uint16_t)0xC000) //串行发
第二个成员变量 SPI_Mode 用来设置 SPI 的主从模式,这里我们设置为主机模式。
#define SPI_Mode_Master ((uint16_t)0x0104) //主
#define SPI_Mode_Slave ((uint16_t)0x0000) //从
第三个参数 SPI_DataSiz 为 8 位还是 16 位帧格式选择项,这里我们使用的 AS5047P 编码器是 16位帧格式的。
#define SPI_DataSize_16b ((uint16_t)0x0800)
#define SPI_DataSize_8b ((uint16_t)0x0000)
第四个参数 SPI_CPOL 用来设置时钟极性 ,因为我们使用的AS5047P编码器串行同步时钟的空闲状态为低电平所以我们选择 SPI_CPOL_Low。
#define SPI_CPOL_Low ((uint16_t)0x0000)
#define SPI_CPOL_High ((uint16_t)0x0002)
第五个参数 SPI_CPHA 用来设置时钟相位,也就是选择在串行同步时钟的第几个跳变沿(上升或下降)数据被采样,AS5047P是在串行同步时钟的第二个跳变沿数据被采样,所以我们选择SPI_CPHA_2Edge。
#define SPI_CPHA_1Edge ((uint16_t)0x0000)
#define SPI_CPHA_2Edge ((uint16_t)0x0001)
第六个参数 SPI_NSS 设置 NSS 信号由硬件(NSS 管脚)还是软件控制,这里我们通过软件控制 NSS 关键,而不是硬件自动控制,所以选择 SPI_NSS_Soft。
#define SPI_NSS_Soft ((uint16_t)0x0200)
#define SPI_NSS_Hard ((uint16_t)0x0000)
第七个参数 SPI_BaudRatePrescaler 很关键,就是设置 SPI 波特率预分频值也就是决定 SPI 的时钟的参数,从不分频道 256 分频 8 个可选值。我们这里没有什么特殊的要求,选择了SPI_BaudRatePrescaler_256。传输速度为 36M/256=140.625KHz。
#define SPI_BaudRatePrescaler_2 ((uint16_t)0x0000)
#define SPI_BaudRatePrescaler_4 ((uint16_t)0x0008)
#define SPI_BaudRatePrescaler_8 ((uint16_t)0x0010)
#define SPI_BaudRatePrescaler_16 ((uint16_t)0x0018)
#define SPI_BaudRatePrescaler_32 ((uint16_t)0x0020)
#define SPI_BaudRatePrescaler_64 ((uint16_t)0x0028)
#define SPI_BaudRatePrescaler_128 ((uint16_t)0x0030)
#define SPI_BaudRatePrescaler_256 ((uint16_t)0x0038)
第八个参数 SPI_FirstBit 设置数据传输顺序是从 MSB 位开始还是从 LSB 位开始,这里我们选择
SPI_FirstBit_MSB 高位在前。
#define SPI_FirstBit_MSB ((uint16_t)0x0000)
#define SPI_FirstBit_LSB ((uint16_t)0x0080)
第九个参数 SPI_CRCPolynomial 是用来设置 CRC 校验多项式,提高通信可靠性,大于 1 即可。
最终我们的SPI的初始化配置如下:
SPI_InitTypeDef SPI_InitStructure;
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; //双线双向全双工
SPI_InitStructure.SPI_Mode = SPI_Mode_Master; //主 SPI
SPI_InitStructure.SPI_DataSize = SPI_DataSize_16b; //SPI发送接收16位帧结构
SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; //串行同步时钟的空闲状态为低电平
SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge; //第二个跳变沿数据被采样
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; //NSS信号由软件控制
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_256; //预分频256
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; //数据传输从MSB位开始
SPI_InitStructure.SPI_CRCPolynomial = 7; //CRC值计算的多项式
SPI_Init(SPI2, &SPI_InitStructure); //根据指定的参数初始化外设 SPIx 寄存器
3)使能 SPI2
我们完成SPI的初始化配置之后,还有关键的一步就是使能 SPI 。我们使能SPI使用到的函数是SPI_Cmd()。这个函数的第一个参数是 SPI 的选择,我们使用的是 SPI2 ;第二个参数是使能或失能,我们这里选择使能 ENABLE。
SPI_Cmd(SPI2, ENABLE);
SPI的读写函数
固件库已经给我们提供了发送和接收数据的函数了。分别是发送数据函数 SPI_I2S_SendData() 和接收数据函数 SPI_I2S_ReceiveData()。其中 SPI_I2S_SendData() 有两个参数,第一个是选择SPI,第二个是我们要发送的 16 位的数据,但是这个函数没有返回值。而 SPI_I2S_ReceiveData() 函数只有一个参数,就是选择SPI,要注意的是它有返回值,返回值就是接收到的数据。
由于SPI的数据发送和接收是同时进行的,所以我们直接将读和写两个操作整合在一个读写函数中。
u8 SPI2_ReadWriteByte(u8 TxData)
{
u8 retry=0;
while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_TXE) == RESET)
SPI_I2S_SendData(SPI2, TxData);
retry=0;
while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_RXNE) == RESET)
return SPI_I2S_ReceiveData(SPI2);
}
这里我们使用了两个while循环来分别等待标志位 SPI_I2S_FLAG_TXE 和 SPI_I2S_FLAG_RXNE被置位,SPI_I2S_FLAG_TXE 是发送缓冲区是否为空的标志位,SPI_I2S_FLAG_RXNE 是接收缓冲区是否为空的标志位。这样能确保未发送出去或未取出来的在缓冲区的数据不会被覆盖造成数据丢失。
AS5047P的初始化函数
其实AS5047P的初始化就是相应片选信号线CS的初始化。
void AS5047P_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOB,ENABLE);
//CS: PB12
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
AS5047P=1; //片选拉高
SPI2_Init(); //SPI2初始化
}
AS5047P的读函数
在写AS5047P的读和写函数前,我们需要先做一些准备。我们要去查看AS5047P的数据手册,清除它的通信格式是怎么样的。
1)首先,我们来看AS5047P接收命令的数据格式是怎么样的。
我们看表格里面的内容,外面的内容也是对表格的解释
这里的读或写的寄存器的地址就是存储着角度数据的寄存器的地址,总共有两个,分别是ANGLEUNC 和 ANGLECOM,它们的地址分别对应的是 0x3FFE 和 0x3FFF,其中ANGLEUNC是无动态角度误差补偿的测量角度,另一个ANGLECOM是带有动态角度误差补偿的测量角度。
而我使用的是带有动态角度误差补偿的测量角度的寄存器地址ANGLECOM。
所以我们可以直接用宏定义先把 ANGLEUNC 和 ANGLECOM 的地址的值和奇偶校验位的奇偶性定义好。
#define ANGLEUNC 0x3FFE //无动态角度误差补偿的测量角度
#define ANGLEUNC_Parity 1 //读ANGLEUNC的奇偶性(偶)
#define ANGLECOM 0x3FFF //带有动态角度误差补偿的测量角度
#define ANGLECOM_Parity 2 //读ANGLECOM的奇偶性(奇)
2)然后我们来看从AS5047P读取数据的数据格式是怎么样的。
读数据时SPI读取在CSn的上升边缘进行采样,通过下一个读取命令在MISO上传输数据,如下图15所示。
3)我们再来看向AS5047P写数据的数据格式是怎么样的。
在SPI写事务中,写命令帧之后是在MOSI处的写数据帧。写入数据帧由地址在命令帧中的寄存器的新内容组成。在新内容通过写数据帧在MOSI上传输期间,旧内容在MISO上发送。在MOSI上的下一个命令中,寄存器的实际内容将在MISO上传输,如图17所示 。
4)最后我们开始写AS5047P的读函数和写函数
对于读函数,按上面我们看数据手册所说,结合我们 SPI 的学习。要对AS5047P进行读数据,应该先对其片选信号拉低进行片选,再准备开始数据的传输。然后把我们的合适的 16 位命令(包含了奇偶校验位、读或写命令、地址)放进我们的一个变量里面,再通过调用我们前面写的 SPI 的读写函数把命令发送出去,同时把我们接收回来的 16 位数据即是函数的返回值存放进另一个合适的变量里。最后将片选信号拉高取消片选。由于我们接收回来的 16 位数据还包含了第 15 位的 PARD 位和第 14 位的EF 位,如果只要我们编码器的角度数据的话,我们还需要通过位操作把 PARD 位和 EF 位清空。
u16 AS5047P_Read(u8 Addr_Parity,u16 Addr)
{
u16 readdata=0;
AS5047P=0; //拉低片选
if(Addr_Parity==2)
Addr=Addr|0x4000|0x8000; //写入读命令,奇偶校验位置1
else
Addr=Addr|0x4000; //写入读命令,奇偶校验位置0
readdata=SPI2_ReadWriteByte(Addr); //接收数据
readdata=readdata&0x3fff; //清空高两位
AS5047P=1; //片选拉高
return readdata;
}
而对于写函数,与读函数相比也是大同小异,不同的就是多一个存储要写入的数据的变量需要我们进行处理,方法上都是一样的。
然后我们还可以写一个均值滤波函数对我们取得的数据进行滤波。比如下面我的这个是去每 10 次数据的均值作为获取到的数据的均值滤波函数。这个函数非常简单,就不过多解释了。
float filter(void)
{
int i;
float sum = 0;
for(i=0;i<num;i++)
{
sum += AS5047P_Read(ANGLECOM_Parity,ANGLECOM);
}
sum = (float)(sum / num);
return sum;
}
到现在,我们获取到的数据仍然不是我们想要的角度数据,现在获取到的数据只是一个角度对应的分辨率。什么意思呢?我们现在得到的数据是 16 位数据清空高两位后得到的 14 位的二进制数据,14 位的二进制数可以表示多少个数?16384个数。讲到这,聪明的小伙伴应该明白了,就是说这16384个数把角度那360度给细分成了16384份,0度对应0,360度对应16383(注意是16383),我们还需要对其进行一些数学上的处理,最后就能得到我们相应的角度数据了。下面是我的处理方式。
angle = rawdate*360/16383;
好了,今天的 SPI 实验就到此为止了,内容有点多,建议多看几遍消化一下,我们下一期再见!!!