目录
六、如果这篇文章能帮助到你,请点个赞鼓励一下吧ξ( ✿>◡❛)~
一、前言
SPI(Serial Peripheral Interface,串行外设接口)是由摩托罗拉公司于20世纪80年代中期开发的一种同步串行通信协议,已成为嵌入式系统中微控制器与外围设备之间数据交互的最常用接口之一,SPI是一种高速、全双工、同步的通信总线,主要用于短距离通信,通信速率可以达到数十兆Hz,相较于IIC、UART等协议速率更快,并且现在改进版的SPI,例如DSPI(在读取数据时将MOSI管脚转换为MISO管脚)能够达到双倍SPI速率、QSPI(六线制SPI,并行访问,位宽更宽)数据传输速度甚至最高可以达到800Mbps,这个速度已经超过内部FLASH读写速度,因此使用QSPI协议的外挂FLASH可以当作内部FLASH来使用,以再资源有限的单片机上实现更加丰富的功能。
二、SPI四线功能介绍

标准的SPI通信协议需要四根线,分别是CLK时钟线、CS片选线、MOSI数据线、MISO数据线,接下来详细解释这四根协议线的用途:
1、CLK时钟控制线:
CLK管脚用来为SPI通信数据提供时钟信号,SPI协议是全双工同步通信协议,通信时钟由SPI协议中的主机提供,主机的CLK管脚为输出,控制SPI通信的时钟,对应的,从机是被控对象,它的CLK管脚为输入模式,SPI通信中主/从机的区别主要就是CLK和CS管脚输入输出的区别。
在使用软件模拟SPI时,主机的CLK管脚配置为GPIO推挽输出模式。
2、CS片选控制线:
CS片选端控制从机设备的响应,一般情况下CS低电平有效,低电平表示主机从机之间的SPI通信协议开始;当CS为高电平时,表示主机从机之间的SPI通信协议结束。CS可以用来控制SPI通信协议的启停,也可以用来在多个从机设备之间进行切换。
在使用软件模拟SPI时,主机的CS管脚配置为GPIO推挽输出模式。
3、MOSI主机输出数据线:
MOSI数据线,也叫主机输出,从机输入数据线,功能就是字面意思。
在使用软件模拟SPI时,主机的MOSI管脚配置为GPIO推挽输出模式。
4、MISO主机输入数据线。
MISO数据线,也叫主机输入,从机输出数据线,同上。
在使用软件模拟SPI时,主机的MISO管脚配置为GPIO浮空输入模式。
三、SPI通信协议参数介绍
SPI通信协议中有两个重要的参数——CPOL和CPHA。在配置主机模拟SPI通信协议前,一定要先从数据手册了解从机的SPI的CPOL和CPHA控制参数,这两个参数可以组成四种不同的SPI通信模式,需要明白从机在使用SPI协议进行通信时的协议特征,这都是在出厂时规定设计好的。
接下来对这两个参数进行详细的介绍说明。
1、CPOL(Clock Polarity,时钟极性)
决定了SPI通信中空闲状态的时钟电平;
CPOL=0,空闲状态下CLK时钟线为低电平;
CPOL=1,空闲状态下CLK时钟线为高电平。
2、CPHA(Clock Phase,时钟相位)
决定了数据是在CLK时钟线的第一个边沿还是第二个边沿被采样;
CPHA=0,数据在CLK的第一个边沿被采样;
CPHA=1,数据在CLK的第二个边沿被采样;
本例程中从机SPI通信时,规定CPOL=0,CPHA=1,时钟信号低电平时为空闲状态,数据采样边沿为第二个,也就是时钟信号下降沿进行数据采样发送。因此设计软件模拟SPI时,需要考虑到这个情况。
需要说明的是,SPI通信协议以字节为单位进行数据传输,本质上是主机和从机的移位寄存器之间一字节数据的交换,本质是交换,这一点需要记住。以下是详细的解释,赶时间的同学可以直接跳过。
SPI通信是通过硬件8位数据移位寄存器进行数据传输的,主机发送数据时,先把一个字节的数据放入发送缓冲区,缓冲区数据会被自动加载到8位移位寄存器,然后按位进行发送,首先将字节数据最高位对应的电信号输出到主机的MOSI输出引脚,与此同时,从机的MISO输入引脚接收到主机发来的数据位电信号,将该位电信号传入从机设备的8位移位寄存器中,同时从机将8位移位寄存器的最高位也转换为电信号通过从机输出管脚输出值主机MISO,主机接收该位电信号到移位寄存器,至此主机从机完成了一个数据位的交换,接下来主&从机移位寄存器同时进行移位,准备下一个数据位的发送接收。以上步骤重复8次,就完成了一个字节数据的传输,等效于主机从机的移位寄存器交换了一字节数据。
四、SPI时序分析
SPI通信协议通常为MSB高位先行,举个例子,要发送的字节数据为0x80,对应二进制0x1000 0000,MOSI管脚一次发送一位二进制数据,从高位1开始先发,这就是MSB高位先行。从机SPI时序示意图如下所示:

