SPI通信协议讲解

本文详细介绍了SPI通信协议,包括物理层和协议层的细节,并以STM32为例,阐述了SPI的器件安装、配置以及与W25Q64芯片的通信过程,包括读取ID、擦除、写入和读取数据的操作。

实验准备

一块STM32最小系统板,W25Q64模块,一块PCB转换板模块、串口TTL转USB模块
图片:

在这里插入图片描述

器件安装

在这里插入图片描述

SPI通信概念

前面已经学习过了I2C通信,了解了I2C通信协议大概,弄懂了I2C通信那么相信你也可以轻松弄懂的,因为I2C和SPI之间有一些共同点。I2C通信的通信速率最高只有400Kbit/s,而SPI通信最高36Mbit/s乃至更高,可以说是直接碾压IIC。单片机一般也用不到这么快的速度,所以一般用于ADC、LCD等设备与MCU间,要求通讯速率较高的情景。

SPI通信

SPI 物理层

spi系统通信图

在这里插入图片描述

SPI 通讯使用 3 条总线及片选线, 3 条总线分别为 SCK、 MOSI、 MISO,片选线为NSS
,它们的作用介绍如下:

  1. SS (Slave Select):从设备选择信号线,常称为片选信号线,也称为 NSS、 CS,以下用 NSS 表示。 当有多个 SPI 从设备与 SPI 主机相连时,设备的其它信号线 SCK、MOSI 及 MISO 同时并联到相同的 SPI 总线上,即无论有多少个从设备,都共同只使用这 3 条总线;而每个从设备都有独立的这一条 NSS 信号线,本信号线独占主机的一个引脚,即有多少个从设备,就有多少条片选信号线。 I2C 协议中通过设备地址来寻址、选中总线上的某个设备并与其进行通讯;而 SPI 协议中没有设备地址,它使用 NSS 信号线来寻址,当主机要选择从设备时,把该从设备的 NSS 信号线设置为低电平,该从设备即被选中,即片选有效,接着主机开始与被选中的从设备进行 SPI 通讯。所以SPI 通讯以 NSS 线置低电平为开始信号,以 NSS 线被拉高作为结束信号。

  2. SCK (Serial Clock): 时钟信号线,用于通讯数据同步。它由通讯主机产生,决定了通讯的速率,不同的设备支持的最高时钟频率不一样,如 STM32 的 SPI 时钟频率最大为fpclk/2,两个设备之间通讯时,通讯速率受限于低速设备。

  3. MOSI (Master Output, Slave Input): 主设备输出/从设备输入引脚。主机的数据从这条信号线输出,从机由这条信号线读入主机发送的数据,即这条线上数据的方向为主机到从机。

  4. MISO(Master Input, Slave Output): 主设备输入/从设备输出引脚。主机从这条信号线读入数据,从机的数据由这条信号线输出到主机,即在这条线上数据的方向为从机到主机。

协议层

SPI 基本通讯过程

与 I2C 的类似, SPI 协议定义了通讯的起始和停止信号、数据有效性、时钟同步等环节。

在这里插入图片描述

这个是一个主机的通讯时序。NSS、 SCK、 MOSI 信号都由主机控制产生,而 MISO 的信号由从机产生,主机通过该信号线读取从机的数据。 MOSI 与 MISO 的信号只在 NSS 为低电平的时候才有效,在 SCK 的每个时钟周期 MOSI 和 MISO 传输一位数据。单单看看这个时序图和简单的介绍是看不懂。那么我们逐个分析吧。

通讯的起始和停止信号

图中标号①处,NSS 信号线由高变低,是 SPI 通讯的起始信号。 NSS 是每个从机各自独占的信号线,当从机在自己的 NSS 线检测到起始信号后,就知道自己被主机选中了,开始准备与主机通讯。在图中的标号⑥处, NSS 信号由低变高,是 SPI 通讯的停止信号,表示本次通讯结束,从机的选中状态被取消。

