stm32学习笔记:SPI通信协议原理(未完)

一、SPI简介(serial Peripheral Interface(串行 外设 接口))

1、电路模式(采用一主多从的模式)、同步,全双工

1 所有SPI设备的SCK、MOSI、MISO分别连在一起
2 主机另外引出多条SS控制线,分别接到各从机的SS引脚
3 输出引脚配置为推挽输出,输入引脚配置为浮空或上拉输入
4 推挽输出:高低电平都有很强的驱动能力,使得SPI引脚信号的下降沿和上升沿非常迅速
5 (IIC因为要实现半双工,经常切换输出输入,IIC又要实现多主机的时钟同步和总线仲裁,若使用推挽输出任意电源短路)
6 SPI的MISO可能有冲突,一位内主机是输入,三个从机都是输出,若三个从机始终是推挽输出,势必会导致冲突。
故SPI有个规定:
当从机的SS引脚为高电平时,即从机未被选中,其MISO引脚必须切换成高阻态,高阻态相当于引脚断开,不输出任何电平,这样可以防止一条线有多个输出,导致电平冲突问题
SS为低电平时,MISO才允许变为推挽输出(切换在从机中,不需要关注)

2、4条信号线

1 SS(片选信号线(理解为从机选择线)、Slave Select):单片机通过给片选信号线高低电平来确定哪一个从机通讯,一般当这根线为低电平时,片选才有效
2 SCK(时钟信号线、Serial Clock):主设备产生
3 MOSI(发送信号线、Master Output Slave Input):主设备从MOSI输出数据,而从设备通过MOSI接收数据
4 MISO(接收信号线、Master Input Slave Output):主设备通过这根线接收数据

 

3、通信过程 

同步,肯定有时钟,因此,SCK引脚就是用来提供时钟信号的。数据位的输出和输入都是SCK的上升沿和下降沿进行的。这样数据位的收发时刻就可以明确的确定。

全双工:数据发送和数据接收单独各占一条线。MOSI和MISO就是分别用于发送和接收的两条线路。MOSI是主机输出从机输入,主机向从机发送数据的线路。MISO就是主机从从机接收数据的线路。

SPI:仅支持一主多从,不支持多主机。I2C太麻烦,直接开辟一条通讯线,专门用来指定我要跟哪个从机进行通信,即SS从机选择线。

I2C:实现一主多从的方式是在其实条件下,主机必须先发送一个字节进行寻址,用来指定我要跟哪个从机进行通信。所以I2C要涉及分配地址和寻址的问题。

4、SPI的硬件规定、SPI的软件规定 

主机:stm32
从机:存储器、显示屏、通信模块、传感器等

 SPI所有通信线都是单端信号,它们的高低电平都是相对GND的电压差,所以单端信号,所有设备还需要共地。

如果从机没有独立供电,主机需要额外引出电源正极VCC给从机供电。

SCK:时钟线完全由主机掌控,所以对于主机来说,时钟线为输出,对于所有从机来说,时钟线为输入。这样主机的同步时钟,就能送到各个从机

MOSI:主机输出,从机输入

MISO:主机输入,从机输出

SS:低电平有效,主机想指定谁,就把对应的SS输出线置低电平。比如,主机初始化,所有SS线都置高电平(谁也不指定)

输出引脚:推挽输出,高低电平均有很强的驱动能力,这将使得SPI引脚信号的下降沿和上升沿非常迅速,不像I2C下降沿非常迅速,但是下降沿就非常缓慢。SPI信号变化快,自然它就能达到更高的传输速度。一般SPI信号都能轻松达到MHz的速度级别。I2C并不是不想使用更快的推挽输出,而是I2C要实现半双工,经常要切换输入输出,而且I2C又要实现多主机的时钟同步和总线仲裁,这些功能都不允许I2C使用推挽输出。(否则容易导致电源短路,I2C选择更多的功能,自然放弃更强的性能)

输入引脚:浮空或上拉输入

SPI有个缺点:如果三个从机始终都是推挽输出,主机一个是输入,势必会导致冲突。规定:当从机的SS引脚为高电平时,也就是从机未被选中,它的MISO引脚必须切换为高阻态,高阻态就相当于引脚断开,不输出任何电平。这样就可以防止一条线有多个输出,而导致的电平冲突的问题。在SS为低电平时,MISO才允许变为推挽输出。

5、I2C和SPI的比较 

(1)I2C
1、I2C在硬件和软件电路设计都比较复杂,但可在消耗最低硬件资源的情况下,实现最多的功能
2、由于I2C开漏外加上拉电阻的电路结构,使得通信线高电平的驱动能力比较弱,这会导致通信线由低电平变到高电平的时候,上升沿耗时比较长,限制了IIC的最大通信速度
3、故IIC的标准模式只有100kHz的时钟频率,快速模式只有400kHz