图中的 SSN 对应 CS 片段线; SCK 为 CLK 时钟线; SI 为MOSI 主机输出、从机输入数据线;SO为 MISO 主机输入、从机输出数据线,该从机的CPOL=0,CPHA=1。由图可知:
SPI开始信号:
CS片选端由高电平降为低电平,表示SPI主机从机通信开始。
SPI停止信号:
CS片选端由低电平升为高电平,表示SPI主机从机通信结束。
SPI时钟信号:
规律的方波信号,为数据线提供时钟基准。
SPI发送/接收数据时序:
在CPOL=0,CPHA=1的情况下,数据线在时钟的下降沿时被采样并发送,这要求数据线在下降沿之前需要完成电平状态的变化,并在下降沿来临时保持稳定不变。
发送/接收数据过程解析:
SPI发送接收数据都是以1个字节,8位二进制数据为单位进行的,这是由SPI中的8位移位寄存器(前面介绍的有)决定的。将一字节数据按位发送,例如要发送0x80,对应二进制数就是0b1000 0000,高位先行,使用软件模拟SPI过程如下:首先将CS片选段拉低,表示SPI通信开始,然后将该字节最高位1用MOSI端口表示,即为高电平,然后控制时钟产生一个下降沿,即可完成一位数据的发送,重复8次,就完成了一字节数据的发送。
五、软件模拟SPI完整代码及注释
1、C版本
该版本替换GPIO宏定义即可使用,适合单一SPI通信应用场景。
//从机的时钟极性CPOL = 0,时钟空闲状态为低电平
//从机的时钟相位CPHA = 1,采样边沿为第二个下降沿
#define SPI0_CS_SET HAL_GPIO_WritePin(SPI0_CS_GPIO_Port, SPI0_CS_Pin, GPIO_Pin_SET)
#define SPI0_CS_CLR HAL_GPIO_WritePin(SPI0_CS_GPIO_Port, SPI0_CS_Pin, GPIO_Pin_RESET)
inline void Delay_Us( int16 us ) //提供机器周期级别延时
{
while( us-- > 0 )
__nop();
}
/*****************************************************************************
[函数名称]vSPI0_CLK
[函数功能]SPI0的时钟控制线
[备 注]
*****************************************************************************/
void vSPI0_CLK( uint8 sta )
{
if( sta == SET )
HAL_GPIO_WritePin(SPI0_SCK_GPIO_Port, SPI0_SCK_Pin, GPIO_PIN_SET);
else
HAL_GPIO_WritePin(SPI0_SCK_GPIO_Port, SPI0_SCK_Pin, GPIO_PIN_RESET);
}
/*****************************************************************************
[函数名称]vSPI0_MOSI
[函数功能]SPI0的时钟控制线
[备 注]
*****************************************************************************/
void vSPI0_MOSI( uint8 sta )
{
if( sta == SET )
HAL_GPIO_WritePin(SPI0_MOSI_GPIO_Port, SPI0_MOSI_Pin, GPIO_PIN_SET);
else
HAL_GPIO_WritePin(SPI0_MOSI_GPIO_Port, SPI0_MOSI_Pin, GPIO_PIN_RESET);
}
/*****************************************************************************
[函数名称]vSPI0_CLK_Up
[函数功能]模拟时钟上升沿
[备 注]
*****************************************************************************/
void vSPI0_CLK_Up()
{
vSPI0_CLK(0);
Delay_Us(5);
vSPI0_CLK(1);
Delay_Us(5);
}
/*****************************************************************************
[函数名称]vSPI0_CLK_Down
[函数功能]模拟时钟下降沿
[备 注]
*****************************************************************************/
void vSPI0_CLK_Down()
{
vSPI0_CLK(1);
Delay_Us(5);
vSPI0_CLK(0);
Delay_Us(5);
}
/*****************************************************************************
[函数名称]vReadByte
[函数功能]字节读取数据函数
[备 注]
*****************************************************************************/
uint8 u8ReadByte_SPI0( void )
{
SPI0_CS_CLR; //开启SPI通信使能
uint8 u8RecBuf = 0;
for( uint8 j=0; j<8; j++ )
{
vSPI0_CLK_Down(); //下降沿接收数据
u8RecBuf <<= 1; //先进行数据移位,如果后进行移位的话,数据会多移一位
if( Read_SPI0_MISO == SET )
u8RecBuf |= 0x01;
}
SPI0_CS_SET; //关闭SPI通信使能
return u8RecBuf;
}
/*****************************************************************************
[函数名称]vSendByte
[函数功能]字节发送数据函数
[备 注]
*****************************************************************************/
void vSendByte_SPI0( uint8 sed )
{
SPI0_CS_CLR; //开启SPI通信使能
for( uint8 j=0; j<8; j++ )
{
if( sed & 0x80 ) //高位先发送
vSPI0_MOSI(1);
else
vSPI0_MOSI(0);
sed <<= 1;
vSPI0_CLK_Down();
}
SPI0_CS_SET; //关闭SPI通信使能
}
2、C++版本
该版本需要继承后,重构纯虚函数,将类内的SPI四个管脚定义链接到自己的管脚宏定义,之后就可以使用,适合多个SPI通信设备的应用场景。
.h头文件
class SoftSpi
{
public:
SoftSpi();
public:
virtual BOOL bInit_SoftSPI(void) = 0; //纯虚函数,继承重写,链接SPI管脚
inline BOOL bRead_MISO_Pin(void);
void vSet_CLK_Pin(uint8 sta);
void vSet_MOSI_Pin(uint8 sta);
void vCLK_UpEdge(void);
void vCLK_DownEdge(void);
uint8 u8Read_Byte_SPI(void);
void vSend_Byte_SPI(uint8 send);
public:
GPIO_TypeDef *CS_Port;
uint16_t CS_Pin;
GPIO_TypeDef *CLK_Port;
uint16_t CLK_Pin;
GPIO_TypeDef *MOSI_Port;
uint16_t MOSI_Pin;
GPIO_TypeDef *MISO_Port;
uint16_t MISO_Pin;
};
.cpp源文件
#include "softspi.h"
#define SPI0_CS_SET HAL_GPIO_WritePin(SPI0_CS_GPIO_Port, SPI0_CS_Pin, GPIO_Pin_SET)
#define SPI0_CS_CLR HAL_GPIO_WritePin(SPI0_CS_GPIO_Port, SPI0_CS_Pin, GPIO_Pin_SET)
inline void Delay_Us( int16 us )
{
while( us-- > 0 )
__nop();
}
SoftSpi::SoftSpi()
{
;
}
/*****************************************************************************
[函数名称]bRead_MISO_Pin
[函数功能]读MISO数据
[备 注]
*****************************************************************************/
inline BOOL SoftSpi::bRead_MISO_Pin(void)
{
return HAL_GPIO_ReadPin(MISO_Port, MISO_Pin);
}
/*****************************************************************************
[函数名称]vSet_CLK_Pin
[函数功能]设置时钟信号
[备 注]
*****************************************************************************/
void SoftSpi::vSet_CLK_Pin(uint8 sta)
{
if( sta == SET )
HAL_GPIO_WritePin(CLK_Port, CLK_Pin, GPIO_PIN_SET);
else
HAL_GPIO_WritePin(CLK_Port, CLK_Pin, GPIO_PIN_RESET);
}
/*****************************************************************************
[函数名称]vSet_MOSI_Pin
[函数功能]设置MOSI信号
[备 注]
*****************************************************************************/
void SoftSpi::vSet_MOSI_Pin(uint8 sta)
{
if( sta == SET )
HAL_GPIO_WritePin(MOSI_Port, MOSI_Pin, GPIO_PIN_SET);
else
HAL_GPIO_WritePin(MOSI_Port, MOSI_Pin, GPIO_PIN_RESET);
}
/*****************************************************************************
[函数名称]vCLK_UpEdge
[函数功能]时钟上升沿
[备 注]
*****************************************************************************/
void SoftSpi::vCLK_UpEdge()
{
vSet_CLK_Pin(0);
Delay_Us(5);
vSet_CLK_Pin(1);
Delay_Us(5);
}
/*****************************************************************************
[函数名称]vCLK_DownEdge
[函数功能]时钟下降沿
[备 注]
*****************************************************************************/
void SoftSpi::vCLK_DownEdge()
{
vSet_CLK_Pin(1);
Delay_Us(5);
vSet_CLK_Pin(0);
Delay_Us(5);
}
/*****************************************************************************
[函数名称]u8Read_Byte_SPI
[函数功能]字节读取函数
[备 注]
*****************************************************************************/
uint8 SoftSpi::u8Read_Byte_SPI(void)
{
SPI0_CS_CLR;
uint8 u8RecBuf = 0;
for(uint8 j=0; j<8; j++)
{
vCLK_DownEdge(); //下降沿接收数据
u8RecBuf <<= 1; //先进行数据移位,如果后进行移位的话,数据会多移一位
if(bRead_MISO_Pin() == SET)
u8RecBuf |= 0x01;
}
SPI0_CS_SET;
return u8RecBuf;
}
/*****************************************************************************
[函数名称]vSend_Byte_SPI
[函数功能]字节发送函数
[备 注]
*****************************************************************************/
void SoftSpi::vSend_Byte_SPI(uint8 send)
{
SPI0_CS_CLR;
for( uint8 j=0; j<8; j++ )
{
if( send & 0x80 ) //高位先发送
vSet_MOSI_Pin(1);
else
vSet_MOSI_Pin(0);
send <<= 1;
vCLK_DownEdge();
}
SPI0_CS_SET;
}

4655

被折叠的 条评论
为什么被折叠?