数据有效性

    SPI 使用 MOSI 及 MISO 信号线来传输数据,使用 SCK 信号线进行数据同步。 MOSI及 MISO 数据线在 SCK 的每个时钟周期传输一位数据,且数据输入输出是同时进行的。数据传输时, MSB 先行或 LSB 先行并没有作硬性规定,但要保证两个 SPI 通讯设备之间使用同样的协定,一般都会采用图中的 MSB 先行模式。
     观察图中的②③④⑤标号处, MOSI 及 MISO 的数据在 SCK 的上升沿期间变化输出,在 SCK 的下降沿时被采样。即在 SCK 的下降沿时刻, MOSI 及 MISO 的数据有效,高电平时表示数据“1”,为低电平时表示数据“0”。在其它时刻,数据无效, MOSI 及 MISO为下一次表示数据做准备。
     SPI 每次数据传输可以 8 位或 16 位为单位,每次传输的单位数不受限制。

CPOL/CPHA 及通讯模式

上面只展示了一种SPI的通讯模式,SPI一共有四种通讯模式,它们的主要区别是总线空闲时 SCK 的时钟状态以及数据采样时刻。为方便说明,在此引入“时钟极性 CPOL”和“时钟相位 CPHA”的概念。

时钟极性 CPOL 是指 SPI 通讯设备处于空闲状态时, SCK 信号线的电平信号(即 SPI 通讯开始前、 NSS 线为高电平时 SCK 的状态)。 CPOL=0 时, SCK 在空闲状态时为低电平,CPOL=1 时,则相反。

时钟相位 CPHA 是指数据的采样的时刻,当 CPHA=0 时, MOSI 或 MISO 数据线上的信号将会在 SCK 时钟线的“奇数边沿” 被采样。当 CPHA=1 时,数据线在 SCK 的“偶数边沿” 采样。

在这里插入图片描述

通讯引脚

在这里插入图片描述

注:其中 SPI1 是 APB2 上的设备,最高通信速率达 36Mbtis/s, SPI2、 SPI3 是 APB1 上的设
备,最高通信速率为 18Mbits/s。除了通讯速率, 在其它功能上没有差异。其中 SPI3 用到
了下载接口的引脚,这几个引脚默认功能是下载,第二功能才是 IO 口,如果想使用 SPI3
接口,则程序上必须先禁用掉这几个 IO 口的下载功能。一般在资源不是十分紧张的情况下,
这几个 IO 口是专门用于下载和调试程序,不会复用为 SPI3。

测试程序

前面我们已经看过了SPI引脚配置和硬件SPI引脚号,那么我们已经可以开始写一个程序了,这次我选择W24Q64芯片,这块芯片拥有8Mbit的内存空间。下面是这块芯片的数据手册,里面对这块芯片说明得很完善了,建议下载下来反复阅读。

https://pan.baidu.com/s/1EtVvINSISv_hSln_4MBURQ/
提取码:w5ej

SPI引脚初始化及宏定义

#define FLASH_SPI_APBxClock_FUN						RCC_APB2PeriphClockCmd
#define FLASH_SPI_CLK								RCC_APB2Periph_SPI1
#define FLASH_SPI_GPIO_APBxClock_FUN				RCC_APB2PeriphClockCmd
#define FLASH_SPI_GPIO_CLK							RCC_APB2Periph_GPIOA
#define FLASH_SPI_GPIO_PORT							GPIOA
#define FLASH_SPI_CS_PIN							GPIO_Pin_4
#define FLASH_SPI_CLK_PIN							GPIO_Pin_5
#define FLASH_SPI_MISO_PIN							GPIO_Pin_6
#define FLASH_SPI_MOSI_PIN							GPIO_Pin_7
static void SPI_GPIOInit(void){
	GPIO_InitTypeDef GPIO_InitStruoture;
	
	FLASH_SPI_APBxClock_FUN(FLASH_SPI_CLK,ENABLE);
	FLASH_SPI_GPIO_APBxClock_FUN(FLASH_SPI_GPIO_CLK,ENABLE);
	
	
	GPIO_InitStruoture.GPIO_Mode  = GPIO_Mode_AF_PP;
	GPIO_InitStruoture.GPIO_Pin   = FLASH_SPI_CLK_PIN;
	GPIO_InitStruoture.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(FLASH_SPI_GPIO_PORT,&GPIO_InitStruoture);
	
	GPIO_InitStruoture.GPIO_Mode  = GPIO_Mode_AF_PP;
	GPIO_InitStruoture.GPIO_Pin   = FLASH_SPI_MOSI_PIN;
	GPIO_InitStruoture.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(FLASH_SPI_GPIO_PORT,&GPIO_InitStruoture);
	
	GPIO_InitStruoture.GPIO_Mode  = GPIO_Mode_IN_FLOATING;
	GPIO_InitStruoture.GPIO_Pin   = FLASH_SPI_MISO_PIN;
	GPIO_InitStruoture.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(FLASH_SPI_GPIO_PORT,&GPIO_InitStruoture);
	
	GPIO_InitStruoture.GPIO_Mode  = GPIO_Mode_Out_PP;
	GPIO_InitStruoture.GPIO_Pin   = FLASH_SPI_CS_PIN;
	GPIO_InitStruoture.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(FLASH_SPI_GPIO_PORT,&GPIO_InitStruoture);	
	
	FLASH_SPI_CS_HIGHT;
}

