新手必看!!STM32-SPI串行全双工通信协议-SPI驱动W25Q64!


  
  SPI(Serial Peripheral Interface)是一种串行通信协议,通常用于连接微控制器和外部设备,如传感器、存储器、显示屏等。它是一种全双工的通信协议,意味着数据可以在同时的时候在两个方向上传输。

一、IIC、UART、SPI的比较

通信协议UARTIICSPI
通信特征异步串行全双工同步串行半双工同步串行全双工
接口TX、RXSCL、SDAMOSI、MISO、SCL、CS/NSS
速度多种波特率100Khz、400Khz、3.4Mhz由时钟频率与实际功能决定
数据帧格式起始位+数据位+校验位+停止位起始条件+位传输+应答+停止条件四种模式:MODE0~MODE3
主从设备通信没有主从有主从有主从
总线结构一对一一对多一对多

二、SPI通讯特点

  1. SPI是一种高速,全双工,同步串行总线。
  2. SPI有主从俩种模式,通常由一个主设备和一个或者多个从设备组从。SPI不支持多主机。
  3. SPI通们至少需委四根线,分别是 MISO(主设备数据输入,从设备输出),MOSI(王设备数据输出从设备输入),SCLK(时钟信号),CS/SS(片选信号)。
      在通信过程中,主机只能选择一个从机进行通信。将进行通信的从机A的CS/NSS(片选)引脚拉低(低电平),其他从机的CS/NSS(片选)引脚全部拉高(高电平),即可进行与从机A进行通信。

三、通信四根线的作用

  • SCK (Serial Clock): 这是时钟信号,它由主设备产生,用于同步数据传输速度。通常SPI设备在上升沿或下降沿读取数据。
  • MISO (Master In Slave Out): 这是从设备向主设备传输数据的线路。
  • MOSI (Master Out Slave In): 这是主设备向从设备传输数据的线路。
  • CS/SS (Chip Select/Slave Select):从设备选择信号线,常称为片选信号线,也称为 NSS。每个从设备都有独立的这一条 NSS 信号线,本信号线独占主机的一个引脚,即有多少个从设备,就有多少条片选信号线。I2C 协议中通过设备地址来寻址、选中总线上的某个设备并与其进行通讯;而 SPI 协议中没有设备地址,它使用 NSS 信号线来寻址,当主机要选择从设备时,把该从设备的 NSS 信号线设置为低电平,该从设备即被选中,即片选有效,接着主机开始与被选中的从设备进行 SPI 通讯。所以SPI 通讯以 NSS 线置低电平为开始信号,以 NSS 线被拉高作为结束信号。
  • 主机SPI引脚模式配置:
    把SCK/MOSI/MISO引脚初始化成复用推挽模式。 而CS(NSS)引脚由于使用软件控制,我们把它配置为普通的推挽输出模式。
    在这里插入图片描述

四、SPI硬件接线

1. 常规模式接线

在这里插入图片描述

2. 菊花链模式

在这里插入图片描述

五、SPI通讯原理

所有通信(读/写)均有主设备通过控制时钟线产生跳变沿发起。

  1. 由主机拉低对应的CS,找到从机(选中从机通信)。
  2. 主机控制时钟线产生跳变沿(上升沿、下降沿),进行数据读/写。
  3. 主机拉高片选(完成通信,断开从机)。

六、SPI工作原理

在这里插入图片描述
(1)主机和从机都有一个串行移位寄存器,主机通过向它的SPI串行寄存器写入一个字节来发起一次传输
(2)串行移位寄存器通过MOSI信号线将字节传送给从机,从机也将自己的串行移位寄存器中的内容通过MISO信号线返回给主机。这样,两个移位寄存器中的内容就被交换。
(3)外设的写操作和读操作是同步完成的。
  主机通过 MOSI 线发送 1bit 效据,从机通过该线读取这 1bt 数据;从机通过 MIS0 线发送 1bit 数据,主机通过该线读取这 1bt 数据。当寄存器中的内容全部移出时,相当于完成了两个寄存器内容的交换。
  如果主设备要给从设备传输数据,主设备只需要忽略掉从从设备按收到的效据即可。如果主设备要从从设备接收数据,主设备向从设备随机发送数据,从设备忽略掉从主设备接收的数据即可。