(2)SPI
1、SPI传输更快,没有严格规定最大传输速度,最大传输速度取决于芯片厂商的设计需求,即看手册
2、SPI的设计比较简单粗暴,实现的功能没IIC多,硬件开销比较大,通行线的个数比较多哈,并且通行过程中,经常会有资源浪费的现象
3、SPI的风格:最简单最快速的完成任务,没有应答机制

6、移位示意图(SPI核心) 

左边是SPI主机,里面有一个8位的移位寄存器,右边是SPI从机,里面也有一个8位的移位寄存器。移位寄存器有一个时钟输入端,因为SPI一般是高位先行,因此,每来一个时钟,移位寄存器都会向左进行移位。移位寄存器的时钟源是由主机提供的(这里叫做波特率发生器),它产生的时钟驱动主机的移位寄存器进行移位。同时这个时钟也通过SCK引脚进行输出,接到从机的移位寄存器里。

组成一个圈
主机移位寄存器左边移出去的数据,通过MOSI引脚输入到从机移位寄存器的右边。
从机移位寄存器左边移出去的数据,通过MISO引脚输入到主机移位寄存器的右边。

(1)SPI的基础是交换一个字节

 SPI通信的基础是交换一个字节,从而可以实现①发送一个字节,②接收一个字节和③发送同时接收一个字节三种功能。

(2)SPI时序基本单元

 

交换一个字节(模式0)(应用最多)
CPOL=0(时间极性):空闲状态时,SCK为低电平
CPHA=0(时钟相位):SCK第一个边沿移入数据,第二个边沿移出数据

四个模式

模式0:CPOL=0:空闲SCK为0,CPHA=0:SCK第一个边沿移入数据,第二个边沿移出

模式1:CPOL=0:空闲SCK为0,CPHA=1:SCK第一个边沿移出数据,第二个边沿移入

模式2:CPOL=0:空闲SCK为1,CPHA=0:SCK第一个边沿移入数据,第二个边沿移出

模式3:CPOL=0:空闲SCK为1,CPHA=1:SCK第一个边沿移出数据,第二个边沿移入

上述MOSI、MISO为两条线表示发送的既有可能是高电平又有可能是低电平

7、SPI完整的时序波形(基于W25Q64)

I2C中,有效数据流第一个字节是寄存器地址,之后依次是读写的数据,使用的是读写寄存器的模型。

SPI中,通常采用的是指令码加读写数据的模型。

SPI起始后,第一个交换发送给从机的数据,一般叫做指令码,在从机中,对应会定义一个指令集,当我们需要发送什么时,就可以在起始后第一个字节发送指令集里面的数据。

在W25Q64里,0X06代表的是写使能,在这里使用SPI模式0,在空闲状态,SS为高电平,SCK为低电平,MOSI和MISO电平没有严格规定。然后,SS产生下降沿,时序开始,在这个下降沿时刻,MOSI和MISO就要开始变换数据。

SCK低电平是数据变化的时期,高电平是读取数据的时期

以下:主机用0x06换来从机0xFF,但是实际上从机并没有输出,0XFF是默认高电平。 那整个时序的功能就是发送指令,指令码是0x06,从机比对后事先定义好的指令集,发现0x06是写使能的指令,那从机就会控制硬件进行写使能。这样一个指令从发送到执行就完成了。


1、发送指令(实现向从机的0x123456的地址上发送0x55)
(1)主机向SS指定的设备发送指令码0x06(写使能的指令)

(2)发送指令0x02,表示要写入

(3)指定地址0x123456:发送0x12(高位地址)、0x34、0x56(低位地址)

(4)发送指定数据0x55

 2、指定地址读数

(1)主机向SS指定的设备发送指令码0x06(写使能的指令)

(2)发送指令0x03,表示要读取

(3)指定地址0x123456:发送0x12(高位地址)、0x34、0x56(低位地址)

(4)主机为高电平,从机发送数据

二、W25Q64简介 

 

三、 软件SPI读写W25Q64

程序的整体框架

一、SPI模块(通信层)

该模块主要包含通信引脚封装、初始化以及SPI通信的3个拼图(起始、终止和交换一个字节)

二、W25Q64驱动层(基于SPI层)

在这个模块里,调用底层SPI的拼图,来拼接各种指令和功能的完整时序,比如写使能、擦除、页编程、读数据等。我们称作W25Q64的硬件驱动层

三、在主函数里调用驱动层的函数,来完成我们想要实现的功能。

main.c

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "W25Q64.h"

uint8_t MID;
uint16_t DID;

uint8_t ArrayWrite[] = {0x01, 0x02, 0x03, 0x04};
uint8_t ArrayRead[4];

