SPI-----软件模拟SPI

一、软件SPI

有关SPI的介绍我这里就不说了,可以去看我上一条博客,这里直接切入主题

1.四根通信线:SCK(时钟)、MOSI(主设备输出从设备输入)、MISO(主设备输入从设备输出)、SS(片选)

2.同步、全双工 接收数据线和发送数据线可同时工作

3.支持总线挂载多个设备,实现一主多从模式

4.所有SPI设备的SCK、MOSI、MISO、SS分别连在一起,主机另外引出多条SS控制线,分别接到各从机SS引脚

5.输出引脚配置为推挽输出,输入引脚配置为浮空输入

   🌕下图是 SPI 主从机的 MOSI 和 MISO 进行数据交换的示意图:

   🐠SPI 主机内部有一个波特率发生器,用来产生时钟SCK,波特率发生器产生时钟驱动主机的移位寄存器进行移位,同时产生时钟驱动从机的移位寄存器。

SPI 通讯是高位在前,     当 SCK 上升沿时,主机移位寄存器的最高位移至 MOSI 数据线上,从机的最低位移至 MISO 数据线上;      SCK 低电平期间,MOSI 数据线上的数据移至 SPI 从机,MISO 数据线上的数据移至 SPI 主机;以此完成一位的交换;

更通俗易懂点就是:

  • 第一次交换时,SCK高电平期间,主机的最高位1放到MOSI数据线上,从机的最低位1放到MISO数据线上,
  • SCK低电平期间,MOSI数据线上的数据移至从机的最低位,MISO数据线上的数据移至主机的最低位,
  • 这样经过8次循环,就可以完成一个字节的交换,并且每次循环都会将最低位的值逐步移到移位寄存器的最高位,保证了SPI高位在前的原则)

 🐠根据CPOL和CPHA的变化(CPOL和CPHA在STM32F4硬件驱动SPI有介绍),交换一个字节,可以产生4种模式:分别是模式0、模式1、模式2、模式3;

🌖这里模拟的时序是SPI的模式3(CPOL=1,CPHA=1),原因有两点:

1、模式3的SCK空闲电平为高,有高电平向低电平翻转较为容易和快,

2、模式3在偶数边沿采样,防止第一个信号没采到。

🌘首先,对于软件模拟SPI的GPIO初始化,可以参考SPI的GPIO初始化的配置,我们不用使用复用功能就行,使用普通推挽输出和浮空输入。

void SPI_GPIO_Config(void)
{
	GPIO_InitTypeDef GPIO_InitStruct;
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOC,ENABLE);
	/*SPI CS GPIO Confgi*/
	GPIO_InitStruct.GPIO_Mode	=	GPIO_Mode_Out_PP;
	GPIO_InitStruct.GPIO_Pin	=	EEPROM_SPI_CS_PIN;
	GPIO_InitStruct.GPIO_Speed	=	GPIO_Speed_50MHz;
	GPIO_Init(EEPROM_SPI_CS_PORT,&GPIO_InitStruct);
	
	/*SPI SCK GPIO Config*/
	GPIO_InitStruct.GPIO_Mode	=	GPIO_Mode_Out_PP;
	GPIO_InitStruct.GPIO_Pin	=	EEPROM_SPI_CLK_PIN;
	GPIO_InitStruct.GPIO_Speed	=	GPIO_Speed_50MHz;
	GPIO_Init(EEPROM_SPI_CLK_PORT,&GPIO_InitStruct);
	
	/*MISO GPIO Config*/
	GPIO_InitStruct.GPIO_Mode	=	GPIO_Mode_IN_FLOATING;
	GPIO_InitStruct.GPIO_Pin	=	EEPROM_SPI_MISO_PIN;
	GPIO_Init(EEPROM_SPI_MISO_PORT,&GPIO_InitStruct);
	
	/*MOSI GPIO Config*/
    GPIO_InitStruct.GPIO_Mode	=	GPIO_Mode_Out_PP;
	GPIO_InitStruct.GPIO_Pin	=	EEPROM_SPI_MOSI_PIN;
	GPIO_InitStruct.GPIO_Speed	=	GPIO_Speed_50MHz;
	GPIO_Init(EEPROM_SPI_MOSI_PORT,&GPIO_InitStruct);
 
	EEPROM_SPI_CS_HIGH(); //位选拉高
	EEPROM_SPI_CLK_HIGH();//时钟线拉高
}