(4)SPI总线在进行数据传输时,默认先传输高位,后传输低位。 数据线为高电平表示逻辑1,数据线为低电平表示逻辑0,一个字节传输完成以后无需应答信号即可开启下一个字节的传输。
  SPI总线采用同步方式,在时钟线的第一个或者第二个跳变沿采集数据(主机侧读数据),然后在紧接着的下一个跳变沿发数据。8个时钟周期即可完成一个字节的效据传输。

七、SPI的极性和相位–决定工作模式

四种模式:MODE0~MODE3。
  SPI可以工作在不同的模式,主要取决于时钟信号的相位(CPHA)和极性(CPOL)。这两个参数决定了数据的采样时机。实际中采用较多的是“模式 0”与“模式 3”。
  CPHA(Clock Phase,时钟相位):表示SCK在第几个时钟边缘采样数据。当CPHA=0,在SCK周期的第一个边沿采样数据,当CPHA=1,在SCK周期的第二个边沿采样数据。
  CPOL(Clock Polarity,时钟极性):表示SCK在空闲时为高电平还是低电平。当CPOL=0,SCK空闲时为低电平,当CPOL=1,SCK空闲时为高电平。

模式CPOL(SCK)CPHASCK空闲采样点
000低电平数据在周期的第一个电平转换沿处采样(上升沿采样)
101低电平数据在周期的第二个电平转换沿处采样(下降沿采样)
210高电平数据在周期的第一个电平转换沿处采样(下降沿采样)
311高电平数据在周期的第二个电平转换沿处采样(上升沿采样)

●CPHA=0时SPI通讯模式:在这里插入图片描述

●CPHA=1时SPI通讯模式:
在这里插入图片描述

八、SPI通讯时序图

在这里插入图片描述
(1)起始信号: 在上图①处,NSS 信号线由高变低,是 SPI 通讯的起始信号。
NSS 是每个从机各自独占的信号线,当从机在自己的 NSS 线检测到起始信号后,就知道自己被主机选中了,开始准备与主机通讯。
(2)数据的有效性: 图中的②③④⑤标号处,MOSI 及 MISO 的数据在 SCK 的上升沿期间变化输出,在 SCK 的下降沿时被采样。即在 SCK 的下降沿时刻,MOSI 及 MISO 的数据有效,高电平时表示数据“1”,为低电平时表示数据“0”。在其它时刻,数据无效,MOSI 及 MISO为下一次表示数据做准备。
(3)停止信号: 在上图⑥处,NSS 信号由低变高,是 SPI 通讯的停止信号。表示本次通讯结束,从机的选中状态被取消。

九、实验一:IO口模拟SPI驱动W25Q64

使用IO口来模拟出SPI时序信号,即通过拉高拉低IO口电平来实现。
在这里插入图片描述

1. 传输一个字节的数据(发送和接收数据同时进行。sByte:发送数据,rByte:接收数据)

u8 SPI_TransferByte (u8 sByte)
{
	u8 i, rByte;
	for(i =0; i<8;i++)
	{
		SPI_SCK_L;               //下降沿-准备发送数据
		if(sByte &(0x80 >> i))  //高位发起
		    SPI_MOSI_H;
		else 
		    SPI_MOSI_L;
		    
        rByte <<= 1;      //空出最低位,准备接收数据
        SPI_SCK_H;        //上升沿-采集数据
        if(SPI_MISO_R)   //读取 MISO的电平
         rByte |= 1;
   }
         return rByte;
}

2. W25Q64指令时序分析

