通讯协议(2)—— SPI

本文详细介绍了SPI(串行外设接口)的基本概念、工作原理及四种工作模式的时序。SPI是一种高速、全双工、同步的通信总线,广泛应用于EEPROM、FLASH等设备间的数据交换。文中还提供了STM32和TM4C的SPI配置代码示例,包括引脚复用、时钟配置、数据传输等关键步骤。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

  • 小结关于spi的内容,附TM4C & stm32配置代码

一、SPI简介

  • 这里直接照搬正点原子的介绍词:SPI,是英语Serial Peripheral interface的缩写,顾名思义就是串行外围设备接口。是Motorola首先在其MC68HCXX系列处理器上定义的。SPI接口主要应用在 EEPROM,FLASH,实时时钟,AD转换器,还有数字信号处理器和数字信号解码器之间。SPI,是一种高速的,全双工,同步的通信总线,并且在芯片的管脚上只占用四根线,节约了芯片的管脚,同时为PCB的布局上节省空间,提供方便,正是出于这种简单易用的特性,现在越来越多的芯片集成了这种通信协议

  • spi是一种串行设备,需要时钟信号控制着数据传输。设备间有主机和从机之分,从机的时钟信号只能来自主机,因此主机必须存在

  • spi有四根连接线线“:MOSI是从机到主机的信号传输; MOSI是主机到从机的信号传输;SCLK是时钟线,控制数据传递过程;CS是片选线,主机通过它选定通讯对象,这允许一主多从连接

  • spi协议允许信号一位一位传输

  • spi的数据输入和输出线独立,所以允许同时完成数据的输入和输出。事实上这是一个数据交换协议,主从双方各有一个移位寄存器,主机向其移位寄存器传送一个数据来启动一次传输。主从机移位寄存器中的一个数据通过 MOSIMISO交换。因此如果想只读或只写一个数据,都需要传输无效数据,无效数据在收到后应被忽略
    在这里插入图片描述

  • spi的缺点:没有指定的流控制,没有应答机制确认是否接收到数据