int main(void)
{
	OLED_Init();
	W25Q64_Init();
	
	OLED_ShowString(1, 1, "MID:   DID:");
	OLED_ShowString(2, 1, "W:");
	OLED_ShowString(3, 1, "R:");
	//获取ID号,检测是否通信成功
	W25Q64_ReadID(&MID, &DID);
	OLED_ShowHexNum(1, 5, MID, 2);
	OLED_ShowHexNum(1, 12, DID, 4);
	//擦除扇区
	W25Q64_SectorErase(0x000000);
	//页编程写入
	W25Q64_PageProgram(0x000000, ArrayWrite, 4);
	//读取数据
	W25Q64_ReadData(0x000000, ArrayRead, 4);
	
	OLED_ShowHexNum(2, 3, ArrayWrite[0], 2);
	OLED_ShowHexNum(2, 6, ArrayWrite[1], 2);
	OLED_ShowHexNum(2, 9, ArrayWrite[2], 2);
	OLED_ShowHexNum(2, 12, ArrayWrite[3], 2);
	
	OLED_ShowHexNum(3, 3, ArrayRead[0], 2);
	OLED_ShowHexNum(3, 6, ArrayRead[1], 2);
	OLED_ShowHexNum(3, 9, ArrayRead[2], 2);
	OLED_ShowHexNum(3, 12, ArrayRead[3], 2);
	
	while (1)
	{
		
	}
}

添加通信层代码

MySPI.c

#include "stm32f10x.h"                  // Device header
//封装置高低电平的函数
//写SS引脚,CS片选信号A4,写数据位
void MySPI_W_SS(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue);
}
//写SCK引脚,时钟同步信号A5,写数据位
void MySPI_W_SCK(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOA, GPIO_Pin_5, (BitAction)BitValue);
}
//写MOSI引脚,A7,写数据位
void MySPI_W_MOSI(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOA, GPIO_Pin_7, (BitAction)BitValue);
}
//读MISO引脚,读数据位
uint8_t MySPI_R_MISO(void)
{
	return GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6);
}
// SPI初始化
//输出引脚配置为推挽输出,输入引脚配置为浮空或上拉输入,对于主机来说,时钟、主机输出和片选都是输出引脚
//主机输入是输入引脚
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); //信号线,计划使用SPI模式0,因此默认低电平
	//MOSI无要求
	//MISO输入引脚,无需输出电平
}
/*SPI3个时序基本单元*/
//起始信号,CS信号置低电平
void MySPI_Start(void)
{
	MySPI_W_SS(0);
}
//终止信号,CS信号置高电平
void MySPI_Stop(void)
{
	MySPI_W_SS(1);
}

//前提是SS为下降沿
//交换一个字节(SPI核心部分),W25Q64支持模式0和模式3,一般选择模式0
uint8_t MySPI_SwapByte(uint8_t ByteSend)  //ByteSend是传进来的参数,要通过交换一个字节的时序发送出去,返回值是ByteReceive是通过交换一个字节接收到的数据
{
	//用于接收数据
	uint8_t i, ByteReceive = 0x00;
	//在ss下降沿后,开始交换字节
	//实现时序
	for (i = 0; i < 8; i ++)
	{
		//主机和从机同时移出数据,就是主机移出数据最高位放到MOSI上,从机移出它的数据最高位放到MISO上,(MISO数据变化是从机的事情,不归主机管)
		//写MOSI,发送ByteSend由高到低位-->(放数据位)
		MySPI_W_MOSI(ByteSend & (0x80 >> i));  //发送的数据是BySend的最高位
		//SCK上升沿,主机和从机同时移入数据,从机会自动把B7读走,从机移入不归我们管,主机只需要读取MISO的数据即可
		//SCK上升沿时,从机会自动把MOSI的数据读走 
		MySPI_W_SCK(1);
		//读MISO,主机的任务就是把从机刚才放到MISO的数据读进来
		if (MySPI_R_MISO() == 1){ByteReceive |= (0x80 >> i);}  //读到的数据为接收的最高位
		//SCK产生下降沿,主机和从机移出下一位
		MySPI_W_SCK(0); 
		//通过循环,依次写入和读入B6、B5...B0
	}
	//返回接收到的数据
	return ByteReceive;
}

/*软件的交换字节总体流程*/
/*
先SS下降沿
再移出数据
再SCK上升沿
再移入数据
再SCK下降沿
再移入数据

*/


以下方法更符合移位框图
//uint8_t MySPI_SwapByte(uint8_t ByteSend)  //ByteSend是传进来的参数,要通过交换一个字节的时序发送出去,返回值是ByteReceive是通过交换一个字节接收到的数据
//{

//	uint8_t i;
//	//在ss下降沿后,开始交换字节
//	//实现时序
//	for (i = 0; i < 8; i ++)
//	{
//		MySPI_W_MOSI(ByteSend & 0x80);  
//		ByteSend <<= 1;
//	
//		MySPI_W_SCK(1);
//		
//		if (MySPI_R_MISO() == 1){ByteSend |= 0x01;} 
//	
//		MySPI_W_SCK(0); 
//		//通过循环,依次写入和读入B6、B5...B0
//	}
//	
//	return ByteSend;
//}

 MySPI.h