🌑接下来就是最为关键的发送和接收数据函数了,

  • 其中注意,使用SPI时,发送和接收其实是同一个函数,通过判断RXNE来确认发送结束,此时也接收完数据,接收数据同样要主机产生时序,时序通过主机发送数据产生,所以会发送无用的Dummy数据。
  • 软件模拟不需要,直接接收自己本身会产生时序,不用发送DUMMY数据。Delay函数随便设置,不要小于手册的时间即可。 

软件模拟发送数据

void SPI_SendData(uint8_t data)
{
	uint8_t cnt;
	
	for(cnt=0;cnt<8;cnt++)
	{
		EEPROM_SPI_CLK_LOW();//拉低CLK
		SPI_Delay(10);//这个延时时间任意,但要大于芯片数据手册上的(纳秒级的)
		if(data &0x80)
		{
			EEPROM_SPI_MOSI_HIGH();
		}
		else
		{
			EEPROM_SPI_MOSI_LOW();
		}
		data <<= 1;
		EEPROM_SPI_CLK_HIGH();//拉高CLK
		SPI_Delay(10);
	}
 
}

软件模拟接收数据

uint8_t SPI_ReadData(void)
{
	uint8_t i = 0;
	uint8_t value=0;
	for(i=0;i<8;i++)
	{
		EEPROM_SPI_CLK_LOW();
		SPI_Delay(10);
		value<<=1;
		if(EEPROM_SPI_MISO())
		{
			value |= 0x01;
		}
		EEPROM_SPI_CLK_HIGH();
		SPI_Delay(10);
	}
	return value;
}

这个是模拟的连接W25Qxx的读写数据,接下来看下模式0的读写

二、基于模式0的模拟SPI---

🌎交换一个字节(模式0)

        CPOL=0:空闲状态下,SCK为低电平

        CPHA=0:SCK第一个边沿移入数据,第二个边沿移出数据(在硬件驱动SPI时,将这一过程称作奇数边沿采样或者偶数边沿采样)

//通讯开始
void MySPI_Start(void)
{
    GPIO_WriteBit(GPIOB,GPIO_Pin_6,Bit_RESET);//片选信号线拉低,表示SPI通讯的开始
}
//通讯结束
void MySPI_Stop(void)
{
    GPIO_WriteBit(GPIOB,GPIO_Pin_6,Bit_SET);//片选信号线拉高,表示SPI通讯的结束
//交换一个字节,SPI是全双工通讯,MOSI和MISO同时进行工作,
也就是发送和接收是同时开始的,所以SPI的通讯速率比IIC要快
unsigned char MySPI_SwapByte(unsigned char ByteSend)
{
    //交换的原则就是根据SCK高低电平的变换,将MOSI和MISO数据线上数据进行移入和移出
    unsigned char i,ByteReceive = 0x00;
    for(i=0;i<8;i++)
    {
//初始化SCK低电平,所以SCK低电平期间,主机取出要发送数据的最高位放在MOSI数据线上
        GPIO_WriteBit(GPIOB,GPIO_Pin_9,(ByteSend&(0x80>>i)));
 
        GPIO_WriteBit(GPIOB,GPIO_Pin_8,Bit_SET); //SCK置高电平
//因为SPI发送和接收是同步进行的,所以主机将数据放至MOSI数据线的同时,从机也将数据放至MISO数据线上了
        if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_7)==1) 
//此时读取MISO数据线的电平,因为PA7设置上拉输入,所以若PA7引脚为高电平,则表示有数据交换
        {
//将接收的这一位放至变量ByteReceive的最高位,依次移位,获取完成的一个字节
            ByteReceive = ByteReceive | (0x80>>i);
        }
        GPIO_WriteBit(GPIOB,GPIO_Pin_8,Bit_RESET); //SCK置低电平,完成一位的发送接收
    }
    return ByteReceive;
//这里解释一下为什么只判断了主机的电平变换,因为SPI可以设置主发送主接收、主发送从接收、主接收从发送;
    //所谓设置模式,简单来说就是,你可以发送,但是我不看你发送的内容就是了;
}

上面的三个函数分别要对应的通讯开始和通讯结束,,然后是字节的发送和接收

初始化的代码就不说了,大家参照头部的解说就行,主要是发送数据和接收数据部分重要

接下来在看下实例,

1.先配置4根通信线

先初始化4根通信线,给它们配置成相应的状态函数

void MySPI_Init(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_7;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	MySPI_W_SS(1);  //初始化好给片选引脚拉高
	MySPI_W_SCK(0);  //时钟引脚拉低
}
 
void MySPI_W_SS(uint8_t BitValue)  //片选高低电平配置函数
{
	GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue);
}
 
