一、简介
SPI(Serial Peripheral interface)串行外围设备接口,该通信协议由Motorola首先在其MC68HCxx系列处理器上定义的。主要用于EEPROM、FLASH、实时时钟、AD转换器,还有数字信号处理器和数字信号解码器之间。
SPI是一种高速的、全双工、同步的通信总线,并且在芯片的管脚上只占用四根线,节省了芯片管脚,同时为PCB的布局上节省空间、提供方便,正是出于这种简单易用的特性,现在越来越多的芯片集成了这种通信协议。
二、物理层
(1)PIN脚接口定义
SPI通信要求有数据线、时钟线、控制信号线即片选信号线进行通信实现数据传输。
-
数据线
-
SDA(单线数据线时,同时做输入输出)
-
或
-
MOSI、MISO(MOSI:Master Output Slave Input;MISO:Master Input Slave Output)
-
-
时钟线
- SCL(主要作用是Master设备往Slave设备传输时钟信号,控制数据交换的时机和速率)
-
片选信号线
- CS(用于Master设备片选Slave设备,使被选中的Slave设备能够被Master设备所访问)
-
命令/数据线
- A0(Only 4-Line SPI,用于控制输入的数据为命令还是数据)
(2)硬件特性
-
支持全双工通信
-
通信简单
-
数据传输速率快
-
无指定的流控制,位于应答机制确认是否接收到数据
三、协议内容
(1)采用主-从模式的控制方式
SPI规定了两个SPI设备之间通信必须有主设备(Master)来控制从设备(Slave)。Master设备可以通过提供Clock以及对Slave设备进行片选(Slave Select) 来控制多个Slave设备,SPI协议还规定Slave设备的Clock由Master设备通过SCL管脚提供给Slave设备,Slave本身不能产生或控制Clock,没有Clock,则Slave设备不能正常工作。
(2)采用同步方式(Synchronous)传输数据
Master设备会根据要交换的数据来产生相对的时钟脉冲(Clock Pulse),时钟脉冲组成了时钟信号(Clock Signal),时钟信号通过时钟极性(CPOL)和时钟相位(CPHA)控制着两个SPI设备间何时数据交换以及何时对接收到的数据进行采样,来保证数据在两个设备之间的是同步传输的。
(3)数据交换(Data Exchanges)
SPI 设备间的数据传输之所以又称之为数据交换,是因为SPI协议规定一个SPI设备不能在数据通信过程中仅仅只充当一个“发送者(Tranesmitter)”或者“接收者(Receiver)”。在每个Clock周期内SPI设备都会发送并接收一个bit大小的数据(不管主设备还是从设备),相当于该设备有一个bit大小的数据被交换了。一个Slave设备要想能够接收到Master发送过来的控制信号,必须在此之前能被Master设备进行访问(Access)。所以,Master设备必须首先通过SS/CS PIN对Slave设备进行片选,把想要访问的Slave设备选上。在数据传输过程中,每次接收到的数据必须在下一次数据传输前被采样,如果之前接收到的数据没有被读取,那么这些已经接收完成的数据将有可能被丢弃,导致SPI物理模块最终失效。因此,在程序中一般会在SPI传输完数据后,去读取SPI设备的数据,及时这些数据(Dummy Data)在我们的程序都是无用的(虽然发送后紧接着读取是无意义的,但仍然需要从寄存器读取出来)。
四、编程实现
(1)纯逻辑实现
以4-Line SPI为例:
PIN定义:
-
SCL - 时钟线
-
SDA - 数据线
-
CS - 片选控制线
-
A0 - 命令/数据线
volatile void Write_Command(uint8_t cmd)
{
uint8_t i;
CS = 0; // 拉低片选选中Slave
A0 = 0; // A0 = 0时写入命令(根据产品规格书定义)
for(i=0; i<8; i++)
{
if( cmd & 0x80 )
SDA = 1;
else
SDA = 0;
SCL = 0;
SCL = 1;
cmd <<= 1;
}
CS = 1;
}
volatile void Write_Data(uint8_t data)
{
uint8_t i;
CS = 0; // 拉低片选选中Slave
A0 = 1; // A0 = 1时写入数据(根据产品规格书定义)
for(i=0; i<8; i++)
{
if( data & 0x80 )
SDA = 1;
else
SDA = 0;
SCL = 0;
SCL = 1;
data <<= 1;
}
CS = 1;
}
(2)配置单片机SPI外设
以STM32F103C8T6为例:
void SPI1_Init(void)
{
// 1、定义结构体存储配置
GPIO_InitTypeDef GPIO_InitStructure;
SPI_InitTypeDef SPI_InitStructure;
// 2、使能SPI1时钟源
RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOA|RCC_APB2Periph_SPI1, ENABLE );
// 3、配置SPI引脚模式(复用输出模式)
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 4、配置SPI工作模式
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; //设置SPI单向或者双向的数据模式: SPI设置为双线双向全双工
SPI_InitStructure.SPI_Mode = SPI_Mode_Master; //设置SPI工作模式: 设置为主SPI
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; //设置SPI的数据大小: SPI发送接收8位帧结构
SPI_InitStructure.SPI_CPOL = SPI_CPOL_High; //选择了串行时钟的稳态: 时钟悬空高
SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge; //数据捕获于第二个时钟沿
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; //NSS信号由硬件(NSS管脚)还是软件(使用SSI位)管理:内部NSS信号有SSI位控制
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_256;//定义波特率预分频的值:波特率预分频值为256
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; //指定数据传输从MSB位还是LSB位开始:数据传输从MSB位开始
SPI_InitStructure.SPI_CRCPolynomial = 7; //CRC值计算的多项式
SPI_Init(SPI1, &SPI_InitStructure); //根据SPI_InitStruct中指定的参数初始化外设SPIx寄存器
//5、使能SPI外设
SPI_Cmd(SPI1, ENABLE);
}
// 发送数据
// 使用标准库函数:void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data);
// 例:
SPI_I2S_SendData(SPI1, TxData);
// 读取数据
uint8_t SPI1_ReadWriteByte(uint8_t TxData)
{
uint8_t retry = 0;
while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET) //检查指定的SPI标志位设置与否:发送缓存空标志位
{
retry++;
if(retry > 200)
return 0;
}
SPI_I2S_SendData(SPI1, TxData); //通过外设SPIx发送一个数据
retry = 0;
while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET)//检查指定的SPI标志位设置与否:接受缓存非空标志位
{
retry++;
if(retry > 200)
return 0;
}
return SPI_I2S_ReceiveData(SPI1); //返回通过SPIx最近接收的数据
}