二、硬件连接

  • spi有四根连接线
  1. SDO / MOSI:主机输出,从机输入
  2. SDI / MISO:主机输入,从机输出
  3. SCK / SCLK:时钟信号(由主机控制
  4. CS:从设备使能信号,即片选信号(由主机控制
  • 一主一从连接
    在这里插入图片描述
  • 一主多从连接
    在这里插入图片描述

三、四种工作模式的时序

  • SPI 模块为了和外设进行数据交换,根据外设工作要求,其输出串行同步时钟极性和相位可以进行配置。可配置的两个位为:
作用描述
CPOL时钟极性选择为0时SPI总线空闲为低电平,为1时SPI总线空闲为高电平,对传输协议没有重大的影响
CPHA时钟相位选择为0时在SCK第一个跳变沿采样,为1时在SCK第二个跳变沿采样
  • 就像前面那个动图显示的,SPI是一个环形总线结构,无论哪种工作方式,都可以看作是在sck的控制下,两个双向移位寄存器进行数据交换。
  • 根据配置,当sck的某个边沿到来时,主从双方移位寄存器中高位的值被采样到线上,寄存器移动一位,最低位暂时悬空,当下一个紧接着的边沿边沿到来时,线上的数据传送到双方移位寄存器低位,从而完成数据交换

(1)工作方式1

  • CPHA=0,CPOL=0;总线空闲为低电平,在SCK第一个跳变沿采样
  • 时序图: 在这里插入图片描述

(2)工作方式2

  • CPHA=0,CPOL=1;总线空闲为高电平,在SCK第一个跳变沿采样
  • 时序图:
    在这里插入图片描述

(3)工作方式3

  • CPHA=1,CPOL=0;总线空闲为低电平,在SCK第二个跳变沿采样
  • 时序图:
    在这里插入图片描述

(4)工作方式4

  • CPHA=1,CPOL=1;总线空闲为高电平,在SCK第二个跳变沿采样
  • 时序图:
    在这里插入图片描述

(5)注意

  • 在主设备这边配置SPI接口时钟的时候一定要弄清楚从设备的时钟要求,因为主设备这边的时钟极性和相位都是以从设备为基准的
  • 注意主SDO连接从SDI,主SDO的极性和从SDI相反,和从SDO相同。例如:从设备在时钟上升沿接收数据,则主设备应在下降沿输出数据

四、spi示例代码

(1)stm32f4标准库版本

  • 来自正点原子探索者例程
  • stm32作为主机,配置流程为:
  1. 配置相关引脚复用功能,使能spi时钟
  2. 初始化spi,配置spi工作模式
  3. 使能spi SPI_Cmd()
  4. 利用spi传输数据 SPI_I2S_SendData()SPI_I2S_ReceiveData()
  5. 查看spi状态 SPI_I2S_GetFlagStatus()
  • 以下为正点原子的代码
//以下是SPI模块的初始化代码,配置成主机模式 						  
//SPI口初始化
//这里针是对SPI1的初始化
void SPI1_Init(void)
{	 
  GPIO_InitTypeDef  GPIO_InitStructure;
  SPI_InitTypeDef  SPI_InitStructure;
	
  RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);				//使能GPIOB时钟
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);				//使能SPI1时钟
 
  //GPIOFB3,4,5初始化设置
  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3|GPIO_Pin_4|GPIO_Pin_5;	//PB3~5复用功能输出	
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;						//复用功能
  GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;					//推挽输出
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;				//100MHz
  GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;						//上拉
  GPIO_Init(GPIOB, &GPIO_InitStructure);							//初始化
	
	GPIO_PinAFConfig(GPIOB,GPIO_PinSource3,GPIO_AF_SPI1);			//PB3复用为 SPI1
	GPIO_PinAFConfig(GPIOB,GPIO_PinSource4,GPIO_AF_SPI1); 			//PB4复用为 SPI1
	GPIO_PinAFConfig(GPIOB,GPIO_PinSource5,GPIO_AF_SPI1); 			//PB5复用为 SPI1
 
	//这里只针对SPI口初始化
	RCC_APB2PeriphResetCmd(RCC_APB2Periph_SPI1,ENABLE);				//复位SPI1
	RCC_APB2PeriphResetCmd(RCC_APB2Periph_SPI1,DISABLE);			//停止复位SPI1

	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信号(CS)由硬件(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寄存器
 
	SPI_Cmd(SPI1, ENABLE); 			//使能SPI外设
	SPI1_ReadWriteByte(0xff);		//启动传输		 
}   

//SPI1速度设置函数
//SPI速度=fAPB2/分频系数
//@ref SPI_BaudRate_Prescaler:SPI_BaudRatePrescaler_2~SPI_BaudRatePrescaler_256  
//fAPB2时钟一般为84Mhz:
void SPI1_SetSpeed(u8 SPI_BaudRatePrescaler)
{
	assert_param(IS_SPI_BAUDRATE_PRESCALER(SPI_BaudRatePrescaler));	//判断有效性
	SPI1->CR1&=0XFFC7;												//位3-5清零,用来设置波特率
	SPI1->CR1|=SPI_BaudRatePrescaler;								//设置SPI1速度 
	SPI_Cmd(SPI1,ENABLE); 											//使能SPI1
} 

//SPI1 读写一个字节
//TxData:要写入的字节
//返回值:读取到的字节
u8 SPI1_ReadWriteByte(u8 TxData)
{		 			 
 
  	while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET){}	//等待发送区空  
	
	SPI_I2S_SendData(SPI1, TxData);				 						//通过外设SPIx发送一个byte  数据
		
  	while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET){} 	//等待接收完一个byte  
 
	return SPI_I2S_ReceiveData(SPI1);							 		//返回通过SPIx最近接收的数据	
 		    
}

(2)TM4C123版本

  • 来自匿名拓空者飞控源码
  • TM4C的封装度比stm32高一些,不过spi配置使用流程基本一样的
