SPI通信协议

1、SPI简介
SPI(Serial Periphreral Interface,串行外设接口)是一种高速的、全双工的、串行的、同步的通信总线。

全双工的意味着SPI至少有两根数据线
串行的意味着按bit一个bit接着一个bit的传输
同步的意味着通信双方有共同的时钟线

SDO : Serial Data Output 	串行数据输出
SDI : Serial Data Input 	串行数据输入
SCK : Serial Clock			时钟线

同样的SPI总线上可以同时接多个SPI设备。

通过谁控制时钟线人为的将SPI总线上的设备分为:

Master	主设备:产生时钟信号的设备
Slave	从设备:接收时钟信号的设备

根据主从设备的分别,有时候两个数据接口命名也会有所不同:

MOSI:Master Output Slave Input 主设备发送,从设备接收线
MISO:Master Input Slave Output 主设备接收,从设备发送线

SPI还有另外一根SS(Slave Select)线

	因为SPI总线上的设备不像IIC总线上一样有地
址,所以我们需要一根额外的线来确定到底与谁进
行通信。

这个SS就是片选信号线,此线可以利用任何一个GPIO引脚,发送0/1进行片选。

所以SPI通信一般需要4根线:

	SDO/SDI/SCK/SS

2、SPI协议

CPOL:CLock Polarity 时钟极性
	时钟极性决定不传任何数据(SPI空闲)时时钟线的电平状态。
	CPOL == 1 	不传数据时,时钟线保持高电平
	CPOL == 0   不传数据时,时钟线保持低电平

CPHA:Clock PHase  时钟相位
	时钟相位决定SPI在何时锁定数据(决定数据采集的时刻)
	CPHA == 1	在时钟线SCK的第二个边沿采集数据
	CPHA == 0	在时钟线SCK的第一个边沿采集数据

CPOL和CPHA的组合决定了数据采集和数据发送的时间。

SPI数据的收发是按帧(Frame)为单位

DFF:Data Frame Format 数据帧格式
	规定的是一帧数据占多少个bit位
	8bit	一帧数据8bit
	16bit	一帧数据16bit

LSBFIRST:决定到底是LSB先发还是MSB先发
	1	LSB(低位)先发
	0	MSB(高位)先发

SPI的传输速率也是由时钟频率决定的。有一个原则:就低不就高。

SPI接线方式

3、STM32F4xx SPI控制器
在这里插入图片描述

4、STM32F4xx 固件库中SPI相关的功能函数
SDO/SDI/SCK都是复用自GPIO口(无论是主从都没有区别)。

如果是作为从设备,则NSS(片选)也是复用自GPIO。
如果是主设备,则不需要利用GPIO去复用为NSS。

1)初始化SPI对应的GPIO引脚
	a. 使能GPIO分组时钟
		RCC_AHB1...
	b. 初始化GPIO为复用功能(NSS为推挽输出   SDO/SDI/SCK复用功能)
		GPIO_Init()
	c. 指定GPIO复用成何种功能
		GPIO_PinAFConfig

2)配置SPI控制器
	a. 使能SPI控制器时钟
		RCC_APBx...
	b. 初始化SPI控制器
		SPI_Init(SPI_TypeDef* SPIx, SPI_InitTypeDef* SPI_InitStruct)
		@SPIx:指定要配置的SPI控制器的编号
					SPI1~SPI6
		@SPI_InitStruct:指向SPI控制器初始化信息结构体

3)使能SPI
	SPI_Cmd(SPI_TypeDef* SPIx, FunctionalState NewState)

4)收发数据
	a. 读取数据:只有RXNE == 1时才能读取
		可以利用中断也可以利用轮询读取数据
	读取标志位:
		FlagStatus SPI_I2S_GetFlagStatus(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG)
		利用轮询读取数据: 
		while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_RXNE) != SET);
		return SPI_ReceiveData(SPI1);
	b. 发送数据,只能在TXE == 1时才能发送数据