void MySPI_W_SCK(uint8_t BitValue)   //时钟高低电平配置函数
{
	GPIO_WriteBit(GPIOA, GPIO_Pin_5, (BitAction)BitValue);
}
 
void MySPI_W_MOSI(uint8_t BitValue)  //主机输出引脚高低电平配置函数
{
	GPIO_WriteBit(GPIOA, GPIO_Pin_7, (BitAction)BitValue);
}
 
uint8_t MySPI_R_MISO(void)   主机输入引脚读取高低电平
{
	return GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6);
}

 2.起始和终止信号

按照起始和终止时序图写出模拟的高低电平

void MySPI_Start(void)
{
	MySPI_W_SS(0);
}
 
void MySPI_Stop(void)
{
	MySPI_W_SS(1);
}

3.交换一个字节

主机发送一个字节,并且从从机得到一个字节

uint8_t MySPI_SwapByte(uint8_t ByteSend)
{
	uint8_t i, ByteReceive = 0x00;
	
	for (i = 0; i < 8; i ++)
	{
		MySPI_W_MOSI(ByteSend & (0x80 >> i));
		MySPI_W_SCK(1);
		if (MySPI_R_MISO() == 1){ByteReceive |= (0x80 >> i);}
		MySPI_W_SCK(0);
	}
	
	return ByteReceive;
}

上面就是用GPIO口模拟一个SPI通信的时序用于主机发送或接收一个字节,具体的某款芯片对应的SPI通信要根据芯片手册来看给芯片发送什么字节或者我们需要接收什么字节,但是上面是SPI通信的基础有了这个就可以进行我们想要的接收和发送数据了,这样才能进行接下来的通信,

简单的步骤就是

(1)配置GPIO工作模式,/CS,CLK,MOSI引脚配置为输出模式,MISO配置为输入模式。

(2)编写模拟SPI函数。在使用模拟SPI时,选择了模式0,时钟极性为0,时钟相位为0。时钟空闲电平为低电平,上升沿捕获数据。

  • 定义一个无符号字符类型的变量data,用于保存接收到的数据。
  • 拉低时钟管脚。
  • for循环中循环8次,读取或发送一个字节。
  • 上升沿时捕获数据,所以在时钟管脚低电平时输出高电平或低电平,使其在上升沿到来之前保持稳定。这里使用三目运算符获取要发送字节的最高位。并将低以为左移至最高位,便于下一位传输。
  • 拉高时钟管脚。
  • 在时钟管脚为高电平时读取数据。并移位至对应位。
  • 拉低时钟为下一次上升沿到来做准备,也可以在最后以为数据传输完成后保持时钟为高电平。

这里用W25Qxx来使用SPI发送指令 

W25Q64模块代码

🐶1.写使能

void W25Qxx_Write_Enable(void)
{
	SPI_Start();//拉低
	SPI_SwapByte(W25Q64_WRITE_ENABLE);//发送获取ID指令
	SPI_Stop();//拉高
}

🐶2.忙等待

void W25Qxx_WaitBusy(void) 
{
	uint32_t TimeOut = 100000;
	
	W25Qxx_Write_Enable();//写使能
	
	SPI_Start();
	SPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1);				//读取寄存器状态
	while((SPI_SwapByte(W25Q64_DUMMY_BYTE) & 0x01) == 0x01)	//最低位1:忙;0:空闲!!!!!!!这个地方写错了!!!!!!!!!!!!
	{
		if(--TimeOut == 0)										//防止程序卡死
			break;
	}
	SPI_Stop();
}

🐶3.擦除

void W25Qxx_SectorEreza(uint32_t Address)//擦除函数  每次写入的时候提前擦除下内存 否则写入数据出错(只能1写0 不能0写1 相当于&)
{
	W25Qxx_Write_Enable();//写使能
	SPI_Start();//开始
	SPI_SwapByte(W25Q64_SECTOR_ERASE_4KB);//发送指令
	SPI_SwapByte(Address >> 16);
	SPI_SwapByte(Address >> 8);
	SPI_SwapByte(Address);
	SPI_Stop();
	
	W25Qxx_WaitBusy();//等待
}

。。。。。。。。。。。。。。。。

。。。。。。。。。。。。。。。。

无非就是拉低发送数据,拉高读取数据,只是发送数据的方式不同,

有关W25Qxx的函数在W25Qxx那章的博客会详细讲,现在只是详细说下软件模拟SPI。