void Drv_Spi0Init(void)
{	
	ROM_SysCtlPeripheralEnable( SYSCTL_PERIPH_SSI0 );
	ROM_SysCtlPeripheralEnable(SPI0_SYSCTL);
	/*配置IO口*/	
	ROM_GPIOPinTypeSSI(SPI0_PROT,SPI0_CLK_PIN|SPI0_RX_PIN|SPI0_TX_PIN);
	ROM_GPIOPinConfigure(SPI0_CLK);	
	ROM_GPIOPinConfigure(SPI0_RX);
	ROM_GPIOPinConfigure(SPI0_TX);
	/* SSI配置 模式3(Polarity = 1 Phase = 1) 主设备模式 速率1MHz 数据长度8位*/
	ROM_SSIConfigSetExpClk(SPI0_BASE, ROM_SysCtlClockGet(), SSI_FRF_MOTO_MODE_3,  SSI_MODE_MASTER, 10000000,  8);
	/*开启SSI0*/
	ROM_SSIEnable(SPI0_BASE);
}
 
/* SPI读写函数 */
uint8_t Drv_Spi0SingleWirteAndRead(uint8_t SendData)
{
    uint32_t ui_TempData;
    uint8_t uc_ReceiveData;
    /* 向SSI FIFO写入数据 */
    ROM_SSIDataPut(SPI0_BASE, SendData);
    /* 等待SSI不忙 */
    while(ROM_SSIBusy(SPI0_BASE));
    /* 从FIFO读取数据 */
    ROM_SSIDataGet(SPI0_BASE, &ui_TempData);
    /* 截取数据的低八位 */
    uc_ReceiveData = ui_TempData & 0xff;
    return uc_ReceiveData;
}
 
void Drv_Spi0Transmit(uint8_t *ucp_Data, uint16_t us_Size)
{
    uint16_t i = 0;
    /* 连续写入数据 */
    for(i = 0; i < us_Size; i++)
    {
        Drv_Spi0SingleWirteAndRead(ucp_Data[i]);
    }
}
 
void Drv_Spi0Receive(uint8_t *ucp_Data, uint16_t us_Size)
{
    uint16_t i = 0;
    /* 连续读取数据 */
    for(i = 0; i < us_Size; i++)
    {
        ucp_Data[i] = Drv_Spi0SingleWirteAndRead(0xFF);
    }
}

### I2C与SPI通讯协议的比较 #### 通信方式 I2C采用两线制,即SDA(数据线)和SCL(时钟线),支持多主机和多从机模式,在同一总线上可以挂载多个设备[^3]。而SPI则至少需要四条线:MOSI(主输出/从输入)、MISO(主输入/从输出)、SCK(串行时钟)和CS(片选)。对于每增加一个从设备,则需额外一条CS线来区分不同从设备[^5]。 #### 数据传输效率 由于SPI具备全双工特性,能够在同一个周期内同时发送和接收数据,因此理论上具有更高的吞吐量;相比之下,I2C在同一时间内只能单向传递信息,并且存在应答机制,这使得它的有效带宽相对较低一些[^2]。 #### 地址管理 在I2C系统里,每个连接到总线上的器件都分配有一个唯一的7位或10位地址用于识别身份并防止冲突发生。当有新的节点加入网络时可能引起现有成员重新配置的情况。然而,在SPI架构下不存在这样的概念——通过单独设置各路芯片的选择信号即可轻松实现一对一或多对一的选择操作而不必担心地址重叠问题。 #### 成本考量 考虑到PCB布局布线复杂度以及所需引脚数量的不同,通常来说基于较少连线数目的I2C方案会更节省空间成本;但是若追求极致性能表现的话,则往往倾向于选择更为直接高效的SPI接口设计[^4]。 #### 应用实例分析 - **I2C典型用途** - 实现微控制器与其他外围传感器(如温度计、加速度计等)间简单的命令交互; - 构建小型嵌入式系统的内部组件互联框架。 - **SPI常见场合** - 高速存储器读写访问任务; - 数字音频处理器件间的实时数据交换; - 图形显示驱动程序加载等功能需求下的快速大量资料搬运作业。 ```python # Python伪代码展示两种协议初始化差异 class CommunicationProtocol: pass class I2CCommunication(CommunicationProtocol): def __init__(self, address=0x20): # 初始化指定目标设备地址 self.address = address class SPICommunication(CommunicationProtocol): def __init__(self, cs_pin=None): # 初始化时定义片选针脚编号 self.cs_pin = cs_pin ```
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

云端FFF

所有博文免费阅读,求打赏鼓励~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值