STM32 SPI标准库函数详解
一、初始化时钟和GPIO
SPI作为外设,必须使能对应的时钟才能正常运转,而SPI挂载在APB2总线上,因此使能RCC_APB2Periph_SPI1总线的时钟。SPI还需要使用GPIO接口(MOSI、MISO、NSS、SCK)来完成与主机/从机的通信,因此也必须使能对应的GPIOA的时钟。
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_SPI1,ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7;//PA5 SCK;PA6 MISO;PA7 MOSI
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //开的最大速度
GPIO_Init(GPIOA, &GPIO_InitStructure);
本文主要讲解SPI部分,不再赘述GPIO初始化。
二、初始化SPI
2.1 SPI各个寄存器
以SPI1为例,它也包含以上9个寄存器。根据图中寄存器组成,可以使用C语言构建SPI的结构体,如下所示。上图中Reserved为未被利用的空间,虽没有实际意义,但的确占用了两字节空间,因此在将SPI1内存反序列化变成结构体时,必须定义两字节空间来占位。
typedef struct
{
uint16_t CR1;
uint16_t RESERVED0;//未被利用的空间
uint16_t CR2;
uint16_t RESERVED1;
uint16_t SR;
uint16_t RESERVED2;
uint16_t DR;
uint16_t RESERVED3;
uint16_t CRCPR;
uint16_t RESERVED4;
uint16_t RXCRCR;
uint16_t RESERVED5;
uint16_t TXCRCR;
uint16_t RESERVED6;
uint16_t I2SCFGR;
uint16_t RESERVED7;
uint16_t I2SPR;
uint16_t RESERVED8;
}MySPI_TypeDef;
2.2 SPI CR1寄存器
上图为SPI CR1的十六位功能,这是包含SPI基本功能最多的一个寄存器,我们可以选择它的发送方向、帧格式、数据大小等等。下面是对源码的解析,这里对数据每一位功能不再赘述,细节自行翻看数据手册。
//MySPI_InitTypeDef结构体用于SPI初始化
typedef struct
{
uint16_t Direction;//SPI传输方向
uint16_t DataSize;//数据大小
uint16_t SPIEN;//SPI使能状态
uint16_t SendOrder;//SPI帧格式发送顺序
uint16_t CRCEN;//CRC使能
uint16_t CRCNEXT;//CRC下一次是否传输
uint16_t SSM;//软件从器件管理
uint16_t SSI;//内部从器件选择
uint16_t BoudConrtol;//波特率控制分频
uint16_t Mode;//主模式选择
uint16_t CPOL;//时钟极性
uint16_t CPHA;//时钟相位
}MySPI_InitTypeDef;
首先你需要明确所需SPI的基本功能是什么,再根据所需功能确定CR1寄存器的基本参数,根据收据手册建立SPI初始化结构体 MySPI_InitTypeDef ,接下来将初始化结构体实例化并填入相关参数。
MySPI_InitTypeDef MySPI_InitStructure;
MySPI_InitStructure.Direction = MYSPI_DoubleLines_FullDuplex;
MySPI_InitStructure.DataSize = MYSPI_DATASIZE_16BIT;
//MySPI_InitStructure.SPIEN等待初始条件配置完成之后再使能
MySPI_InitStructure.SendOrder = MYSPI_SendFirst_MSB;
MySPI_InitStructure.CRCEN = MYSPI_CRC_ENBLE;//TODO:这里需要使能吗
//MySPI_InitStructure.CRCNEXT在发送的时候再修改状态
MySPI_InitStructure.SSM = MYSPI_SSM_ENABLE;
MySPI_InitStructure.SSI = MYSPI_SSI_High;//在选择从设备的时候对SSI再进行修改
MySPI_InitStructure.BoudConrtol = MYSPI_BAUD_FPCLK_256;
MySPI_InitStructure.Mode = MYSPI_MODE_MASTER;
MySPI_InitStructure.CPOL = MYSPI_CPOL_Low;
MySPI_InitStructure.CPHA = MYSPI_CPHA_1Edge;
参数确定之后,我们进行将初始化结构体的参数写入CR1寄存器并使能SPI。
MySPI_Init(MYSPI1, MySPI_InitStructure);
SPI1->CR1 |= MYSPI_ENABLE;//使能SPI1
#define CR1_MASK 0x3040 //只保留13、12、6位的数据,CRC和SPI使能部分
#define MYSPI_CRCPolynomial 0x07
void MySPI_Init(MySPI_TypeDef *SPIx , MySPI_InitTypeDef MySPI_InitStructure)
{
//判断各个参数是否在选择范围内
assert_param(IS_MYSPI_Direction(MySPI_InitStructure.Direction));
assert_param(IS_MYSPI_DataSize(MySPI_InitStructure.DataSize));
assert_param(IS_MYSPI_SendOrder(MySPI_InitStructure.SendOrder));
assert_param(IS_MYSPI_CRCEN(MySPI_InitStructure.CRCEN));
assert_param(IS_MYSPI_SSM(MySPI_InitStructure.SSM));
assert_param(IS_MYSPI_SSI(MySPI_InitStructure.SSI));
assert_param(IS_MYSPI_BoudConrtol(MySPI_InitStructure.BoudConrtol));
assert_param(IS_MYSPI_Mode(MySPI_InitStructure.Mode));
assert_param(IS_MYSPI_CPOL(MySPI_InitStructure.CPOL));
assert_param(IS_MYSPI_CPHA(MySPI_InitStructure.CPHA));
//将SPI初始化的配置写入CR1寄存器
uint16_t temp_cr1 = SPI1->CR1;
//利用掩码清除CR1相关位的数值
temp_cr1 &= CR1_MASK;
temp_cr1 |= (uint16_t)(MySPI_InitStructure.BoudConrtol | MySPI_InitStructure.CPHA |
MySPI_InitStructure.CPOL | MySPI_InitStructure.DataSize |
MySPI_InitStructure.Direction | MySPI_InitStructure.Mode |
MySPI_InitStructure.SendOrder | MySPI_InitStructure.SSI |
MySPI_InitStructure.SSM);
SPI1->CR1 = temp_cr1;
//研究SPI其他寄存器用途:
//I2SCFGR:选择I2S和SPI模式,位11为0选择SPI模式,为1选择I2S模式
/* Activate the SPI mode (Reset I2SMOD bit in I2SCFGR register) */
SPIx->I2SCFGR &= SPI_Mode_Select;
/*---------------------------- SPIx CRCPOLY Configuration --------------------*/
/* Write to SPIx CRCPOLY */
//CRCPR:此寄存器包含用于 CRC 计算的多项式
SPIx->CRCPR = MYSPI_CRCPolynomial; //CRC用来校验的生成多项式
}
现在我们来研读SPI初始化函数
- assert_param函数用来判断传入参数是否符合规范,是否在限定条件内。
- temp_cr1 &= CR1_MASK;用来清除相关位的数据,以防之前的条件影响到本次SPI传输。
- CR1_MASK 为 0x3040 即 0011000001000000 只保留13、12、6位的数据,CRC和SPI使能部分的数据。
MySPI_InitStructure.BoudConrtol | MySPI_InitStructure.CPHA等等 使用位或运算符将所有的参数计算得到16为数据即为CR1寄存器的数据。
I2SCFGR寄存器可选择SPI模式和I2S模式,这里选择SPI模式
CRCPR寄存器选择生成多项式0x07,多项式表示为
,这里多项式可以有很多种情况。
在初始化SPI功能并使能之后,开始进行数据的发送。要进行数据的发送,我们首先要知道DR寄存器的发送缓冲区为空,上一次的数据才完成发送,本次数据才能被写入。相反的,在接收的情况下,只有DR寄存器的接收缓冲区不为空,程序才可以读取接收缓存区的数据。
那么如何判断发送缓存区和接收缓存区的是否为空的状态呢?我们使用SPI SR状态寄存器来判断,SR寄存器位0判断接收缓存区非空,位1判断发送寄存器缓存区为空。
MySPI_SendAndRead(SPI1, 0xff);
//发送缓冲区为空
#define Transmit_Buffer_Empty 0x10
//接收缓冲区非空
#define Receive_Buffer_Not_Empty 0x01
//通过SPI发送和接收数据
uint16_t MySPI_SendAndRead(MySPI_TypeDef* SPIx, uint16_t MySPIdata)
{
uint8_t temp = 0;
//等待SPI发送完成,即SPI发送缓冲区清零
//当SR寄存器没有将数据发送出去,SR还是0x00,SR不为空,按位与运算结果为0x00
//当SR寄存器将数据全部发送出去后,SR寄存器发送缓存区清零,可以进行发送操作了,SR变成0x10,按位与运算结果为0x10
while((SPIx->SR & Transmit_Buffer_Empty) == RESET)
{
temp++;
if(temp > 200)
return 0;//错误机制
}
SPIx->DR = MySPIdata;
temp = 0;
//SR接收缓存区有数据了,可以进行接收了
while((SPIx->SR & Receive_Buffer_Not_Empty) == RESET)
{
temp++;
if(temp > 200);
return 0;//错误机制
}
return SPIx->DR;//返回接收的数据
}
最后的最后,让我们来讲解为什么在SPI使能完成后要先发送 0xFF 的数据?
在SPI(Serial Peripheral Interface)双线传输模式中,经常需要采取一种称为“虚拟发送以接收”(dummy transmit to receive)的策略。这是因为在许多SPI从设备的设计中,接收数据的过程是由主设备发送一个时钟信号和一个(可能是无意义的)数据字节来触发的。因此我们只有先发送一个数据,才能接收到从设备的数据。
发送这个无意义的数据字节(如0xFF或其他任意值)主要是为了满足从设备的通信协议要求,以确保它能够开始发送回数据给主设备。由于主设备的主要目标是接收数据,因此发送的数据内容并不重要,只要它不会干扰通信过程即可。
作者:HairyCrabs6