SPI通信(软件/硬件)

本文详细介绍了SPI通信协议,包括其由Motorola开发的背景,四根通信线的作用,同步全双工特性和SS的使用。还涉及了STM32中SPI的硬件配置、软件编程以及与I2C的比较。
摘要由CSDN通过智能技术生成

SPI通信

SPI(Serial Peripheral Interface)是由Motorola公司开发的一种通用数据总线

四根通信线:SCK(Serial Clock)、MOSI(Master Output Slave Input)、MISO(Master Input Slave Output)、SS(Slave Select)

同步,全双工 支持总线挂载多设备(一主多从)

没有应答机制的设计

所有SPI设备的SCK、MOSI、MISO分别连在一起

主机另外引出多条SS控制线,分别接到各从机的SS引脚,设置为推挽输出。

输出引脚配置为推挽输出,输入引脚配置为浮空或上拉输入,SCK对主机来说是输出,对从机是输入,也设置成推挽输出。

当从机的SS引脚为高电平也就是从机未被选中时,他的MISO必须切换为高阻态。防止一条线有多个输出造成电平冲突。当SS为低电平MISO设置成推挽,当然这些都是在从机里实现的,所以只需要把引脚设置为浮空或者上拉输入。

(I2C开漏外加弱上拉电阻的电路结构,使得通信线高电平恶的驱动能力比较弱。当SDA由低到高的时候上升沿耗时长。所以I2C标准模式只有100khz的时钟频率,快速模式只有400khz。I2C之后又通过改进电路的方式,设计出了高速模式。速度达到3.4Mhz,但是普及率不太高)

SPI移位示意图

高位先行。可以理解为来一个边沿时把数据放到线上,再来一个边沿把数据从线上拿到寄存器。这样对应了移出、移入。


SPI时序

模式0,当SS下降沿时数据紧接着就移出了。这样SCK的第一个上升沿就可以把数据移入了。在代码实现时许图时,先写上升下降沿,再对数据进行操作(并不是严格的同时)。最后一个SCK下降沿,从机发送的是下一个字节的B7。如果想连续交换个字节就不拉高SS,重复上述时许即可。

在SS的上升沿 MOSI可以置高低电平,但是SPI并没有规定MOSI默认电平,所以没有必要。

软件代码

/*引脚配置层*/

/**
  * 函    数:SPI写SS引脚电平
  * 参    数:BitValue 协议层传入的当前需要写入SS的电平,范围0~1
  * 返 回 值:无
  * 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SS为低电平,当BitValue为1时,需要置SS为高电平
  */
void MySPI_W_SS(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue);		//根据BitValue,设置SS引脚的电平
}

/**
  * 函    数:SPI写SCK引脚电平
  * 参    数:BitValue 协议层传入的当前需要写入SCK的电平,范围0~1
  * 返 回 值:无
  * 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SCK为低电平,当BitValue为1时,需要置SCK为高电平
  */
void MySPI_W_SCK(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOA, GPIO_Pin_5, (BitAction)BitValue);		//根据BitValue,设置SCK引脚的电平
}

/**
  * 函    数:SPI写MOSI引脚电平
  * 参    数:BitValue 协议层传入的当前需要写入MOSI的电平,范围0~0xFF
  * 返 回 值:无
  * 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置MOSI为低电平,当BitValue非0时,需要置MOSI为高电平
  */
void MySPI_W_MOSI(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOA, GPIO_Pin_7, (BitAction)BitValue);		//根据BitValue,设置MOSI引脚的电平,BitValue要实现非0即1的特性
}

/**
  * 函    数:I2C读MISO引脚电平
  * 参    数:无
  * 返 回 值:协议层需要得到的当前MISO的电平,范围0~1
  * 注意事项:此函数需要用户实现内容,当前MISO为低电平时,返回0,当前MISO为高电平时,返回1
  */
