1、SPI协议基础
①、什么是SPI?
SPI,全称为Serial Peripheral Interface,是一种串行外设接口。由Motorola(摩托罗拉)公司(现为NXP Semiconductors的一部分)最早推出的同步串行通信接口标准,主要用于微控制器和其他外围设备之间的高速、全双工数据交换。SPI协议允许在一个主设备(Master)和一个或多个从设备(Slave)之间建立连接,主设备负责生成时钟信号并控制数据传输。当前,SPI广泛应用于各种嵌入式系统中,如SD卡、RFID、LCD屏幕、网络通信PHY以及其他需要快速可靠数据交换的器件之间。如下图所示为SPI一主多从通信示意图。
如图可见,在多从机通信时,有3根SPI的引脚是共用的,但唯独片选引脚CS没有共用,因为通信时,只允许主机和一个从机进行通信,需要通过片选引脚进行从机设备选择。
SPI通常为四线制通信方式:
- SCLK(Serial Clock):时钟信号,由主设备提供,决定数据传输的节拍。
- MOSI(Master Out, Slave In):主设备输出数据,从设备输入数据引脚。
- MISO(Master In, Slave Out):主设备输入数据,从设备输出数据引脚。
- CS/SS(Chip Select / Slave Select):片选引脚,主设备控制,用于选择当前通信的从设备。
②、SPI工作模式
SPI存在四种不同的工作模式,这些模式主要区别在于时钟极性和时钟相位的不同组合,其中使用的最为广泛的是模式0和模式3方式。
CPOL表示高低电平顺序,叫做极性;CPHA表示第一个/第二个边沿,叫做相位。
CPOL(Clock Polarity):时钟极性选择
- CPOL=0,SPI总线空闲时,时钟线SCLK为低电平 ;
- CPOL=1,SPI总线空闲时,时钟线SCLK为高电平。
数据的传输是从电平跳变开始的,CPOL控制在数据传输开始前,时钟信号的电平是处于低电平0还是高电平1状态。
CPHA(Clock Phase):时钟相位选择
- CPHA=0,在SCLK第一个跳变沿,主机对MISO引脚电平采样;主机的数据发送则在第二个跳边沿。
- CPHA=1,在SCLK第二个跳变沿,主机对MISO引脚电平采样。主机的数据发送则在第一个跳边沿。
一个时钟周期会有2个跳变沿。而CPHA相位,直接决定SPI总线从哪个跳变沿开始采样数据。至于这个跳变沿究竟是上升沿还是下降沿,取决于CPOL的值,CPHA只决定第几个跳变沿进行采样。
工作模式汇总:
- CPHA=0、CPOL=0:此时空闲态时,SCLK处于低电平,数据采样是在第1个边沿,就是SCLK由低电平到高电平的跳变,所以数据采样是在上升沿(准备数据),(发送数据)数据发送是在下降沿。
- CPHA=0、CPOL=1:此时空闲态时,SCLK处于高电平,数据采集是在第1个边沿,即SCLK由高电平到低电平的跳变,所以数据采集是在下降沿,数据发送是在上升沿。
- CPHA=1、CPOL=0:此时空闲态时,SCLK处于低电平,数据发送是在第1个边沿,即SCLK由低电平到高电平的跳变,所以数据采样是在下降沿,数据发送是在上升沿。
- CPHA=1、CPOL=1:此时空闲态时,SCLK处于高电平,数据发送是在第1个边沿,即SCLK由高电平到低电平的跳变,所以数据采集是在上升沿,数据发送是在下降沿。
2、SPI通信时序图解析
①、SPI模式0
MODE0:极性0 相位0
当CPOL为低电平,CPHA为低电平时
在SCLK的上升沿时期,MISO接收数据
在SCLK的下降沿时期,MOSI发送数据
②、SPI模式1
MODE1:极性0 相位1
当CPOL为低电平,CPHA为高电平时
在SCLK的上升沿时期,MOSI发送数据
在SCLK的下降沿时期,MISO接收数据
③、SPI模式2
MODE2:极性1 相位0
当CPOL为高电平,CPHA为低电平时
在SCLK的上升沿时期,MOSI发送数据
在SCLK的下降沿时期,MISO接收数据
④、SPI模式3
MODE3:极性1 相位1
当CPOL为高电平,CPHA为高电平时
在SCLK的上升沿时期,MISO接收数据
在SCLK的下降沿时期,MOSI发送数据
⑤、SPI通信模式总结
MODE0:
SCK默认为低电平
在第一边沿(上升沿)进行读取 在第二边沿(下降沿)进行发送
MODE1:
SCK默认为低电平
在第一边沿(上升沿)进行发送 在第二边沿(下降沿)进行读取
MODE2:
SCK默认为高电平
在第一边沿(下降沿)进行读取 在第二边沿(上升沿)进行发送
MODE3:
SCK默认为高电平
在第一边沿(下降沿)进行发送 在第二边沿(上升沿)进行读取
3、SPI通信源码
①、编程思路分析
(1)、初始化通信GPIO引脚
(2)、确定SPI通信的具体模式
(3)、控制SPI的时钟CLK引脚
(4)、确定该模式在上升沿时刻对应的操作
(5)、确定该模式在下降沿时刻对应的操作
②、spi.h
#ifndef __SPI_H
#define __SPI_H
#define SPI_CLK(val) HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, val)
#define SPI_MISO HAL_GPIO_ReadPin (GPIOA, GPIO_PIN_2)
#define SPI_MOSI(val) HAL_GPIO_WritePin(GPIOA, GPIO_PIN_3, val)
#define SPI_CS(val) HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, val)
void SPI_GPIO_Init(void);
void delay(void);
char SPI_Mode0_ReadWrite_Data(char WData);
char SPI_Mode1_ReadWrite_Data(char WData);
char SPI_Mode2_ReadWrite_Data(char WData);
char SPI_Mode3_ReadWrite_Data(char WData);
#endif
③、spi.c
#include “spi.h”
/**
* @brief SPI GPIO引脚初始化
* @param NONE
* @retval NONE
*/
void SPI_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_GPIOC_CLK_ENABLE();
__HAL_RCC_GPIOF_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1|GPIO_PIN_2|GPIO_PIN_3|GPIO_PIN_4, GPIO_PIN_SET);
//CLK、MOSI、CS
GPIO_InitStruct.Pin = GPIO_PIN_1|GPIO_PIN_3|GPIO_PIN_4;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
//MISO
GPIO_InitStruct.Pin = GPIO_PIN_2;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}
/**
* @brief SPI粗延时
* @param NONE
* @retval NONE
*/
void delay(void)
{
for(int i=0; i<10;i++)
{
;
}
}
/**
* @brief SPI模式0读写数据
* @param WData: 需要写的数据
* @retval 读取的数据
*/
char SPI_Mode0_ReadWrite_Data(char WData)
{
//模式0,SCLK默认低电平
//上升沿读取,下降沿发送
SPI_CLK(0);
char RData = 0;
for(int i=0; i<8; i++)
{
delay();
SPI_CLK(1); //上升沿 0->1的瞬间
RData |= SPI_MISO<<i; //接收数据
delay();
SPI_CLK(0); //下降沿 1->0的瞬间
if((WData>>i)&0x01) //发送数据
{
SPI_MOSI(1);
}else{
SPI_MOSI(0);
}
}
return RData;
}
/**
* @brief SPI模式1读写数据
* @param WData: 需要写的数据
* @retval 读取的数据
*/
char SPI_Mode1_ReadWrite_Data(char WData)
{
//模式1,SCLK默认低电平
//上升沿发送,下降沿读取
SPI_CLK(0);
char RData = 0;
for(int i=0; i<8; i++)
{
delay();
SPI_CLK(1); //上升沿 0->1的瞬间
if((WData>>i)&0x01) //发送数据
{
SPI_MOSI(1);
}else{
SPI_MOSI(0);
}
delay();
SPI_CLK(0); //下降沿 1->0的瞬间
RData |= SPI_MISO<<i; //接收数据
}
return RData;
}
/**
* @brief SPI模式2读写数据
* @param WData: 需要写的数据
* @retval 读取的数据
*/
char SPI_Mode2_ReadWrite_Data(char WData)
{
//模式2,SCLK默认高电平
//下降沿读取,上升沿发送
SPI_CLK(1);
char RData = 0;
for(int i=0; i<8; i++)
{
delay();
SPI_CLK(0); //下降沿 1->0的瞬间
RData |= SPI_MISO<<i; //接收数据
delay();
SPI_CLK(1); //上升沿 0->1的瞬间
if((WData>>i)&0x01) //发送数据
{
SPI_MOSI(1);
}else{
SPI_MOSI(0);
}
}
return RData;
}
/**
* @brief SPI模式3读写数据
* @param WData: 需要写的数据
* @retval 读取的数据
*/
char SPI_Mode3_ReadWrite_Data(char WData)
{
//模式3,SCLK默认高电平
//下降沿发送,上升沿读取
SPI_CLK(1);
char RData = 0;
for(int i=0; i<8; i++)
{
delay();
if((WData>>i)&0x01) //发送数据
{
SPI_MOSI(1);
}else{
SPI_MOSI(0);
}
SPI_CLK(0); //下降沿 1->0的瞬间
delay();
SPI_CLK(1); //上升沿 0->1的瞬间
RData |= SPI_MISO<<i; //接收数据
}
return RData;
}