#ifndef __MYSPI_H
#define __MYSPI_H

void MySPI_Init(void);
void MySPI_Start(void);
void MySPI_Stop(void);
uint8_t MySPI_SwapByte(uint8_t ByteSend);

#endif

 W25Q64.c

#include "stm32f10x.h"                  // Device header
#include "MySPI.h"
#include "W25Q64_Ins.h"

void W25Q64_Init(void)
{
	//作为SPI上层的W25Q64模块,需要调用底层MySPI_Init,底层初始化好,上层才能正常工作
	MySPI_Init();
}

/********************************************
         业务代码-拼接完整时序
********************************************/

//有两个返回值,我们使用指针来实现多返回值
void W25Q64_ReadID(uint8_t *MID, uint16_t *DID)
{
	MySPI_Start();
	//抛玉引砖
	MySPI_SwapByte(W25Q64_JEDEC_ID);  //发送的是读ID号的指令,从机收到读ID号的指令,他就会在下一次交换把ID号返回给主机
	//通信过程,不同时间调用相同的函数,它的意义是不一样的
	//抛砖引玉
	//虽然调用同一个函数,但是它的返回值是不一样的,此时正在通信,通信是有时序的,不同时间调用相同的函数,意义是不一样的
	*MID = MySPI_SwapByte(W25Q64_DUMMY_BYTE);  //参数我要给从机一个东西,此时我的目的时接收,所以给它抛的东西没有意义,返回值就是厂商ID号
	*DID = MySPI_SwapByte(W25Q64_DUMMY_BYTE);  //收到设备ID的高8位
	*DID <<= 8;                                //把第一次读到的数据运到DID的高8位                    
	*DID |= MySPI_SwapByte(W25Q64_DUMMY_BYTE); //收到设备ID的低8位(第二次读取需要变为|=,不能写等于,否则高8位就置0)
	MySPI_Stop();                              //停止时序
}

//Write Enable
//写使能,只需要发送一个指令码06即可
void W25Q64_WriteEnable(void)
{
	MySPI_Start();
	MySPI_SwapByte(W25Q64_WRITE_ENABLE);  //发送指令码0x06
	MySPI_Stop();
}

//Read Status Register-1  主要用途是判断芯片是不是忙状态
//等待busy为0
//如果不忙,则很快退出,如果忙,就会卡在函数里面等待
void W25Q64_WaitBusy(void)
{
	uint32_t Timeout;
	MySPI_Start();
	//发送指令码,读状态寄存器1
	MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1);
	Timeout = 100000;
	//接收状态寄存器(主要用途是判断芯片是不是忙状态),等待busy为0状态
	//发送dumy_byte,接收数据,返回值是状态寄存器1,与上0x01,用掩码取出最低位,如果它==0x01,就是busy为1,此时进入while循环等待,否则为0
	//利用连续读出状态寄存器,实现等待BUSY的功能
	while ((MySPI_SwapByte(W25Q64_DUMMY_BYTE) & 0x01) == 0x01)  //防止死循环
	{
		Timeout --;
		if (Timeout == 0)
		{
			break;
		}
	}
	//终止时序
	MySPI_Stop();
}

//Page Program
//页编程,C语言没有24位数据类型,定义32位即可,定义指针传递数组,count表示一次写的多少个字节(256字节,因此定义为16数据类型)
//写数据的数组是输入参数,要写的数据通过数组输入,
void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count)  //数组需要通过指针传递
{
	uint16_t i;
	
	W25Q64_WriteEnable();
	//拼接时序
	//起始
	MySPI_Start();
	//发送指令码
	MySPI_SwapByte(W25Q64_PAGE_PROGRAM);
	//交换发送3个字节地址的数据(由高到低 )
	MySPI_SwapByte(Address >> 16);
	MySPI_SwapByte(Address >> 8);
	MySPI_SwapByte(Address);
	//地址发送完毕,即可一次发送写入的数据
	for (i = 0; i < Count; i ++)
	{
		MySPI_SwapByte(DataArray[i]);
	}
	//停止时序
	MySPI_Stop();
	
	W25Q64_WaitBusy();
}

//Sector Erase
//擦除功能
void W25Q64_SectorErase(uint32_t Address)
{
	//写使能
	W25Q64_WriteEnable();
	//起始
	MySPI_Start();
	//发送擦除扇区指令码20
	MySPI_SwapByte(W25Q64_SECTOR_ERASE_4KB);
	//再发送三个字节的地址,这样指定地址所在的整个扇区就会被擦除
	MySPI_SwapByte(Address >> 16);  //
	MySPI_SwapByte(Address >> 8);
	MySPI_SwapByte(Address);
	//停止
	MySPI_Stop();
	  
	W25Q64_WaitBusy();
}