<think>好的,我现在需要详细解释软件实现的SPI协议。首先,用户已经提供了一些关于硬件SPI的引用资料,我需要结合这些内容来对比说明软件SPI的不同之处。 首先,SPI的基本概念。根据引用[3],SPI是一种高速、全双工、同步的串行通信总线,使用主从架构,包含SCLK、MOSI、MISO、CS四个信号线。硬件SPI通常由微控制器内置的专用模块处理,而软件SPI则需要通过GPIO模拟这些信号的时序。 接下来,软件SPI的核心在于用程序控制GPIO引脚的电平变化,手动实现时钟信号和数据传输。这涉及到时序的精确控制,比如时钟极性和相位(CPOL和CPHA),不同的模式需要不同的时序方式。例如,当CPOL=0时,时钟空闲状态为低电平,CPHA=0表示在时钟的第一个边沿采样数据。 然后,我需要考虑软件SPI的实现步骤。首先配置GPIO引脚为输出模式,控制CS、SCLK、MOSI,而MISO可能需要配置为输入。接着,根据选定的SPI模式,编写数据传输函数。例如,发送一个字节时,逐位在时钟的上升沿或下降沿改变MOSI的状态,并在对应的边沿读取MISO的数据。 软件SPI的优缺点也需要分析。优点包括灵活性高,不依赖硬件模块,适用于没有硬件SPI的微控制器;缺点则是速度较慢,占用CPU资源,时序控制复杂,容易受中断干扰。 应用场景方面,软件SPI常用于低速设备或资源受限的系统,比如驱动简单的传感器、显示屏,或者在需要多个SPI接口时扩展数量。例如,引用[2]提到SPI用于单片机与外设通信,软件SPI可以扩展这种能力。 最后,根据用户要求,生成相关问题,比如软件SPI与硬件SPI的对比、优化方法、常见问题等。需要确保问题覆盖用户可能感兴趣的方面,并参考提供的引用资料中的内容,比如引用[3]提到的SPI速度优势在软件实现中的限制。 总结来说,软件SPI的实现需要手动控制GPIO时序,理解SPI的模式和参数,权衡灵活性与性能。通过代码示例和步骤分解,用户可以更好地掌握其工作原理和应用方法。</think>### 软件实现的SPI协议详解 #### 1. 软件SPI的核心概念 软件SPI(Software SPI)是通过通用输入输出(GPIO)引脚和程序逻辑模拟SPI协议的通信方式。与硬件SPI(依赖专用外设模块)不同,软件SPI完全由CPU控制时序,灵活性高但传输速率较低[^3]。 **关键参数**: - **时钟极性(CPOL)**:空闲状态下SCLK的电平(0=低电平,1=高电平)。 - **时钟相位(CPHA)**:数据采样的边沿(0=第一个边沿,1=第二个边沿)。 - **数据传输模式**:全双工(同时收发)或半双工(分时收发)。 - **时钟频率**:由程序延时控制,通常低于硬件SPI。 #### 2. 实现步骤与代码示例 以下以CPOL=0、CPHA=0模式为例,展示软件SPI发送一个字节的流程: ```python # 定义GPIO引脚(以树莓派为例) import RPi.GPIO as GPIO CS = 8 SCLK = 11 MOSI = 10 MISO = 9 def spi_init(): GPIO.setmode(GPIO.BCM) GPIO.setup(CS, GPIO.OUT) GPIO.setup(SCLK, GPIO.OUT) GPIO.setup(MOSI, GPIO.OUT) GPIO.setup(MISO, GPIO.IN) GPIO.output(CS, GPIO.HIGH) # 初始时CS为高电平 def spi_write_byte(data): GPIO.output(CS, GPIO.LOW) # 片选使能 for i in range(7, -1, -1): # 高位先发 bit = (data >> i) & 0x01 GPIO.output(MOSI, bit) # 设置MOSI电平 GPIO.output(SCLK, GPIO.HIGH) # 上升沿触发从机采样 GPIO.output(SCLK, GPIO.LOW) # 下降沿准备下一次数据 GPIO.output(CS, GPIO.HIGH) # 传输结束 ``` #### 3. 软件SPI的优缺点 | **优点** | **缺点** | |-----------------------------|-----------------------------| | 无需专用硬件模块,节省芯片资源 | 传输速率低(通常<1MHz) | | 可自定义时序,适配非标设备 | 占用CPU资源,影响实时性 | | 支持多从机扩展(通过多CS引脚) | 时序精度受程序延迟影响 | #### 4. 应用场景 - **低速外设驱动**:如温度传感器(DS18B20)、OLED屏幕(SSD1306)[^2]。 - **硬件资源受限系统**:如低成本单片机(ATtiny系列)。 - **多SPI通道扩展**:当硬件SPI通道不足时,通过软件模拟额外通道。 ---
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值