SPI模式初始化及宏定义

#define FLASH_SPIx											SPI1


static void SPI_Mode_Config(void){
	SPI_InitTypeDef SPI_InitStructure;
	
	SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_2;
	//使用模式3
	SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;
	SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;
	SPI_InitStructure.SPI_CRCPolynomial = 0;
	SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;
	SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; //双线全双工
	SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;
	SPI_InitStructure.SPI_Mode = SPI_Mode_Master;
	SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;
	
	//写入配置
	SPI_Init(FLASH_SPIx,&SPI_InitStructure);
	//使能SPI
	SPI_Cmd(FLASH_SPIx,ENABLE);
}

此次SPI通信我选择模式3,即空闲时SLK时钟为高电平,采样时刻偶数边缘,数据大小为8位,双线全双工。完成SPI初始化后那么SPI通信通道已经建立起来了,下一步就是和W25Q64芯片进行通讯。W25Q64是一块FLASH存储芯片,那么我们可以用它存储一些数据。不过对于新手来说,刚开始就做存储数据有点难度,那我们先进行一次SPI通信获取W25Q64的设备ID号。

static __IO uint32_t SPITimeout = SPI_LONG_TIMEOUT;

static uint32_t SPI_TIMEOUT_UserCallback(uint8_t errorCode)
{
	FLASH_ERROR("SPI 等待超时!erroCode = %d",errorCode);
	
	return 0;
}


//发送一个字节
uint8_t SPI_FLASH_Send_Byte(uint8_t data)
{
	SPITimeout = SPI_FLAG_TIMEOUT;
	
	//检查并等待至TX缓冲区为空
	while(SPI_I2S_GetFlagStatus(FLASH_SPIx,SPI_I2S_FLAG_TXE) == RESET){
		if((SPITimeout--) == 0)
			return SPI_TIMEOUT_UserCallback(0);
	}
	
	//程序执行到此处,TX缓冲区已空,开始发送数据^^
	SPI_I2S_SendData(FLASH_SPIx,data);
	
	SPITimeout = SPI_FLAG_TIMEOUT;
	//检查并等待至RX缓冲区为非空
	while(SPI_I2S_GetFlagStatus(FLASH_SPIx,SPI_I2S_FLAG_RXNE) == RESET){
		if((SPITimeout--) == 0)
			return SPI_TIMEOUT_UserCallback(0);
	}
	
	//程序执行到此处,说明数据发送完毕,并接收到一字字节
	
	return SPI_I2S_ReceiveData(FLASH_SPIx);
}
//读取ID号
uint32_t SPI_Read_ID(void){
	static uint32_t flash_id = 0;
	
	//片选使能
	FLASH_SPI_CS_LOW;
	SPI_FLASH_Send_Byte(READ_JEDEC_ID);
	flash_id 	|= SPI_FLASH_Send_Byte(DUMMY);
	flash_id <<= 8;
	flash_id 	|= SPI_FLASH_Send_Byte(DUMMY);
	flash_id <<= 8;
	flash_id	|= SPI_FLASH_Send_Byte(DUMMY);
	FLASH_SPI_CS_HIGHT;
	return flash_id;
}