//Read Data
//读数据的数组是输出参数,读到的参数通过数组输出
//读取数据没有页限制,所以读取count范围非常大
void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count)
{
	uint32_t i;
	MySPI_Start();
	MySPI_SwapByte(W25Q64_READ_DATA);  //发送指令03,
	//再发送3个字节地址
	MySPI_SwapByte(Address >> 16);  
	MySPI_SwapByte(Address >> 8);
	MySPI_SwapByte(Address);
	
	//随后转入接收,可跨页读取,无限制
	//开始读数据(抛砖引玉)
	for (i = 0; i < Count; i ++)
	{
		//发送FF,置换为有用的数据
		//在每次调用交换读取之后,存储器芯片内部地址指针自动自增,依次返回指定地址开始,往后线性区域地址下的数据
		DataArray[i] = MySPI_SwapByte(W25Q64_DUMMY_BYTE);
	}
	//结束
	MySPI_Stop();
}

/*注意*/
//涉及到写入操作的时序有扇区擦除和页编程(写入前需要写使能)
//在每次写操作时序结束后,调用waitBusy,事前等待、事后等待(高效)(写入操作后,芯片进入忙状态)


W25Q64.h

#ifndef __W25Q64_H
#define __W25Q64_H

void W25Q64_Init(void);
void W25Q64_ReadID(uint8_t *MID, uint16_t *DID);
void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count);
void W25Q64_SectorErase(uint32_t Address);
void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count);

#endif

 W25Q64_Ins.h

#ifndef __W25Q64_INS_H
#define __W25Q64_INS_H
//根据W25Q64手册,写出所有的指令名称和指令码
#define W25Q64_WRITE_ENABLE							0x06
#define W25Q64_WRITE_DISABLE						0x04
#define W25Q64_READ_STATUS_REGISTER_1				0x05
#define W25Q64_READ_STATUS_REGISTER_2				0x35
#define W25Q64_WRITE_STATUS_REGISTER				0x01
#define W25Q64_PAGE_PROGRAM							0x02
#define W25Q64_QUAD_PAGE_PROGRAM					0x32
#define W25Q64_BLOCK_ERASE_64KB						0xD8
#define W25Q64_BLOCK_ERASE_32KB						0x52
#define W25Q64_SECTOR_ERASE_4KB						0x20
#define W25Q64_CHIP_ERASE							0xC7
#define W25Q64_ERASE_SUSPEND						0x75
#define W25Q64_ERASE_RESUME							0x7A
#define W25Q64_POWER_DOWN							0xB9
#define W25Q64_HIGH_PERFORMANCE_MODE				0xA3
#define W25Q64_CONTINUOUS_READ_MODE_RESET			0xFF
#define W25Q64_RELEASE_POWER_DOWN_HPM_DEVICE_ID		0xAB
#define W25Q64_MANUFACTURER_DEVICE_ID				0x90
#define W25Q64_READ_UNIQUE_ID						0x4B
#define W25Q64_JEDEC_ID								0x9F
#define W25Q64_READ_DATA							0x03
#define W25Q64_FAST_READ							0x0B
#define W25Q64_FAST_READ_DUAL_OUTPUT				0x3B
#define W25Q64_FAST_READ_DUAL_IO					0xBB
#define W25Q64_FAST_READ_QUAD_OUTPUT				0x6B
#define W25Q64_FAST_READ_QUAD_IO					0xEB
#define W25Q64_OCTAL_WORD_READ_QUAD_IO				0xE3
//在接收数据时交换过去的无用数据
#define W25Q64_DUMMY_BYTE							0xFF

#endif

四、硬件SPI读写W25Q64(代码)

软件SPI就是用代码手动翻转电平,来实现时序,优势是方便灵活

硬件SPI就是使用STM32内部的SPI外设来实现时序,优势是高性能,节省软件资源

SPI外设简介

1、硬件电路自动生成时序,不用手动翻转电平,节省软件资源

2、SPI外设的功能和参数

3、常用:8位(一个字节),高位先行

4、SPI、IIC高位先行

5、串口低位先行

(低位先行要反过来看)

 

时钟频率:传输速率

PCLK:外设时钟

SPI1挂载在APB2,PCLK是72M

SPI2挂载在APB1,PCLK是36M

最高频率是PCLK的2分频,最低频率是PCLK的256分频。

常用:SPI做主机

全双工:一根MOSI用于主机发送,一根MISO用于主机接收

 半双工:去掉其中一根线,只在其中一根线上分时进行发送或接收

单工:直接去掉接收的数据线,再发送数据线进行只发的数据传输,只发模式,只收模式同理