●统一解答:下文中为什么读数据时,使用SPI_TransferByte(0xFF); 发送0xFF。
答:因为SPI读取数据和接收数据是同步进行的,所以我们想要接收(读取)数据时,必须先发送一个任意数据,这里我们使用0xFF(自定)。SPI_TransferByte()函数的返回值就是我们要读取的数据。

(1) 读状态寄存器1:指令0x05

在这里插入图片描述

u8 w25Q64_Readstatus1(void)
{
	u8 status;
	SPI_CS_L;//拉低片选
	SPI_TransferByte(0x05);//发送指令0x05
	status = SPI_TransferByte(0xFF); //接收状态寄存器的值,发送任意数据都行。
	SPI_CS_H;//拉高片选
	
	return status;
}

(2) 写使能:指令0x06

在这里插入图片描述

void w25Q64writeEnable(void)
{
	SPI_CS_L;//拉低片选
	SPI_TransferByte (0x06);//发送写使能指令0x06
	SPI_CS_H;//拉高片选
}

(2)扇区擦除:指令0x20

在这里插入图片描述

void w25Q64_sectorErase (u32 addr) //扇区擦除
{
	u8 *pAddr = (u8*) &addr; //扇区首地址
	u8 status;
	w25Q64writeEnable(); //写使能
	
	SPI_CS_L;//拉低片选
	SPI_TransferByte (0x20);//发送扇区擦除指令0x20
	
	//发送24bit内部地址
	SPI_TransferByte (pAddr[2]); // bit 23-16
	SPI_TransferByte (pAddr[1]); //bit 15-8
	SPI_TransferByte (pAddr[0]);  //bit 7-0
	
	SPI_CS_H; //拉高片选
	
	//等待擦除完成 BUSY ==0
	do{
        status=w25Q64_Readstatus1();  //状态寄存器值为0时,擦除完成
        delay_ms(1);
     } while(status &(0x1));

}

(3)页写:指令0x20

例如:addr: 0x123456
bit 23-16: 0x12(addr >> 16)& OxFF
bit 15-8:  Ox34(addr >> 8)& OxFF

u8 *pAddr = &addr;
*pAddr==pAddr[0]== 0x56
*(pAddr+1)==pAddr[1]= 0x34
*(pAddr+2) ==DAddr[2]= 0x12

在这里插入图片描述

void w25Q64_Page_Write_Data(u32 addr, u32 num, u8 *sBuf) //首地址、写几个、写什么
{
	u8 *pAddr = (u8 *) &addr;
	u8 status;
	w25Q64_writeEnable(); //写使能
	
	SPI_CS_L;//拉低片选
	SPI_TransferByte (0x02);//发送指令0x03
	
	//发送24bit内部地址
	SPI_TransferByte(pAddr[2]); // bit 23-16
	SPI_TransferByte(pAddr[1]); //bit 15-8
	SPI_TransferByte(pAddr[0]);//bit 7-0
	
	while (num--)
	{
		SPI_TransferByte(*sBuf++);
	}
	SPI_CS_H;//拉高片选
	
	//等待写入完成 BUSY ==0
	do{
        status=w25Q64_Readstatus1();  //状态寄存器值为0时,写入完成
        delay_ms(1);
     } while(status &(0x1));
}

●补充:顺序写

页写不可以跨页写,但是顺序写可以。
在这里插入图片描述

void w25Q64_Orderwrite(u32 addr, u32 num, u8 *sBuf)
{
	u32 remain = 0; 
	remain = 256 - addr % 256; //从本页首地址算出能写下的数量
	if (num <= remain)//判断本页能否写下
	{
	    w25Q64_Page_Write_Data(addr, num, sBuf);//直接写完所有数据
	}
	else   //本页写不完
	{
	    w25Q64_Page_Write_Data(addr, remain, sBuf);//将本页写完
	 }   
	 
    addr +=remain;	//地址偏移
   	sBuf +=remain;	//数据地址偏移
    remain = num - remain;//剩下的需要写入的数据项
    
	//判断剩下的数据一页能否写完  
	while (remain >=256)		//不能写完
	{
		w25Q64_Page_Write_Data(addr, 256, sBuf); //写满一页
		addr +=256;		//地址偏移
		sBuf += 256;   //数据地址偏移
		remain = remain - 256;//剩下的需要写入的数据项
	)
	
	if (remain > 0)    // 0 <num <256剩下的不够写满一页的
	{
	    w25Q64_Page_Write_Data(addr,remain,sBuf);//剩下的写完
	}
}