void SPI1_Send_Byte(u8 ch)
{
	while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_TXE) != SET);
	SPI_SendData(SPI1,ch);
}

5、W25Q128
是一款SPI接口的Flash存储器芯片。

1)接口说明

	W25Q128			CPU
	  CS			PB14
	  SO			PB4
	  SI			PB5
	  CLK			PB3	
	当CS管脚为高电平时,芯片处在不选择的状态
	当CS管脚为低电平时,芯片处于被选择的状

2)W25Q128内部存储结构

内部的存储地址是24bit,这个24bit的地址分为4部分:
		8bit          4bit 		   4bit       8bit
block number  sector number page number byte number
W25Q128 共有  256  block(块)    一共是65536个字节
		每块 = 16  sector(扇区) 一扇4096个字节(4K)
		每扇 = 16  page(页)     一页256个字节
		每页 = 256 byte(字节)

所以W25Q128存储器容量大小为 = 256 * 16 * 16 * 256 = 2 ^ 24 = 16M
	
反推得知:
	假设某个字节的地址为x,则该字节位于:
			(x >> 16) & 0xFF 块号
			(x >> 12) & 0xF	 扇号
			(x >> 8)  & 0xF  页码
			x & 0xFF 		 字地址

如果要往W25Q128中写入数据的话,需要分为两个步骤:

a.	Erase 擦除
	以块 或者 半块 或者 扇区 或者 整个芯片为单位,清除数据
	
b.	Program写入
	W25Q128芯片所有的操作,全部都是通过指令来进行的。
	通过SPI总线CPU向W25Q128发送指令即可控制。

3)W25Q128的指令
W25Q128BV 共有 34 个基本指令。指令都是以 /CS 下降沿开始的。

-->表示发送指令前,必须先将对应的片选引脚输出
低电平,也就是将MCU的PB14拉低之后才能发送指令。

第一个传输的字节是指令码。
-->指令其实就是一个字节的十六进制数。

在DI上传输的数据是在时钟的上升沿被锁存的。
-->MCU发给W25Q128的数据(指令),必须在时钟的上升沿被锁存,也就是说
W25Q128是在时钟的上升沿去采样。CPU发送数据给W25Q128是下降沿的时候发送。
这句话决定了CPHA和CPOL该如何配置:
		SPI选择模式0(0,0) or 模式3(1,1)
		
MSB首先被传输。
-->高位先发 LSBFIRST-->0

指令是以 /CS 上升沿结束的。
-->MCU将PB14拉高则指令发送完成

所有的读指令可以在任何时钟周期结束。但是,所
有的擦写指令必须在 /CS 拉高之后还有一个 8 位
的时钟间隔,否则前面的擦写指令将被忽略。
-->发送擦写指令后,必须再过8个SPI时钟周期才能发送下一条指令。

3.1)读状态寄存器的指令(指令:0x05/0x35)

	读取状态寄存器1 SR1 -->0x05
	读取状态寄存器2 SR2 -->0x35

MCU向W25Q128发送读取状态寄存器的指令完成后,W25Q128回复一个字节的数据这一个字节的数据就是状态寄存器的值。
		
	SR1:
		SRP0 SEC TB BP2 BP1 BP0 WEL BUSY
			
	BUSY:为1表示W25Q128忙碌,为0表示空闲
		主机在发送指令之前,必须先判断从设备的状态是否忙碌
		BUSY == 0时,才能发送指令
				
	WEL:为1表示写使能,可以写入
		 为0表示写禁止,不能写入
				
	BP2 BP1 BP0:块保护(指定某些块只能读不能写)
	#define W25Q128_ENABLE  GPIO_ResetBits(GPIOB,GPIO_Pin_14)
	#define W25Q128_DISABLE GPIO_SetBits(GPIOB,GPIO_Pin_14)

	u8 Get_W25Q128_SR1(void)
	{
		//使能片选 PB14-->0 
		W25Q128_ENABLE;
		
		Write_Bytes_To_W25Q128(0x05);		//发送读取SR1的指令
		
		u8 sta = SPI_ReceiveData(SPI1);
		
		//禁止片选 PB14-->1
		W25Q128_DISABLE;
		
		return sta;
		
		if(sta & 0x01 != 0)-->意味着W25Q128是忙碌的
	}