宏定义

//CS引脚配置
#define FLASH_SPI_CS_HIGHT	GPIO_SetBits(FLASH_SPI_GPIO_PORT,FLASH_SPI_CS_PIN);
#define FLASH_SPI_CS_LOW		GPIO_ResetBits(FLASH_SPI_GPIO_PORT,FLASH_SPI_CS_PIN);


//等待超时时间
#define	SPI_FLAG_TIMEOUT	((uint32_t)0x1000)
#define SPI_LONG_TIMEOUT	((uint32_t)(10 * SPI_FLAG_TIMEOUT))



//信息输出
#define FLASH_DEBUG_ON	0
#define FLASH_INFO(fmt,arg...)           UsartPrintf(USART_DEBUG,"<<-FLASH-INFO->> "fmt"\n",##arg);
#define FLASH_ERROR(fmt,arg...)          UsartPrintf(USART_DEBUG,"<<-FLASH-ERROR->> "fmt"\n",##arg);
#define FLASH_DEBUG(fmt,arg...)          do{\
                                          if(FLASH_DEBUG_ON)\
                                          UsartPrintf(USART_DEBUG,"<<-FLASH-DEBUG->> [%d]"fmt"\n",__LINE__, ##arg);\
                                          }while(0)

#define DUMMY 				0x00
#define READ_JEDEC_ID		0X9F

main函数

#define USART_DEBUG		USART1		//调试打印所使用的串口组

int main(){
	uint32_t id;
	uint16_t i;
	
	Usart1_Init(115200);
		
	UsartPrintf(USART_DEBUG,"\r\n这是一个SPI—FLASH读写实验\r\n");
	SPI_FLASH_Init();
	id = SPI_Read_ID();
	UsartPrintf(USART_DEBUG,"\r\n id = 0x%X\r\n",id);
	
	while(1)
	{
	}
}

实验效果:
图片

以下是部分W25QXX芯片的厂家ID表用于比对是否正确,及芯片指令表。
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述在这里插入图片描述

READ_JEDEC_ID指令时序图:
在这里插入图片描述

对应前面的SPI_Read_ID的内容,单片机芯片发送READ_JEDEC_ID指令(0X9F),W25Q64芯片先返回Manufacturer ID号(0XEF),接着返回FLASH型号,所有的数据都从高位到低位接收的。

完成获取W25Q64芯片的ID号,那么接下来可以进行存储数据了。储存数据数据之前我们需要一片干净的存储空间,那么必须要先将存储空间擦除干净。由前面的指令表可以查到擦除指令有Page Program——0X02、Block Erase(64KB)——0XD8、Block Erease(32KB)——0X52、Sector Erase(4KB)——0X20,但一般用得多就Page Program和Sector Erase,那对着Sector Erase的时序图写的一段擦除存储空间的代码。
在这里插入图片描述

#define WRITE_ENABLE	0X06
#define ERASE_SECTOR	0x20

//FLASH写入使能
void SPI_Write_Enable(void)
{
	//片选使能
	FLASH_SPI_CS_LOW;
	
	//发送写入使能命令
	SPI_FLASH_Send_Byte(WRITE_ENABLE);
	
	FLASH_SPI_CS_HIGHT;
}

//擦除FLASH指定扇区
void SPI_Erase_Sector(uint32_t addr)
{	
	
	SPI_Write_Enable();
	
	//片选使能
	FLASH_SPI_CS_LOW;
	
	//发送擦除命令
	SPI_FLASH_Send_Byte(ERASE_SECTOR);
	
	SPI_FLASH_Send_Byte((addr>>16)&0XFF);
	SPI_FLASH_Send_Byte((addr>> 8)&0XFF);
	SPI_FLASH_Send_Byte(addr&0XFF);
	
	FLASH_SPI_CS_HIGHT;
	
	SPI_WaitForWriteEnd();
}

擦除后就可以往W25Q64芯片里存储数据了。向FLASH写入内容的指令为Page Program(0X02),对照时序图的要求写就可以了,时序图和代码如下:
在这里插入图片描述

#define WRITE_DATA 0X02