(4)读ID:指令0x90

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

ul6 w25Q64_ReadID(void)
{
	ul6 ID;
	w25Q64writeEnable();  //写使能
	
	SPI_CS_L; //拉低片选
	SPI_TransferByte (0x90);//发送读ID指令0x90
	SPI_TransferByte (0x00);//发送0x000000
	SPI_TransferByte (0x00);
	SPI_TransferByte (0x00);
	
	ID = SPITransferByte (0xFF);// Manufacturer ID
    ID <<= 8;
    ID |=SPI_TransferByte (0xFF); // Device ID
	
	SPI_CS_H;//拉高片选
	
	return ID;
}

(5)读数据:指令0x30

在这里插入图片描述

void w25Q64_ReadData(u32 addr, u32 num, u8 *rBuf) //首地址、读几个、读到哪
{
	u8 *pAddr = (u8 *) &addr;
	w25Q64_writeEnable(); //写使能
	SPI_CS_L;//拉低片选
	SPI_TransferByte (0x03);//发送指令0x03
	
	//发送24bit内部地址
	SPI_TransferByte(pAddr[2]); // bit 23-16
	SPI_TransferByte(pAddr[1]); //bit 15-8
	SPI_TransferByte(pAddr[0]);//bit 7-0
	
	while (num--)
	{
		*rBuf++ = SPI_TransferByte(OxFF);//接收数据
	}
	SPI_CS_H;//拉高片选
}

十、实验二:SPI控制器驱动W25Q64

  使用SPI控制器来驱动W25Q64就不需要IO口来模拟拉高拉低数据线,而是将IO口复用为SPI控制器功能,直接把要传输的内容写入SPI控制器的DR寄存器中,SPI控制器就会自动模拟出SPI时序信号去发送或接收数据。

1. 框图

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

2. 软件设计步骤

代码部分只需要在实验一的基础上修改下面的内容即可。页写、顺序写等代码不需要改动。
(1)修改SPI初始化函数

  1. 修改MOSI、MISO、SCK三个引脚配置为复用推挽功能,SS配置为普通推挽功能。
  2. 映射到SPI1
  3. 配置CR1、CR2
  4. 使能SPI1

(2)修改SPI传输字节函数

  1. 等待发送缓冲区为空。 写 DR。
  2. 等待接收缓冲区非空。 读DR。

3. 代码设计

●修改SPI初始化函数