3.2)读取W25Q128的ID(指令:0x90)

u16 Read_W25Q128_ID(void)
{
	//使能片选 PB14-->0 
	W25Q128_ENABLE;		

	Write_Bytes_To_W25Q128(0x90);		//发送读取ID的指令
	Write_Bytes_To_W25Q128(0x00);
	Write_Bytes_To_W25Q128(0x00);
	Write_Bytes_To_W25Q128(0x00);
	
	u8 idh8 = SPI_ReceiveData(SPI1);	//制造商ID
	u8 idl8 = SPI_ReceiveData(SPI1);	//设备ID
	
	//禁止片选 PB14-->1
	W25Q128_DISABLE;

	return (idh8 << 8) | idl8;
}


//SPI初始化

//SPI1_Send_A_Byte

//SPI1_Recv_A_Byte

3.3)读取数据

3.4)写入数据

以页为单位进行写入。
		
Flash芯片在写入数据之前,一般要先擦除,所谓擦除就是将页内字节的值都变成0xFF
	
当然在擦除之前,要先进行写使能(指令:0x06)
		
W25Q128的擦除单位是:
	擦除单元		Sector(4K)		Half of Block(32K)  Block(64K)  Chip(整个芯片)
		  指令			  0x20				 0x52			  0xD8		  0xC7/0x60
	
写入的流程:
	a.	写使能
		在擦除/写入之前,先要写使能,才能修改Flash中的内容。

	b.	擦除	
		在擦除/写入时,都需要时间,所以你在擦除/写入的时候要先判断W25Q128是否BUSY。
		
	c.	写入
		一般的,如果写入数据不够擦除空间,为了使空间其它存储单元的数据不变,一般
		是要将要擦除的空间的数据先读取出来保存到数组中,然后再将要写入的数据
		替换到数组对应的位置,最后将把替换后的数组写入到W25Q128中去。
		
		比如:
			写入0x2B到W25Q128的0x123456地址上去。
			
			地址为0x123456的存储单元处于地址为0x123000的扇区上。

写入操作的流程为:

		//先将整个扇区的数据读取出来
		Read_Bytes_From_W25Q128(0x123000,data,4096);
		
		//将要写入的数据替换到数组对应的位置上去
		data[0x456] = 0x2B;

		//在去擦除整个扇区
		W25Q128_Erase_Sector(0x123000);
		
		//等待擦除完毕
		
		//最后将data中的4096个字节重新写进去
		
		//等待写入完成

需要考虑两个问题:

	1.	写入的内容不足1个擦除空间
	2.	写入的内容超过1个擦除空间(此时就需要分多次写入)