SPI框图 

图上表示的是低位先行:移位寄存器,右边的数据低位一位一位地从MOSI移出去,然后MISO的数据一位一位地移入到左边的数据高位

主从模式切换:

两个缓冲区实际上就是数据寄存器DR。发送数据缓冲区,就是发送数据寄存器TDR,接收缓冲区就是接收数据寄存器RDR。与串口一样,TDR和RDR占用同一个地址,统一叫做DR。

数据寄存器和移位寄存器打配合,可以实现连续的数据流

比如需要连续发送一批数据,第一个数据写入到TDR(发送缓冲区,当移位寄存器没有数据移位时,TDR的数据会立刻转入移位寄存器,开始移位,这个转入时刻,会置状态寄存器的TXE为1,表示发送寄存器为空,当我们检查TXE置1后,紧跟着,下一个数据,就可以提前写入到TDR里等候,一旦上一个数据发送完毕,下一个数据就可以立刻跟进,实现不间断的连续传输。然后移位寄存器这里,一旦有数据过来,它就会自动产生时钟,将数据移出去。在移出的过程中,MISO的数据也会移入。一旦数据移出完成,数据移入也就完成了。这时,移入的数据就会从移位寄存器转入到接收缓冲区RDR,此时,会置状态寄存器的RXNE为1,表示接受寄存器非空,当我们检查RXNE置1后,就要尽快把数据从RDR读出来。在写一个数据到来之前,读出RDR,就可以实现连续接收。否则,如果下一个数据已经收到了,上一个数据还没从RDR读出来,那RDR的数据就会被覆盖。就不能实现连续的数据流。)

简而言之

发送数据先写入TDR,再转到移位寄存器发送,移位寄存器在发送的同时接收数据,接收到的数据转到RDR,再从RDR读取数据。

SPI简洁框图

移位寄存器是左移,高位移出去,通过GPIO到MOSI,从MOSI输出,显然这时SPI的主机

之后移入的数据从MISO进来,通过GPIO,到移位寄存器的低位,这样循环8次,就能实现主机和从机交换一个字节。

然后TDR和RDR的配合可以实现连续的数据流。

TDR数据,整体转入移位寄存器的时刻,置TXE标志位(空),移位寄存器数据,整体转入RDR的时刻,置RXNE标志位(非空)

波特率发生器,产生时钟,输出到SCK引脚

数据控制器就看成是一个管理员,它控制着所有电路的运行。

最后开关控制就是SPI_Cmd, 初始化后,给个ENABLE,使能整个外设。

在一主多从的模型下,使用普通的GPIO模拟的SS是最佳选择。

运行控制部分

如何产生具体时序,什么时候写DR,什么时候读DR

首先,配置还是SPI模式3,SCK默认高电平。我们想要发送数据时,如果检测到TXE=1,TDR为空,就软件写入0xF1到SPI_DR,TDR的值变为F1,TXE变为0(TDR非空),目前移位寄存器为空。所以F1会立刻转入移位寄存器开始发送,波形产生,并且TXE重置为1,表示TDR为空,表示你可以把下一个数据放在TDR里等候。连续传输与非连续传输的区别:此时,TXE=1,不着急把下一个数据写进去,而是一直等待,等第一个字节的时序结束,此时接收第一个字节也完成,RXNE置1,等待RXNE置1后,先把第一个接收到的数据读出来,之后再写入下一个字节数据。

总结:

第一步:等待TXE为1

第二步:写入发送的数据到TDR(当移位寄存器为空,该数据会立刻转入移位寄存器开始发送)

第三步:等待RXNE为1(表示RDR非空)

第四步:读取RDR接收的数据

之后交换第二个字节,重复这4步

因此,可以将以上4步封装为1个函数,调用一次,交换一个字节。

不同SCK的频率,间隙的影响(拖后腿情况)
频率越高,间隙越明显。

软件/硬件波形对比
硬件波形数据线的变化是紧贴SCK边沿的,而软件波形,数据线的变化,再边沿后有一些延迟。

下降沿和低电平期间,都可以作为数据变化的时刻,只是硬件波形会紧贴边沿,软件波形,一半只能在电平期间。

 代码

底层实现由软件改为硬件(初始化和时序执行步骤),在MySPI.c程序修改即可。

基于通讯层的业务代码(W25Q64)不需要更改,因为这些部分只是调用底层的通信函数来实现功能。

SPI的硬件实现:非连续传输方案

初始化流程:

1、开启时钟,开启SPI和GPIO的时钟

2、初始化GPIO口,其中SCK和MOSI是由硬件外设控制的输出信号,所以配置为复用推挽输出,MISO是硬件外设的输入信号,所以可以配置为上拉输入。SS引脚是软件控制的输出信号,所以配置为通用推挽输出。