uint8_t MySPI_R_MISO(void)
{
	return GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6);			//读取MISO电平并返回
}

/**
  * 函    数:SPI初始化
  * 参    数:无
  * 返 回 值:无
  * 注意事项:此函数需要用户实现内容,实现SS、SCK、MOSI和MISO引脚的初始化
  */
void MySPI_Init(void)
{
	/*开启时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);	//开启GPIOA的时钟
	
	/*GPIO初始化*/
	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);					//将PA4、PA5和PA7引脚初始化为推挽输出
	
	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);					//将PA6引脚初始化为上拉输入
	
	/*设置默认电平*/
	MySPI_W_SS(1);											//SS默认高电平
	MySPI_W_SCK(0);											//SCK默认低电平
}

/*协议层*/

/**
  * 函    数:SPI起始
  * 参    数:无
  * 返 回 值:无
  */
void MySPI_Start(void)
{
	MySPI_W_SS(0);				//拉低SS,开始时序
}

/**
  * 函    数:SPI终止
  * 参    数:无
  * 返 回 值:无
  */
void MySPI_Stop(void)
{
	MySPI_W_SS(1);				//拉高SS,终止时序
}

/**
  * 函    数:SPI交换传输一个字节,使用SPI模式0
  * 参    数:ByteSend 要发送的一个字节
  * 返 回 值:接收的一个字节
  */
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{
	uint8_t i, ByteReceive = 0x00;					//定义接收的数据,并赋初值0x00,此处必须赋初值0x00,后面会用到
	
	for (i = 0; i < 8; i ++)						//循环8次,依次交换每一位数据
	{
		MySPI_W_MOSI(ByteSend & (0x80 >> i));		//使用掩码的方式取出ByteSend的指定一位数据并写入到MOSI线
		MySPI_W_SCK(1);								//拉高SCK,上升沿移出数据
		if (MySPI_R_MISO() == 1){ByteReceive |= (0x80 >> i);}	//读取MISO数据,并存储到Byte变量
																//当MISO为1时,置变量指定位为1,当MISO为0时,不做处理,指定位为默认的初值0
		MySPI_W_SCK(0);								//拉低SCK,下降沿移入数据
	}
	
	return ByteReceive;								//返回接收到的一个字节数据
}

SPI外设

STM32内部集成了硬件SPI收发电路,可以由硬件自动执行时钟生成、数据收发等功能,减轻CPU的负担

可配置8位/16位数据帧(发两个字节)、高位先行/低位先行(基本都是高位先行)

时钟频率: fPCLK / (2, 4, 8, 16, 32, 64, 128, 256) 支持多主机模型、主或从操作

可精简为半双工/单工通信

支持DMA

兼容I2S协议 STM32F103C8T6 硬件SPI资源:SPI1、SPI2 (SPI1挂载在APB2,PCLK是72Mhz,SPI2挂载在APB1,PCLK是36Mhz)

LSB控制位是用来控制高位/低位先行的·。寄存器可以去找数据手册看。接受/发送缓冲区(RDR/TDR)跟串口一样是占同一个地址DR 。当TDR数据转入移位寄存器时,置TXE为1,表示发送缓冲区空,下一个数据就会进入DR。当移入的数据进入RDR时,RXNE为1,表示接收缓冲区非空,读出RDR。

对性能要求很高的话可以选择这个,比较少用。

一般都会选择非连续传输发送。BSY=0 表示发送完成。这个模式就是让MOSI和MISO处理完后再把数据放到发送缓冲器。但是缺点是有空隙,没有及时把数据写入TDR。在SCLK慢的时候间隙影响不大。但在SCLK频率非常高的时候影响就明显了,甚至空隙的时间比传输数据的时间要长。

#include "stm32f10x.h"                  // Device header

