前言

当我们发现STM32上的硬件SPI引脚已被占用时,为了实现SPI通信功能,我们可以转而采用软件SPI作为替代方案。然而,需要注意的是,相较于硬件SPI,软件SPI的性能可能会受到一定程度的影响,因为软件实现需要更多的CPU周期和资源来模拟硬件的行为。尽管如此,在硬件资源受限或特定应用需求下,软件SPI仍然是一个可行的选择。

SPI基本原理

SPI是串行外设接口(Serial Peripheral Interface)的缩写,是一种由Motorola(摩托罗拉)公司推出并发展的同步串行接口技术。它主要应用于嵌入式系统中的短距离通信,是一种高速的、全双工、同步的通信总线。

SPI通信协议具有高速传输、简单灵活、可靠稳定等特点,在各种嵌入式系统中得到广泛应用。SPI通信协议由四根信号线组成,包括时钟线(SCK)、主端输出从端输入线(MOSI)、主端输入从端输出线(MISO)和片选线(SS)。其中,时钟线由主设备控制,用于同步数据传输;MOSI和MISO分别用于主设备向从设备发送数据和从设备向主设备发送数据;片选线用于选择从设备,可以有多个从设备,通过片选线来选择具体的从设备进行通信。

SPI设备之间采用全双工模式通信,是一个主机和一个或者多个从机的主从模式。主机负责初始化帧,这个数据传输帧可以用于读与写两种操作,片选线可以从多个从机选择一个来响应主机的请求。

然而,SPI也有其缺点,比如没有指定的流控制,没有应答机制确认是否接收到数据,因此在数据传输的可靠性上存在一些缺陷。

总的来说,SPI是一种非常实用的串行通信协议,尤其适用于连接微控制器和外围设备,如存储器芯片、传感器、显示屏等。

我这里进行一个简单的描述,具体的原理有人写的比我更好

文章:SPI原理超详细讲解---值得一看

STM32软件SPI实现步骤(主机模式)

选择合适的GPIO引脚模拟SPI接口

我这里使用的是STM32CubeMX来生成代码,使用的芯片是STM32F103RCT6,但是未使用CubeMX来配置引脚,所以我这里将给出代码。引脚分配如下(我这里的应用是驱动LCD,所以默认使用主机模式):

SCL=SCLK

SDA=MOSI

RST

CS

PB4

PB5

PB6

PB8

代码如下(这里我的应用是驱动LCD,所以命名的就是LCD):

#define LCD_SCL_PORT GPIOB
#define LCD_SCL_PIN GPIO_PIN_4

#define LCD_SDA_PORT GPIOB
#define LCD_SDA_PIN GPIO_PIN_5

#define LCD_RST_PORT GPIOB
#define LCD_RST_PIN GPIO_PIN_6

#define LCD_DC_PORT GPIOB
#define LCD_DC_PIN GPIO_PIN_7

#define LCD_CS_PORT GPIOB
#define LCD_CS_PIN GPIO_PIN_8

#define LCD_SCL_H HAL_GPIO_WritePin(LCD_SCL_PORT,LCD_SCL_PIN,GPIO_PIN_SET);
#define LCD_SCL_L HAL_GPIO_WritePin(LCD_SCL_PORT,LCD_SCL_PIN,GPIO_PIN_RESET);

#define LCD_SDA_H HAL_GPIO_WritePin(OLED_SDA_PORT,LCD_SDA_PIN,GPIO_PIN_SET);
#define LCD_SDA_L HAL_GPIO_WritePin(OLED_SDA_PORT,LCD_SDA_PIN,GPIO_PIN_RESET);

#define LCD_RST_H HAL_GPIO_WritePin(LCD_RST_PORT,LCD_RST_PIN,GPIO_PIN_SET);
#define LCD_RST_L HAL_GPIO_WritePin(LCD_RST_PORT,LCD_RST_PIN,GPIO_PIN_RESET);

#define LCD_DC_H HAL_GPIO_WritePin(LCD_DC_PORT,LCD_DC_PIN,GPIO_PIN_SET);
#define LCD_DC_L HAL_GPIO_WritePin(LCD_DC_PORT,LCD_DC_PIN,GPIO_PIN_RESET);

#define LCD_CS_H HAL_GPIO_WritePin(LCD_CS_PORT,LCD_CS_PIN,GPIO_PIN_SET);
#define LCD_CS_L HAL_GPIO_WritePin(LCD_CS_PORT,LCD_CS_PIN,GPIO_PIN_RESET);

#define LCD_BL_H HAL_GPIO_WritePin(LCD_BL_PORT,LCD_BL_PIN,GPIO_PIN_SET);
#define LCD_BL_L HAL_GPIO_WritePin(LCD_BL_PORT,LCD_BL_PIN,GPIO_PIN_RESET);

编写软件SPI的初始化代码

首先对引脚的初始化

void LCD_GPIO_Init(void) {
  GPIO_InitTypeDef GPIO_InitStruct = {0};

  __HAL_RCC_GPIOB_CLK_ENABLE();

  GPIO_InitStruct.Pin = LCD_SCL_PIN | LCD_SDA_PIN | LCD_BL_PIN | LCD_CS_PIN | LCD_RST_PIN | LCD_DC_PIN;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;

  HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
}

对引脚的初始化完毕之后,那就是实现基本的函数,接收和发送函数

实现SPI数据的发送和接收功能

发送函数

void SPI_Transmit(uint8_t byte) {
  uint16_t counter;
  LCD_CS_L;
  for (counter = 0; counter < 8; counter++) {
	LCD_SCL_L;
	if ((byte & 0x80) == 0) {
	  LCD_SDA_L;
	} else
	  LCD_SDA_H;
	byte = byte << 1;
	LCD_SCL_H;
  }
  LCD_CS_H;
}

接受函数(使用接受函数,需要将SDA引脚模式改为输入模式)

uint8_t SPI_ReceiveByte(){
	uint8_t i;
	uint8_t byte = 0;
	for(i = 7;i >= 0; i--){
		LCD_SCL_L;
		if(HAL_GPIO_ReadPin(LCD_SDA_PORT,LCD_SDA_PIN)){
			byte |= (1 << i);
		}
		LCD_SCL_H;
	}
	return byte;
}

以上就是实现SPI发送和接受的基本功能函数。

实际应用SPI驱动LCD屏幕

直接复制代码会显得文章特别长,所以我这里使用了代码小抄来分享我的代码,这里需要说明的是我LCD屏幕是1.4寸的,它的驱动芯片为ST7735S

LCD.c->https://codecopy.cn/post/gfio1s

LCD.h->https://codecopy.cn/post/ezgqo8

lcdfont.h->https://codecopy.cn/post/ky0cam

总结

虽然软件SPI相比于硬件SPI的性能比较弱,但是可以为我们解决引脚占用的问题,它的灵活性也比较高,可以不限特定的硬件引脚。但是它的缺点也比较明显,就是它的传输速度较低,无法满足高速的传输需求,将会增加处理的负载,影响系统的整体性能。所以可以使用硬件SPI的情况下,则还是推荐使用硬件SPI为宜。

如果有不对的地方,欢迎指正!