3、配置SPI外设,使用一个结构体选参数即可,再调用SPI_init(),里面的各种参数,如8位/16位数据帧、高位先行/低位先行,spi模式几,主机还是从机等。

4、开关控制SPI_cmd,给SPI使能。

运行控制部分:产生交换字节的时序

  • 写DR
  • 读DR
  • 获取状态标志位
MySPI.c
#include "stm32f10x.h"                  // Device header

/**
  * 函    数:SPI写SS引脚电平,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初始化
  * 参    数:无
  * 返 回 值:无
  */
void MySPI_Init(void)
{
	/*开启时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);	//开启GPIOA的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);	//开启SPI1的时钟
	
	/*GPIO初始化*/
	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);					//将PA4引脚初始化为推挽输出
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);					//将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引脚初始化为上拉输入
	/*SPI初始化*/
	SPI_InitTypeDef SPI_InitStructure;						//定义结构体变量
	SPI_InitStructure.SPI_Mode = SPI_Mode_Master;			//模式,选择为SPI主模式
	SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;	//方向,选择2线全双工
	SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;		//数据宽度,选择为8位
	SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;		//先行位,选择高位先行
	SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_128;	//波特率分频,选择128分频
	SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low;				//SPI极性,选择低极性
	SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;			//SPI相位,选择第一个时钟边沿采样,极性和相位决定选择SPI模式0
	SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;				//NSS,选择由软件控制
	SPI_InitStructure.SPI_CRCPolynomial = 7;				//CRC多项式,暂时用不到,给默认值7
	SPI_Init(SPI1, &SPI_InitStructure);						//将结构体变量交给SPI_Init,配置SPI1
	
	/*SPI使能*/
	SPI_Cmd(SPI1, ENABLE);	

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/454328.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

数据集成工具 ---- datax 3.0

1、datax: 是一个异构数据源离线同步工具&#xff0c;致力于实现关系型数据库&#xff08;mysql、oracle等&#xff09;hdfs、hive、hbase等各种异构数据源之间的数据同步 2、参考网址文献&#xff1a; https://github.com/alibaba/DataX/blob/master/introduction.mdhttps:/…

代码随想录算法训练营Day46 ||leetCode 139.单词拆分 || 322. 零钱兑换 || 279.完全平方数