/*
看板子的引脚图,找到SPI外设对应的引脚
SS信号线可以继续选择用软件模拟的方式

常用库函数
void SPI_I2S_DeInit(SPI_TypeDef* SPIx);
void SPI_Init(SPI_TypeDef* SPIx, SPI_InitTypeDef* SPI_InitStruct);
void SPI_StructInit(SPI_InitTypeDef* SPI_InitStruct);
void SPI_Cmd(SPI_TypeDef* SPIx, FunctionalState NewState);
void SPI_I2S_ITConfig(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT, FunctionalState NewState);
void SPI_I2S_DMACmd(SPI_TypeDef* SPIx, uint16_t SPI_I2S_DMAReq, FunctionalState NewState);

void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data);   写DR寄存器
uint16_t SPI_I2S_ReceiveData(SPI_TypeDef* SPIx);  	读DR

	
标志位	
FlagStatus SPI_I2S_GetFlagStatus(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG);
void SPI_I2S_ClearFlag(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG);
ITStatus SPI_I2S_GetITStatus(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT);
void SPI_I2S_ClearITPendingBit(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT);


*/

void MySPI_W_SS(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOA,GPIO_Pin_4,(BitAction)BitValue);
}

void MySPI_Init(void)
{
	
	/*
	第一步 开启 SPI GPIO时钟
	第二步 初始化GPIO口SCK  MOSI复用推挽输出 MISO上拉输入 SS配置为通用推挽输出(软件实现)
	*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1,ENABLE);
	
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;
	GPIO_InitStructure.GPIO_Pin=GPIO_Pin_4;
	GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStructure);
	
	
	GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF_PP;
	GPIO_InitStructure.GPIO_Pin=GPIO_Pin_5|GPIO_Pin_7;
	GPIO_Init(GPIOA,&GPIO_InitStructure);
	
	
	GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IPU;
	GPIO_InitStructure.GPIO_Pin=GPIO_Pin_6;
	GPIO_Init(GPIOA,&GPIO_InitStructure);
	
	SPI_InitTypeDef SPI_InitStructure;
	SPI_InitStructure.SPI_Mode=SPI_Mode_Master;		//指定当前设备为从机
	SPI_InitStructure.SPI_Direction=SPI_Direction_2Lines_FullDuplex;	//双线全双工
	SPI_InitStructure.SPI_DataSize=SPI_DataSize_8b;		//8位数据帧
	SPI_InitStructure.SPI_FirstBit=SPI_FirstBit_MSB;	//高位先行
	SPI_InitStructure.SPI_BaudRatePrescaler=SPI_BaudRatePrescaler_128;	//SCK的时钟频率  72Mhz/128
	SPI_InitStructure.SPI_CPOL=SPI_CPOL_Low;
	SPI_InitStructure.SPI_CPHA=SPI_CPHA_1Edge;	//第一个边沿开始采样,可以理解为1Edge=0 2Edge=1
	SPI_InitStructure.SPI_NSS=SPI_NSS_Soft;			//软件实现NSS	这个不需要特别关心 一般不用
	SPI_InitStructure.SPI_CRCPolynomial=7;		//CRC校验  基本不用填默认值7
	
	SPI_Init(SPI1,&SPI_InitStructure);
	
	SPI_Cmd(SPI1,ENABLE);
	
	MySPI_W_SS(1);
	
}

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

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

uint8_t MySPI_SwapByte(uint8_t ByteSend)
{
	/*
	第一步:等待TXE为1 发送寄存器为空
	第二步:软件写入至DR
	第三步:等待标志位RXNE
	第四步:读取DR
	*/
	while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_TXE)!=SET);
	
	SPI_I2S_SendData(SPI1,ByteSend);		//自动转入移位寄存器 自动发送
	
	//非连续发送 所以不必在TXE==1时立刻写入下一个数据到DR
	
	while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_RXNE)!=SET);
	
	return SPI_I2S_ReceiveData(SPI1);

    //标志位TXE在写入DR时会自动被清除 读DR时RXNE会自动被清楚
}

  • 23
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值