/*
	SPI1_Init:对SPI1的初始化
		W25Q128			CPU
		  CS			PB14
		  SO			PB4
		  SI			PB5
		  CLK			PB3
	CS-->输出推挽
	SO/SI/CLK-->复用
*/
void SPI1_Init(void)
{
	//1.配置与SPI相关的GPIO
	//1.1 使能GPIO分组时钟
	RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB,ENABLE);

	//1.2 初始化GPIO
	GPIO_InitTypeDef g;
	g.GPIO_Mode			= GPIO_Mode_OUT;		//输出推挽
	g.GPIO_OType		= GPIO_OType_PP;		
	g.GPIO_Pin			= GPIO_Pin_14;
	g.GPIO_PuPd			= GPIO_PuPd_NOPULL;		
	g.GPIO_Speed		= GPIO_Speed_50MHz;
	GPIO_Init(GPIOB,&g);

	g.GPIO_Mode			= GPIO_Mode_AF;			//复用模式
	g.GPIO_Pin			= GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_5;
	GPIO_Init(GPIOB,&g);

	//1.3 指定复用成什么功能
	GPIO_PinAFConfig(GPIOB,GPIO_PinSource3,GPIO_AF_SPI1);
	GPIO_PinAFConfig(GPIOB,GPIO_PinSource4,GPIO_AF_SPI1);
	GPIO_PinAFConfig(GPIOB,GPIO_PinSource5,GPIO_AF_SPI1);

	//2.配置SPI控制器
	//2.1 使能SPI1控制器时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1,ENABLE);

	//2.2 初始化SPI控制器
	SPI_InitTypeDef s;
	s.SPI_BaudRatePrescaler	= SPI_BaudRatePrescaler_4;
	s.SPI_CPHA				= SPI_CPHA_1Edge;		//W25Q128只支持MODE0(0,0)orMODE3(1,1)
	s.SPI_CPOL				= SPI_CPOL_Low;	
	s.SPI_CRCPolynomial		= 5;		
	s.SPI_DataSize			= SPI_DataSize_8b;
	s.SPI_Direction			= SPI_Direction_2Lines_FullDuplex;	//全双工
	s.SPI_FirstBit			= SPI_FirstBit_MSB;		//W25Q128只支持高位先发
	s.SPI_Mode				= SPI_Mode_Master;		//主模式
	s.SPI_NSS				= SPI_NSS_Soft;			//NSS由软件控制
	SPI_Init(SPI1,&s);

	//3.使能SPI
	SPI_Cmd(SPI1,ENABLE);
}

/*
	SPI1_Send_A_Byte:往SPI1总线上发送一个字节的数据
		@data : 为要发送的数据(1个字节)
*/
void SPI1_Send_A_Byte(u8 data)
{
	//等待发送数据寄存器为空
	while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_TXE) != SET);
	SPI_SendData(SPI1,data);

	/*
		当MCU没有向SPI总线上发送数据的时候,总线是空闲的(没有时钟信号)
		当MCU向SPI上发送数据的时候,MCU开始向SCK线上产生时钟
		SPI开始工作,因为SPI是全双工模式,所以MCU在将data通过MOSI引脚发送
		出去的同时,也通过MISO引脚接收到了一个字节的数据(这个数据是无效的dummy)
		意思就是即使W25Q128没有主动通过MISO向MCU发送数据,但是MCU上的SPI
		控制器任然会自动的从MISO上接收数据,所以应该要对无效数据进行处理
	*/
	while(SPI_GetFlagStatus(SPI1,SPI_FLAG_RXNE) != SET);
	u8 c = SPI_ReceiveData(SPI1);
}

/*
	SPI1_Recv_A_Byte:通过SPI1接收一个字节的数据
		@返回值 : 将接收到的数据进行返回
*/
u8 SPI1_Recv_A_Byte(void)
{
	/*
		为啥先发送一个字节之后才能去接收数据呢?
		因为当MCU没有向SPI总线上发送数据的时候,总线是空闲的(没有时钟信号)
		当MCU作为主设备,SPI控制器只有在MCU主动发送数据的时候才会往SCK线上
		产生时钟,而MCU被动的从W25Q128上接收数据的时候,并不会产生时钟信号
		所以为了能够收到从设备的数据,必须先发送一个无效数据
		主要就是为了在SCK上产生时钟,便于接收数据
	*/
	while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_TXE) != SET);
	SPI_SendData(SPI1,0xFF);
	

	while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_RXNE) != SET);
	return SPI_ReceiveData(SPI1);
}

/*
	Get_W25Q128_SR1:用来获取W25Q128的状态寄存器1的值
*/
u8 Get_W25Q128_SR1(void)
{
	//使能片选 PB14-->0 
	W25Q128_ENABLE;
	
	SPI1_Send_A_Byte(0x05);		//发送读取SR1的指令
	
	u8 sta = SPI1_Recv_A_Byte();

	//printf("%d\r\n",sta);
	//禁止片选 PB14-->1
	W25Q128_DISABLE;
	
	return sta;
	
	//if(sta & 0x01 != 0)-->意味着W25Q128是忙碌的
}