139.单词拆分 class Solution { public:bool wordBreak(string s, vector<string>& wordDict) {unordered_set<string> wordSet(wordDict.begin(), wordDict.end());vector<bool> dp(s.size() 1, false);dp[0] true;for (int i 1; i < s.size(); …

【Linux】-Linux下的软件商店yum工具介绍(linux和windows互传文件仅仅一个拖拽搞定!!!!)

目录 1.Linux 软件包管理器yum 1.1快速认识yum 1.2 yumz下载方式&#xff08;如何使用yum进行下载&#xff0c;注意下载一定要是root用户或者白名单用户&#xff08;可提权&#xff09;&#xff09; 1.2.1下载小工具rzsz 1.2.2 rzsz使用 1.2.2查看软件包 1.3软件的卸载 2.yum生…

三、HarmonyOS 应用开发入门之运行Hello World

目录 1、课程对象 1.1、有移动端开发经验 1.2、无移动端开发经验 1.3、对 HarmonyOS 感兴趣 2、DevEco Studio 的使用 2.1、DevEco Studio 的关键特性 智能代码编辑 低代码开发 多段双向实时预览 多端模拟仿真 2.2、安装配置 DevEco Studio 2.2.1、官网开发工具下载地…

蓝桥杯真题讲解:三国游戏(贪心)

蓝桥杯真题讲解&#xff1a;三国游戏&#xff08;贪心&#xff09; 一、视频讲解二、正解代码 一、视频讲解 蓝桥杯真题讲解&#xff1a;三国游戏&#xff08;贪心&#xff09; 二、正解代码 //三国游戏&#xff1a;贪心 #include<bits/stdc.h> #define int long lon…

哪些订单预计会亏?一张报表告诉你

各位数据的朋友&#xff0c;大家好&#xff0c;我是老周道数据&#xff0c;和你一起&#xff0c;用常人思维数据分析&#xff0c;通过数据讲故事。 销售订单一般是企业在销售活动中重要的单据&#xff0c;当我们接到一个客户的订单时&#xff0c;就需要在系统中录入一个销售订…

jQuery模态框弹窗提示代码

jQuery模态框弹窗提示代码 下载地址 jQuery模态框弹窗提示代码

Volatile与JMM

被Volatile修饰的变量有两大特点 可见性 有序性&#xff08;禁重排&#xff09; 如何保证的&#xff1f;内存屏障 Volatile的内存语义 当写一个Volatile变量的时候&#xff0c;JMM会把该线程对应的本地内存共享变量值立即刷新回主内存。 当读一个Volatile变量的时候&…

【Java语言】遍历List元素时删除集合中的元素

目录 前言 实现方式 1.普通实现 1.1 使用【for循环】 方式 1.2 使用【迭代器】方式 2.jdk1.8新增功能实现 2.1 使用【lambda表达式】方式 2.2 使用【stream流】方式 注意事项 1. 使用【for循环】 方式 2. 不能使用增强for遍历修改元素 总结 前言 分享几种从List中移…

程序语言设计

一、程序设计语言及其构成 1.程序设计语言 2.高级程序设计语言划分 3.常见的高级程序语言 4.标记语言 5.程序设计语言的构成 二、表达式 表达式的类型及转换规则 三、传值和传址调用 1.数据类型 2.传值和传址调用 四、语言处理程序 1.语言处理程序 语言处理程序&#xff1…

【JS】浅谈浅拷贝与深拷贝

浅拷贝与深拷贝 前言一、浅拷贝&#xff1f;1.1是什么&#xff1f;1.2做什么&#xff1f;1.3为什么使用&#xff1f;1.4实现方式&#xff1f;1.5 应用场景&#xff1f; 二、深拷贝&#xff1f;2.1是什么&#xff1f;2.2做什么&#xff1f;2.3为什么使用&#xff1f;2.4实现方式…

成都产业园排名出炉!金牛区这个园区成数字产业聚集地

近日&#xff0c;成都产业园排名榜单正式发布&#xff0c;可以看出金牛区成数字产业聚集地&#xff0c;其中&#xff0c;备受瞩目的国际数字影像产业园荣登榜首。这一排名不仅彰显了国际数字影像产业园在数字产业领域的卓越表现&#xff0c;更凸显了成都作为西部重要城市在科技…

51单片机系列-单片机定时器

&#x1f308;个人主页&#xff1a;会编辑的果子君 &#x1f4ab;个人格言:“成为自己未来的主人~” 软件延时的缺点 延时过程中&#xff0c;CPU时间被占用&#xff0c;无法进行其他任务&#xff0c;导致系统效率降低&#xff0c;延时时间越长&#xff0c;该缺点就越明显&…

HBuilder发行微信小程序

首先需要完善mainifest.json中的基本配置 这个需要组测dcloud才可以获取&#xff0c;注册后点击重新获取就可以。 然后发行前还需要完成dcloud的信息&#xff0c;这个他会给你网址 点击连接完成信息填写就可以了 然后就可以发行了。 发行成功后会自动跳转微信小程序&#xff…

day02vue学习

day02 一、今日学习目标 1.指令补充 指令修饰符v-bind对样式增强的操作v-model应用于其他表单元素 2.computed计算属性 基础语法计算属性vs方法计算属性的完整写法成绩案例 3.watch侦听器 基础写法完整写法 4.综合案例 &#xff08;演示&#xff09; 渲染 / 删除 / 修…

Flutter第四弹:Flutter图形渲染性能

目标&#xff1a; 1&#xff09;Flutter图形渲染性能能够媲美原生&#xff1f; 2&#xff09;Flutter性能优于React Native? 一、Flutter图形渲染原理 1.1 Flutter图形渲染原理 Flutter直接调用Skia。 Flutter不使用WebView&#xff0c;也不使用操作系统的原生控件,而是…

如何深度学习

信息爆炸时代&#xff0c;诞生了很多新的学习方式&#xff0c;非常轻松就能掌握知识&#xff0c;比如&#xff0c;每天听一本书&#xff0c;半个小时就能学习一本书的精华&#xff0c;比如订阅名家专栏或者课程&#xff0c;在不长的时间内内就能学到很多知识。 很多人认为这样…

jenkins 使用k8s插件连接k8s集群

jenkins 安装k8s 插件 配置k8s节点 填写k8s 配置信息 生成秘钥 在服务器上面 查看地址 Kubernetes 服务证书 key cat /root/..kube/config 查看秘钥 对秘钥进行base64 位 加密 echo "秘钥内容" | base64 -d -----BEGIN CERTIFICATE----- MIIDITCCAgmgAwIB…

【node】模块化与包(二)

1、模块化的基本概念 模块化是指解决一个复杂的问题时&#xff0c;自顶向下逐层把系统划分成若干模块的过程。对于整个系统来说&#xff0c;模块是可组合、分解和更换的单元。 &#xff08;1&#xff09;模块化的优点 遵循固定规则&#xff0c;把大文件拆分成对立并相互依赖…

【Axure高保真原型】下拉列表切换图表

今天和大家分享通过下拉列表动态切换统计图表的原型模板&#xff0c;我们可以通过下拉列表选择要显示的图表&#xff0c;包括柱状图、条形图、饼图、环形图、折线图、曲线图、面积图、阶梯图、雷达图&#xff1b;而且图表数据可以在左侧表格中动态维护&#xff0c;包括增加修改…