void SPI_Init(void)
{
	//打开GPIOA/B时钟
	RCC->AHB1ENRl=(Ox3 <<0);
	//CS -PB14 推挽输出
	GPIOB->MODER&=~(0x3 <<28);
	GPIOB->MODER|=(0x1 <<28);//输出
	GPIOB->OTYPER&= ~(0x1<<14);//推挽
	GPIOB->OSPEEDR|=(0x3 <<28);//100Mhz
	
	
	//SCK -PA5复用功能
	GPIOA->MODER&= ~(0x3<<10);
	GPIOA->MODER|= (0x2<<10);//复用
	GPIOA->OTYPER&= ~(0x1<<5);//推挽
	GPIOA->OSPEEDR =(0x3 <<10);//100Mhz
	GPIOA->AFR[O]&= ~(0xE<<20);//映射
	GPIOA->AFR[0]|= (0x5<<20) ;//映射- AF5
	
	//MOSI -PA7复用功能
	GPIOA->MODER&= ~(0x3 <<14);
	GPIOA->MODER |=(0x2 <<14);//复用
	GPIOA->OTYPER&= ~(0x1<<7);//推挽
	GPIOA->OSPEEDR |=(0x3<<14); //100Mhz
	GPIOA->AFR[0]&= ~(0xEu<<28);//映射
	GPIOA->AFR[0]|=(0x5u <<28) ;//映射- AF5
	
	//MISO -PA6 复用功能
	GPIOA->MODER&= ~(0x3<< 12);
	GPIOA->MODER |=(0x2<< 12); //复用
	GPIOA->PUPDR&= ~(0x3 <<12);//浮空
	GPIOA->AFR[0]&= ~(0xEu<<24);//映射
	GPIOA->AFR[0]|=(0x5u<<24);//映射-AF5
	
	//初始化SPI控制器
	RCC->APB2ENR |= (0x1<<12);//打开SPI1的时钟
	
	//配置CR1
	SPI1->CR1 =0;
	/*
	*选择双线单向通信
	*禁止CRC
	*8bit数据帧★全双工
	*MSB在前
	*scK波特率=fPCLR/2=84Mhz/ 2=42Mhz
	*/
	SPI1->CR1 |=(0x1<< 9);//使能软件从器件管理
	SPI1->CR1 |=(0x1 <<8);//SSI =1
	SPI1->CR1 |=(0x1 << 2);//SPI1配置为主模式
	SPI1->CR1 |=(0x3 << 0) ;//SPI模式为MODE3
	
	//配置CR2
	SPI1->CR2 &=~(0x1<<4);//SPI Motorola模式
	
	//使能SPI1
	SPI1->CR1 |=(0x1<<6);

}



●修改SPI传输字节函数

u8 SPI_TransferByte (u8 sByte)
{
  u8 rByte;
  
  while( ( SPI1->SR&(0x1<<1) )==0 );//等待发送缓冲区为空
  SPI1->DR = sByte;  //写DR
  while( ( SP11->SR &(Ox1 << 0) )==0 );//等待接收缓冲区非空
  rByte = SPI1->DR;  //读DR
  
  return rByte;
}

补充:库函数

void SPI1_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    SPI_InitTypeDef SPI_InitStructure;

    // 启用时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);  // GPIOA 时钟使能
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);  // GPIOB 时钟使能
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);   // SPI1 时钟使能

    // 初始化 MISO, MOSI 和 SCK 引脚
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7;  // SCK, MISO, MOSI
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;  // 复用推挽输出模式
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);  // 初始化 GPIOA

    // 初始化 CS 引脚
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14;  // CS 引脚
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;  // 普通推挽输出
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOB, &GPIO_InitStructure);  // 初始化 GPIOB

    // 设置 CS 引脚为高电平
    GPIO_SetBits(GPIOB, GPIO_Pin_14);
    GPIO_SetBits(GPIOA, GPIO_Pin_5 | GPIO_Pin_7);

    // SPI 配置
    SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;  // 双线双向全双工
    SPI_InitStructure.SPI_Mode = SPI_Mode_Master;  // 设置为主设备模式
    SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;  // 数据帧大小 8 位
    SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low;  // 空闲状态时钟为低电平
    SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;  // 数据采样在时钟的第一个跳变沿
    SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;  // NSS 管脚由软件控制
    SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4;  // 波特率预分频器值为 4
    SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;  // MSB 位先发送
    SPI_InitStructure.SPI_CRCPolynomial = 10;  // CRC 校验多项式

    // 初始化 SPI1 外设
    SPI_Init(SPI1, &SPI_InitStructure);
    SPI_Cmd(SPI1, ENABLE);  // 启用 SPI 外设

    // 启动传输
    SPI1_ReadWriteByte(0xFF);
}

u8 SPI1_ReadWriteByte(u8 TxData)
{
    // 等待发送缓存为空
    while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET);
    // 发送数据
    SPI_I2S_SendData(SPI1, TxData);
    // 等待接收缓存非空
    while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET);
    // 读取接收到的数据
    u8 RxData = SPI_I2S_ReceiveData(SPI1);
    // 返回接收到的数据
    return RxData;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值