/*
	Read_W25Q128_ID:获取W25Q128的16bit的厂商ID和设备ID
*/
u16 Read_W25Q128_ID(void)
{
	//使能片选 PB14-->0 
	W25Q128_ENABLE; 	

	SPI1_Send_A_Byte(0x90);				//发送读取ID的指令
	SPI1_Send_A_Byte(0x00);
	SPI1_Send_A_Byte(0x00);
	SPI1_Send_A_Byte(0x00);
	
	u8 idh8 = SPI1_Recv_A_Byte();		//制造商ID
	u8 idl8 = SPI1_Recv_A_Byte();		//设备ID
	
	//禁止片选 PB14-->1
	W25Q128_DISABLE;

	printf("0x%x 0x%x\r\n",idh8,idl8);

	return (idh8 << 8) | idl8;
}

/*
	Read_Bytes_From_W25Q128:通过SPI1总线从W25Q128中连续的读取数据
		@addr	: 表示从W25Q128的哪个存储单元开始读取数据
		@data	: 指向的空间用来保存读取出来的数据
		@count	: 表示要读取的数据的个数
		@返回值	: 返回真正读取的字节数
*/
u32 Read_Bytes_From_W25Q128(u32 addr,u8 *data,u32 count)
{
	/*
		读数据指令允许从存储器读一个字节和连续多个字节。该指令是以/CS 拉低开始,
		然后通 DI 在时钟的上升沿来传输指令代码(03H)和 24 位地址。当芯片接受完地址位后,
		相应地址处的值将会,在时钟的下降沿,以高位在前低位在后的方式,在 D0 上传输。
		如果连续的读多个字节的话,地址是自动加 1 的。这意味着可以一次读出整个芯片。该
		指令也是以/CS 拉高来结束的。
	*/
	//使能片选 PB14-->0 
	W25Q128_ENABLE;

	SPI1_Send_A_Byte(0x03);						//发送读指令
	SPI1_Send_A_Byte((addr >> 16) & 0xFF);
	SPI1_Send_A_Byte((addr >> 8) & 0xFF);
	SPI1_Send_A_Byte((addr >> 0) & 0xFF);

	int i;
	for(i = 0;i < count;i++)
	{
		data[i] = SPI1_Recv_A_Byte();
	}

	//禁止片选 PB14-->1
	W25Q128_DISABLE;

	return i;
}

/*
	W25Q128_Write_Enable:W25Q128的写使能
*/
static void W25Q128_Write_Enable(void)
{
	//使能片选 PB14-->0 
	W25Q128_ENABLE;

	SPI1_Send_A_Byte(0x06);						//发送写使能指令

	//禁止片选 PB14-->1
	W25Q128_DISABLE;	
}

/*
	Wait_W25Q128_Is_Not_Busy:等待W25Q128不繁忙
*/
static void Wait_W25Q128_Is_Not_Busy(void)
{
	while(Get_W25Q128_SR1() & 0x01 != 0);
}

/*
	W25Q128_Erase_Sector:擦除指定的扇区(将扇区中的所有的存储单元的值变成0xFF)
		@addr	: 要擦除的扇区的首地址
*/
static void W25Q128_Erase_Sector(u32 addr)
{
	W25Q128_Write_Enable();		//写使能

	Wait_W25Q128_Is_Not_Busy();	//等待W25Q128不繁忙
 
	W25Q128_ENABLE;				//使能片选 PB14-->0

	SPI1_Send_A_Byte(0x20);		//发送扇区擦写指令

	SPI1_Send_A_Byte((addr >> 16) & 0xFF);	//A23-A16
	SPI1_Send_A_Byte((addr >> 8) & 0xFF);	//A15-A8
	SPI1_Send_A_Byte((addr >> 0) & 0xFF);	//A7-A0

	W25Q128_DISABLE;			//禁止片选 PB14-->1

	Wait_W25Q128_Is_Not_Busy();	//等待W25Q128擦写完成
}