//向FLASH写入内容
void SPI_Write_Datas(uint32_t addr,uint8_t *writeBuff,uint32_t numByteToWrite)
{
	SPI_Write_Enable();
	
	//片选使能
	FLASH_SPI_CS_LOW;
	
	//发送擦除命令
	SPI_FLASH_Send_Byte(WRITE_DATA);
	
	SPI_FLASH_Send_Byte((addr>>16)&0XFF);
	SPI_FLASH_Send_Byte((addr>> 8)&0XFF);
	SPI_FLASH_Send_Byte(addr&0XFF);
	
	while(numByteToWrite--)
	{
		SPI_FLASH_Send_Byte(*writeBuff);
		writeBuff++;
	}
	
	FLASH_SPI_CS_HIGHT;
	
	SPI_WaitForWriteEnd();
}

单单向W2564芯片写入数据,是无法验证是否数据正确的,将芯片里的数据读取出比对验证是否正确了。读取W25Q64芯片内容的指令为Read Data(0X03),时序图和代码如下:
在这里插入图片描述

//读取FLASH的内容
void SPI_Read_Datas(uint32_t addr,uint8_t *readBuff,uint32_t numByteToRead)
{
	//片选使能
	FLASH_SPI_CS_LOW;
	
	//发送擦除命令
	SPI_FLASH_Send_Byte(READ_DATA);
	
	SPI_FLASH_Send_Byte((addr>>16)&0XFF);
	SPI_FLASH_Send_Byte((addr>> 8)&0XFF);
	SPI_FLASH_Send_Byte(addr&0XFF);
	
	while(numByteToRead--)
	{
		*readBuff = SPI_FLASH_Send_Byte(DUMMY);
		readBuff++;
	}
	
	FLASH_SPI_CS_HIGHT;
}

每次对W25Q64芯片操纵都要等待FLASH内部时序操作完成,否则W25Q64芯片还没有完成上一个操作就要进行下一个操作。时序图和代码如下:
在这里插入图片描述

//等待FLASH内部时序操作完成
void SPI_WaitForWriteEnd(void)
{
	uint8_t status_reg = 0;
	
	//片选使能
	FLASH_SPI_CS_LOW;
	
	//发送查询是否忙碌命令
	SPI_FLASH_Send_Byte(READ_STATUS);
	
	do{
		status_reg = SPI_FLASH_Send_Byte(DUMMY);	
	}while((status_reg & 0x01) == 1); // 为1则为忙碌,继续发送DUMMY,直到status_reg为0	
	
	FLASH_SPI_CS_HIGHT;
}

main函数

void FLASHMODE1(){
	uint16_t i;
	
	SPI_Erase_Sector(0);
	
	for(i=0;i<10;i++)
	{
		writeBuff[i] = i;
	}
	SPI_Write_Datas(0,writeBuff,10);
	
	SPI_Read_Datas(0,readBuff,4096);
	
	for(i=0;i<4096;i++)
	{
		UsartPrintf(USART_DEBUG,"0x%X ",readBuff[i]);
		if(i%10 == 0)
		{
			UsartPrintf(USART_DEBUG,"\r\n");
		}
	}	
}

int main(){
	uint32_t id;
	uint16_t i;
	
	Usart1_Init(115200);
	LED_GPIO_Config();
	
	UsartPrintf(USART_DEBUG,"\r\n这是一个SPI—FLASH读写实验\r\n");
	SPI_FLASH_Init();
	id = SPI_Read_ID();
	UsartPrintf(USART_DEBUG,"\r\n id = 0x%X\r\n",id);
		
	FLASHMODE1();
	
	while(1)
	{
	}
}

代码逻辑讲解

程序在初始化完成后,首先把W25Q64芯片的一个扇区擦除,然后向被擦除的扇区写入10个数字(从0到9),最后将扇区里的所有数据读取出来

实验效果:
在这里插入图片描述

可能很多人都不是很想去阅读英语文档,但通过软件翻译出来译文总是会有偏差的,而一名优秀的程序员阅读英文文档应该是必备技能。英语基础差不是理由,反复多读或慢慢翻译几遍总会有收获的。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值