/*
	W25Q128_Page_Program:对W25Q128进行页写
		@addr	: 进行页写的页的首地址
		@data	: 要写进去的数据
		@count	: 需要写进去的字节的个数
*/
void W25Q128_Page_Program(u32 addr,u8 *data,int count)
{
	//计算count的合法性
	//当前页最多还能写入max_bytes个字节
	int max_bytes = 256 - (addr & 0xFF);
	count = (count > max_bytes) ? max_bytes : count;

	W25Q128_Write_Enable();		//写使能

	Wait_W25Q128_Is_Not_Busy();	//等待W25Q128不繁忙

	W25Q128_ENABLE;				//使能片选 PB14-->0

	SPI1_Send_A_Byte(0x02);		//发送页写指令

	SPI1_Send_A_Byte((addr >> 16) & 0xFF);	//A23-A16
	SPI1_Send_A_Byte((addr >> 8) & 0xFF);	//A15-A8
	SPI1_Send_A_Byte((addr >> 0) & 0xFF);	//A7-A0

	int i;
	for(i = 0;i < count;i++)
	{
		SPI1_Send_A_Byte(data[i]);
	}

	W25Q128_DISABLE;			//禁止片选 PB14-->1

	Wait_W25Q128_Is_Not_Busy();	//等待W25Q128页写完成
}

//用来临时保存扇区数据的缓冲区
u8 buffer[SECTOR_SIZE] = {0};

/*
	Write_Bytes_To_W25Q128:通过SPI1总线将数据连续写入到W25Q128中去
		@addr 	: 表示从W25Q128的哪个存储单元中开始写入数据
		@data	: 指向的空间中保存的就是要写入的数据
		@count 	: 表示要写入的字节数
		@返回值 : 成功写进去的字节数
*/
u32 Write_Bytes_To_W25Q128(u32 addr,u8 *data,u32 count)
{
	//判断count的合法性
	u32 max_bytes = (1 << 24) - addr;				//从addr开始能够写入的最大的字节数
	count = (count > max_bytes) ? max_bytes : count;//计算出实际能够读取到的字节数

	//计算出第一个扇区的地址
	u32 first_sector = addr & 0xFFF000;				

	//计算出最后一个扇区的地址
	u32 last_sector = (addr + count) & 0xFFF000;

	//已经写入的字节数
	u32 bytes = 0;
	u32 sector_addr;
	//从第一个扇区遍历到最后一个扇区
	for(sector_addr = first_sector;sector_addr <= last_sector;sector_addr += SECTOR_SIZE)
	{
		//1.先将整个扇区的数据读取出来
		Read_Bytes_From_W25Q128(sector_addr,buffer,SECTOR_SIZE);

		//2.将要写入到W25Q128中的数据替换到数组的对应的位置上去
		//先将当前扇区要替换的字节数计算出来
		u32 modify_bytes = 4096 - (addr & 0xFFF);

		//然后计算出从data的哪个位置开始替换
		u32 offset = addr - sector_addr;	//addr & 0xFFF

		//替换工作
		int i;
		for(i = 0;i < modify_bytes && bytes < count;i++,bytes++)
		{
			//data ---> buffer
			buffer[offset + i] = data[bytes];
		}

		//当前扇区替换完后addr指向下一个扇区
		addr += modify_bytes;

		//3.擦除扇区
		W25Q128_Erase_Sector(sector_addr);

		u32 page_addr = sector_addr;
		//4.写入扇区
		for(i = 0;i < 16;i++)
		{
			//一次写入最多写入256个字节(一页),所以需要分十六次
			W25Q128_Page_Program(page_addr,&buffer[256*i],256);

			page_addr += 256;
		}
	}

	return bytes;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

m0_60265426

都是好兄弟

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

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

打赏作者

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

抵扣说明:

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

余额充值