【STM32】SPI通信

1 SPI通信

SPI(Serial Peripheral Interface,串行外设接口)是由Motorola公司开发的一种通用数据总线

四根通信线:SCKSerial Clock,串行时钟)、MOSIMaster Output Slave Input,主机输出从机输入)、MISOMaster Input Slave Output,主机输入从机输出)、SSSlave Select,从机选择

同步,全双工

MOSI接到主机上就是输出,接到从机上就是输入;

MISO接到主机上就是输入,接到从机上就是输出。

支持总线挂载多设备(仅支持一主多从)

逻辑是:在起始条件后,主机必须先发送一个字节进行寻址,用来指定我要跟哪个从机进行通信(I2C这里涉及分配地址和寻址的问题),SPI直接使用一条通信线选择和哪个从机进行通信(SS,从机选择,不止一条)

SPI没有应答机制

W25Q64(Flash存储器)、OLED、2.4G无线通信模块(NRF2401)、SD卡

1.1 硬件电路

所有SPI设备的SCKMOSIMISO分别连在一起

主机另外引出多条SS控制线,分别接到各从机的SS引脚

输出引脚配置为推挽输出,输入引脚配置为浮空或上拉输入

推挽输出的高低电平均有很强的驱动能力,这将使得SPI引脚信号的下降沿非常迅速,上升沿也非常迅速。当从机的SS引脚为高电平时,也就是从机没有被选中时,它的MISO引脚,必须切换为高阻态(相当于断开,不输出任何电平),防止一条线有多个输出,产生冲突。

图中清晰明了

细节:

(1)主机的时钟线SCK是输出,从机的SCK时钟线是输入,主机与从机时钟同步;

(2)SS高电平不选择从机,低电平才选择从机。同一时刻主机只能选择一个从机。

1.1.1 移位示意图

左边是SPI主机,里面有8位的移位寄存器,右边是SPI从机,里面也有8位移位寄存器。这里移位寄存器有一个时钟输入端,因为SPI一般都是高位先行,所以每来一个时钟,移位寄存器就会向左进行移位,从机中的移位寄存器也是如此。移位寄存器的时钟源是由主机提供的,这里叫做波特率发生器,它产生的时钟驱动主机的移位寄存器进行移位,同时这个时钟也通过SCK时钟线进行输出接到从机的移位寄存器里。之后上面移位寄存器的接法是主机移位寄存器左边移出去的数据,通过MOSI引脚输入到从机移位寄存器的右边,从机移位寄存器左边移出去的数据通过MISO引脚输入到主机移位寄存器的右边。

工作流程:规定波特率时钟的上升沿,所有移位寄存器向左移动一位,移出去的位放在引脚上;波特率发生器时钟的下降沿引脚上的位采样输入到移位寄存器的最低位。

假设主机有1010 1010数据需要发送到从机,同时从机有0101 0101数据需要发送到主机。先驱动时钟,先产生一个上升沿,这时所有位都会向左移动一位。此时MOSI的数据是1,所以MOSI的电平是高电平;MOSI的数据是0,所以MISO的电平是低电平。如图所示。

之后时钟继续运行,上升沿之后下一个边沿就是下降沿,在下降沿的时候,主机和从机都会进行数据采样输入,如图所示。这是第一个时钟结束后的现象。

之后以此类推,8次后完成数据的发送和接收。SPI通信的基础是交换一个字节。

只想接收或者发送,不用管另一方即可。

1.1.2 SPI时序基本单元

(1)起始条件:SS从高电平切换到低电平

(2)终止条件:SS从低电平切换到高电平

从机的选中过程中,SS始终保持在低电平。

(3)交换一个字节(模式1)

CPOL:clock polarity时钟极性

CPHA:clock phase,时钟相位

CPOL=0:空闲状态时,SCK为低电平

CPHA=1:SCK第一个边沿移出数据,第二个边沿移入数据(数据采样)

SS从机选择,通信开始前是高电平,在通信过程中SS保持低电平,通信结束,SS恢复高电平。

在SS高电平时,MISO(主机输入,从机输出)用中间的一条线表示高阻态,SS下降沿之后,从机的MISO被允许开始输出;SS上升沿之后,从机的MISO必须置会高组态。

SCK的第一个边沿是上升沿,主机和从机同时移出数据,主机通过MOSI移出最高位,此时MOSI的电平就表示主机要发送的数据B7;从机通过MISO移出最高位,此时MISO表示从机要发送数据的B7;然后时钟运行,产生下降沿,此时主机和从机同时移入数据,也就是进行数据采样。这里主机移出的B7进入从机移位寄存器的最低位;从机移出的B7进入主机移位寄存器的最低位,这样一个脉冲产生完毕,一个数据位传输完毕。接下来以此类推。

(4)交换一个字节(模式0)

CPOL=0:空闲状态时,SCK为低电平

CPHA=0:SCK第一个边沿移入数据,第二个边沿移出数据

相比模式1相位提前了, SCK第一个边沿之前就要开始移出数据了,或者是第0个边沿移出,第1个边沿移入。首先SS下降沿开始通信,现在SCK还没有变化,但是SCK一旦开始变化,就要移入数据了,所以此时趁SCK还没有变化,SS下降沿时,就要立刻触发移位输出,所以这里MOSI和MISO的输出是对齐到SS下降沿的,或者说这里把SS的下降沿也作为时钟的一部分了,SS下降沿触发了输出,SCK上升沿就可以进行数据输入采样了,这样B7就传输完毕,之后SCK下降沿,移出B6,SCK上升沿移入B6,以此类推,SCK下降沿移出数据,上升沿移入数据

模式0是最常见的模式,很多SPI设备都支持这种模式,因此兼容性较好。
模式0可以实现较高的传输速率,因为数据在时钟的上升沿就被采样,不需要等待下降沿。

(5)交换一个字节(模式2)

CPOL=1:空闲状态时,SCK为高电平

CPHA=0:SCK第一个边沿移入数据,第二个边沿移出数据

模式2和模式0波形相反。

(6)交换一个字节(模式3)

CPOL=1:空闲状态时,SCK为高电平

CPHA=1:SCK第一个边沿移出数据,第二个边沿移入数据

模型3和模式1的波形相反。

1.1.3 SPI时序

以W25Q64为例。SPI是指令码+数据

(1)发送指令

向SS指定的设备,发送指令(0x06,W25Q64写使能

SS下降沿时刻,时序开始,此时MOSI和MISO就要开始变化数据了。MOSI由于指令码最高位仍然是0,所以这里保持低电平不变;MISO从机没有数据发送给主机,引脚电平没有变换,STM32的MISO是上拉输入,所以这里呈现高电平。之后SCK第一个上升沿,进行数据采样,从机采样输入,得到0,主机采样输入,得到1。之后继续,第二个时钟,主机数据仍然是0,所以波形没有变化,然后一位一位的发送接收发送接收,到主机发送1时才发生变化,下降沿,数据移出,主机将1移出到MOSI,MOSI变为高电平;然后SCK上升沿,数据采样输入,在最后一位下降沿,数据变化,MOSI变为0;上升沿数据采样,从机接收数据0,SCK低电平是变化的时期,高电平是读取的时期。时序SCK最后一个上升沿结束,一个字节就交换完成了。主机用0x06换来了从机的0xFF

(2)指定地址写

向SS指定的设备,发送写指令(0x02),随后在指定地址(Address[23:0])下,写入指定数据(Data

写指令之后的字节,定义为地址高位(0x12,23~16),0x34(15~8),0x56(7~0),即0x123456,继续交换,写入0x55

(3)指定地址读

向SS指定的设备,发送读指令(0x03),随后在指定地址(Address[23:0])下,读取从机数据(Data

主机交换从机地址0x123456下的数据。即指定地址读,读的是0x55

1.2 W25Q64简介

W25Qxx系列是一种低成本、小型化、使用简单的非易失性存储器,常应用于数据存储、字库存储、固件程序存储等场景

存储介质:Nor Flash(闪存)

时钟频率:80MHz / 160MHz (Dual SPI) / 320MHz (Quad SPI)

存储容量(24位地址,3个字节):

  W25Q40    4Mbit / 512KByte

  W25Q80    8Mbit / 1MByte

  W25Q16    16Mbit / 2MByte

  W25Q32    32Mbit / 4MByte

  W25Q64    64Mbit / 8MByte

  W25Q128  128Mbit / 16MByte

  W25Q256  256Mbit / 32MByte

1.2.1 硬件电路

WP写保护和HOLD这两个引脚如果不需要也可以充当数据传输引脚。加上MISO、MOSI就可以4个数据同时收发了。

SI/和SI上面有一杠表示低电平有效。

引脚

功能

VCCGND

电源(2.7~3.6V

CSSS

SPI片选

CLKSCK

SPI时钟

DIMOSI

SPI主机输出从机输入

DOMISO

SPI主机输入从机输出

WP

写保护

HOLD

数据保持

WP写保护,低电平有效,低电平不让写;高电平可以写。

HOLD数据保持,低电平有效。

1.2.2 W25Q64框图

右上角是存储器的规划示意图。block\sector\page

地址空间从0x00 00 00~0x7F FF FF,在这整个空间中,以64kb为一个基本的单元,划分成若干个块block,从前往后以此是块0.1.2....等等,最后一块是127。观察块内起始地址,第0块从00 00 00到00 FF FF;每个块的起始地址是xx 00 00,结束地址是xx FF FF。

左上是对每一块block进行更细的划分,分为多个扇区sector,在sector中,以4kb为一个单元,进行切分,总共分16个,每个扇区内的地址是xx x0 00~xx xF FF。

在写入的时候还有更细的划分,就是页page,页是对整个存储控件划分的,也可以看作是在扇区里再划分,页的大小是256个字节,一个扇区是4kb,以256字节划分,可以分为16个页,一行就是一页,页的地址是xx xx 00~xx xx FF。

左下角是SPI控制逻辑,也就是芯片内部进行地址锁存、数据读写等操作,由芯片自动完成。 控制逻辑的左边是SPI的通信引脚;状态寄存器可以检查芯片是否忙状态、是否写使能、是否写保护;上面的写控制逻辑配合WP写保护的;接下来是高电压生成器,配合flash进行编程的(flash掉电不丢失);接下来是页地址锁存/计数器和字节地址锁存/计数器,这两个地址锁存和计数器就是用来指定地址的,通过SPI总共发过来3个字节的地址,因为一页是256字节,所以一页内的字节地址,就取决于最低一个字节,而高位的两个字节就对应的是页地址,所以发的三个字节地址,前两个字节,会进入到页地址锁存计数器里,最后一个字节,会进入到字节地址锁存计数器里;然后页地址通过写保护和行解码,来选择要操作哪一页;字节地址通过列解码和256字节页缓存,来进行指定字节的读写操作。地址指针在读写之后,就可以自动加1,就可以实现从指定地址开始,连续读写多个字节的操目的了。右边有个256字节的页缓存区,是个RAM存储器,数据读写通过这个RAM缓冲区来进行的(读一次性写的数据有限制)。

1.2.3 Flash操作注意事项

写入操作时:

(1)写入操作前,必须先进行写使能

(2)每个数据位只能由1改写为0,不能由0改写为1

(3)写入数据前必须先擦除,擦除后,所有数据位变为1

(4)擦除必须按最小擦除单元进行(整个/块/扇区,4069)

(5)连续写入多字节时,最多写入一页的数据(256字节),超过页尾位置的数据,会回到页首覆盖写入

(6)写入操作(包括擦除)结束后,芯片进入忙状态,不响应新的读写操作

读取操作时:

(1)直接调用读取时序,无需使能,无需额外操作,没有页的限制,读取操作结束后不会进入忙状态,但不能在忙状态时读取

手册

BUSY

BUSY是状态寄存器(S0)中的一个只读位,当设备执行页程序、扇区擦除、块擦除、芯片擦除或写状态寄存器指令时,它被设置为1状态。在此期间,设备将忽略除读状态寄存器和擦除暂停指令之外的其他指令(参见交流特性中的tW, tPP, tSE, be和tCE)。当程序、擦除或写状态寄存器指令完成时,BUSY位将被清除为0状态,表明设备已准备好接受进一步的指令。

WEL

写使能锁存(WEL)是状态寄存器(S1)中的一个只读位,在执行写使能指令后被设置为1。当设备禁止写时,WEL状态位被清除为0。写禁用状态发生在上电或以下任何指令之后:写禁用、页程序、扇区擦除、块擦除、芯片擦除和写状态寄存器。(顺手关门)

写使能、写失能(查看忙状态)、页编程(写数据,A23-0指定地址,D7-D0是写入数据)、擦除指令、读ID号指令

读取数据(交换发送指令03,之后交换发送3个字节地址,再之后就可以交换读取了)

电器特性

2 软件SPI读写W25Q64

2.1 接线图

框架:首先建立SPI.c通信层模块,在这个模块里主要包含通信引脚封装、初始化以及SPI通信的3个拼图(起始、终止、交换一个字节);然后基于SPI层,再建立一个W25Q64的模块,在这个模块里调用底层SPI的拼图,来拼接各种指令和功能的完整时序,比如写使能、擦除、页编程、读数据等等,所以这一块可以叫做W25Q64的硬件驱动层;最后在主函数中调用驱动层的函数,完成相应的功能。

2.2 模块封装

函数封装

// 从机选择,输出引脚,即写SS的引脚
void MySPI_W_SS(uint8_t bitValue)
{
	// CS对应PA4
	GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)bitValue);
}

// 写时钟函数CLK
void MySPI_W_SCK(uint8_t bitValue)
{
	GPIO_WriteBit(GPIOA, GPIO_Pin_5, (BitAction)bitValue);
}

// 主机输出引脚MOSI
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);
}

初始化

// 初始化
void MySPI_Init(void)
{
	/*开启时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);		// 开启GPIOA的时钟
	
	/*GPIO初始化*/
	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;						// PA6输入引脚为上拉(浮空)
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	// 置初始化之后引脚的默认电平
	MySPI_W_SS(1);					// SS置为高电平,默认不选择从机
	MySPI_W_SCK(0);					// 使用SPI的模式0
}

起始和终止信号

// 起始信号
void MySPI_Start(void)
{
	MySPI_W_SS(0);					// SS置为低电平
}

// 终止信号
void MySPI_Stop(void)
{
	MySPI_W_SS(1);					// SS置为高电平
}

四种模式

// 交换一个字节,模式0,参数是主机要交换的数据,返回值是从机要交换的数据
uint8_t MySPI_SwapByte(uint8_t byteSend)
{
	uint8_t i, byteReceive = 0x00;
	
//	// 先SS下降沿或者SCK下降沿,再数据移出;再SCK上升沿,再移入数据
//	MySPI_W_MOSI(byteSend & 0x80);					// 主机写,主机移数据
//	MySPI_W_SCK(1);									// SCK上升沿
//	// 主机读数据,存最高位
//	if (MySPI_R_MISO() == 1)
//	{
//		byteReceive |= 0x80;
//	}
//	MySPI_W_SCK(0);									// SCK下降沿,主机移出下一位
//	MySPI_W_MOSI(byteSend & (0x40));				// 主机写,主机移数据
	
	
//	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);	
//	}
//	return byteSend;
	
	for (i = 0; i < 8; i++)
	{
		MySPI_W_MOSI(byteSend & (0x80 >> i));
		MySPI_W_SCK(1);
		if (MySPI_R_MISO() == 1)
		{
			byteReceive |= (0x80 >> i);
		}
		MySPI_W_SCK(0);	
	}
	return byteReceive;
}

// 模式1
//uint8_t MySPI_SwapByte(uint8_t byteSend)
//{
//	uint8_t i, byteReceive = 0x00;
//	for (i = 0; i < 8; i++)
//	{
//		MySPI_W_SCK(1);
//		MySPI_W_MOSI(byteSend & (0x80 >> i));
//		MySPI_W_SCK(0);	
//		if (MySPI_R_MISO() == 1)
//		{
//			byteReceive |= (0x80 >> i);
//		}
//	}
//	return byteReceive;
//}

// 模式3
//uint8_t MySPI_SwapByte(uint8_t byteSend)
//{
//	uint8_t i, byteReceive = 0x00;
//	for (i = 0; i < 8; i++)
//	{
//		MySPI_W_SCK(0);
//		MySPI_W_MOSI(byteSend & (0x80 >> i));
//		MySPI_W_SCK(1);	
//		if (MySPI_R_MISO() == 1)
//		{
//			byteReceive |= (0x80 >> i);
//		}
//	}
//	return byteReceive;
//}

// 模式2
//uint8_t MySPI_SwapByte(uint8_t byteSend)
//{
//	uint8_t i, byteReceive = 0x00;
//	for (i = 0; i < 8; i++)
//	{
//		MySPI_W_MOSI(byteSend & (0x80 >> i));
//		MySPI_W_SCK(0);
//		if (MySPI_R_MISO() == 1)
//		{
//			byteReceive |= (0x80 >> i);
//		}
//		MySPI_W_SCK(1);	
//	}
//	return byteReceive;
//}

总的MySPI.c

#include "stm32f10x.h"                  // Device header

// 从机选择,输出引脚,即写SS的引脚
void MySPI_W_SS(uint8_t bitValue)
{
	// CS对应PA4
	GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)bitValue);
}

// 写时钟函数CLK
void MySPI_W_SCK(uint8_t bitValue)
{
	GPIO_WriteBit(GPIOA, GPIO_Pin_5, (BitAction)bitValue);
}

// 主机输出引脚MOSI
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);
}

// 初始化
void MySPI_Init(void)
{
	/*开启时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);		// 开启GPIOA的时钟
	
	/*GPIO初始化*/
	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;						// PA6输入引脚为上拉(浮空)
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	// 置初始化之后引脚的默认电平
	MySPI_W_SS(1);					// SS置为高电平,默认不选择从机
	MySPI_W_SCK(0);					// 使用SPI的模式0
}

// 起始信号
void MySPI_Start(void)
{
	MySPI_W_SS(0);					// SS置为低电平
}

// 终止信号
void MySPI_Stop(void)
{
	MySPI_W_SS(1);					// SS置为高电平
}

// 交换一个字节,模式0,参数是主机要交换的数据,返回值是从机要交换的数据
uint8_t MySPI_SwapByte(uint8_t byteSend)
{
	uint8_t i, byteReceive = 0x00;	
	for (i = 0; i < 8; i++)
	{
		MySPI_W_MOSI(byteSend & (0x80 >> i));
		MySPI_W_SCK(1);
		if (MySPI_R_MISO() == 1)
		{
			byteReceive |= (0x80 >> i);
		}
		MySPI_W_SCK(0);	
	}
	return byteReceive;
}

W25Q64.c(部分)

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

// 初始化函数
void W25Q64_Init(void)
{
	MySPI_Init();
}

// 读设备ID,厂商ID和设备ID
void W25Q64_ReadID(uint8_t* MID, uint16_t* DID)
{
	MySPI_Start();							// 开始
	MySPI_SwapByte(0x9F);					// 交换0x9f,主机读这个ID号
	*MID = MySPI_SwapByte(0xFF);			// 参数无意义,得到厂商ID
	*DID = MySPI_SwapByte(0xFF);			// 参数无意义,得到设备ID高8位
	*DID <<= 8;
	*DID |= MySPI_SwapByte(0xFF);			// 参数无意义,得到设备ID低8位
	MySPI_Stop();							// 终止
}

主函数测试

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

uint8_t MID;
uint16_t DID;

int main()
{
	OLED_Init();								// 初始化OLED
	W25Q64_Init();
	
	W25Q64_ReadID(&MID, &DID);
	
	OLED_ShowHexNum(1, 1, MID, 2);
	OLED_ShowHexNum(1, 8, DID, 4);
	
	while (1)
	{
		
	}
}

OLED显示:EF     4017

继续完善

封装指令 W25Q64_Ins.h

#ifndef __W25Q64_INS_H
#define __W25Q64_INS_H

#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

最终W25Q64.c

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

// 初始化函数
void W25Q64_Init(void)
{
	MySPI_Init();
}

// 读设备ID,厂商ID和设备ID
void W25Q64_ReadID(uint8_t* MID, uint16_t* DID)
{
	MySPI_Start();								// 开始
	MySPI_SwapByte(W25Q64_JEDEC_ID);			// 交换0x9f,主机读这个ID号
	*MID = MySPI_SwapByte(W25Q64_DUMMY_BYTE);	// 参数无意义,得到厂商ID
	*DID = MySPI_SwapByte(W25Q64_DUMMY_BYTE);	// 参数无意义,得到设备ID高8位
	*DID <<= 8;
	*DID |= MySPI_SwapByte(W25Q64_DUMMY_BYTE);	// 参数无意义,得到设备ID低8位
	MySPI_Stop();								// 终止
}

// 写使能
void W25Q64_WriteEnable(void)
{
	MySPI_Start();								// 开始
	MySPI_SwapByte(W25Q64_WRITE_ENABLE);		// 写使能
	MySPI_Stop();								// 终止
}

// 等待busy位0,即不忙
// 读状态寄存器1,判断芯片是不是忙状态(最低位是不是busy/1)
void W25Q64_WaitBusy(void)
{
	uint32_t timeOut = 100000;
	MySPI_Start();													// 开始
	MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1);					// 读状态寄存器1
	// 参数无意义,得到状态寄存器1
	while ((MySPI_SwapByte(W25Q64_DUMMY_BYTE) & 0x01) == 0x01)
	{
		timeOut--;
		if (timeOut == 0)
		{
			break;
		}
	}
	MySPI_Stop();													// 终止
}

// 页编程函数,数据范围是0~256
// 地址,写数据,写数据的数量
void W25Q64_PageProgram(uint32_t address, uint8_t* dataArray, uint16_t count)
{
	W25Q64_WriteEnable();											// 写使能
	MySPI_Start();													// 开始
	MySPI_SwapByte(W25Q64_PAGE_PROGRAM);							// 页编程
	// 发地址
	MySPI_SwapByte(address >> 16);									// 高位地址
	MySPI_SwapByte(address >> 8);									// 中间地址
	MySPI_SwapByte(address);										// 最低位地址
	// 发数据
	for (uint16_t i = 0; i < count; i++)
	{
		MySPI_SwapByte(dataArray[i]);
	}
	MySPI_Stop();													// 终止
	W25Q64_WaitBusy();												// 等待busy结束再退出,事后等待;也可以事前等待
}

// 擦除的功能(扇区擦除)
void W25Q64_SectorErase(uint32_t address)
{
	W25Q64_WriteEnable();											// 写使能
	MySPI_Start();													// 开始
	MySPI_SwapByte(W25Q64_SECTOR_ERASE_4KB);						// 擦除扇区
	// 发地址
	MySPI_SwapByte(address >> 16);									// 高位地址
	MySPI_SwapByte(address >> 8);									// 中间地址
	MySPI_SwapByte(address);										// 最低位地址
	MySPI_Stop();													// 终止
	W25Q64_WaitBusy();												// 等待busy结束再退出,事后等待;也可以事前等待
}

// 读数据
// dataArray输出参数
void W25Q64_ReadData(uint32_t address, uint8_t* dataArray, uint32_t count)
{
	MySPI_Start();													// 开始
	MySPI_SwapByte(W25Q64_READ_DATA);								// 读数据
	// 发地址
	MySPI_SwapByte(address >> 16);									// 高位地址
	MySPI_SwapByte(address >> 8);									// 中间地址
	MySPI_SwapByte(address);										// 最低位地址
	// 发数据
	for (uint32_t i = 0; i < count; i++)
	{
		dataArray[i] = MySPI_SwapByte(W25Q64_DUMMY_BYTE);			// 发送FF,置换回有用的数据
	}
	MySPI_Stop();													// 终止
}

2.3 主函数

测试1:测试断电不丢失和擦除之后是FF,读的是FF FF FF FF

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

uint8_t MID;
uint16_t DID;

uint8_t arrayWrite[] = {0xA1, 0xB2, 0xC3, 0xD4};
uint8_t arrayRead[4];

int main()
{
	OLED_Init();								// 初始化OLED
	W25Q64_Init();
	OLED_ShowString(1, 1, "MID:   DID:");
	OLED_ShowString(2, 1, "W:");
	OLED_ShowString(3, 1, "R:");
	
	W25Q64_ReadID(&MID, &DID);
	OLED_ShowHexNum(1, 5, MID, 2);
	OLED_ShowHexNum(1, 12, DID, 4);
	
	// 这两行全部注释测试断电不丢失
	// 放开擦除,测试擦除之后是FF,读的是FF FF FF FF
	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)
	{
		
	}
}

测试2:Flash只能1写0,不能0写1

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

uint8_t MID;
uint16_t DID;

uint8_t arrayWrite[] = {0x55, 0x66, 0x77, 0x88};    // 先是{0xAA, 0xBB, 0xCC, 0xDD},修改成{0x55, 0x66, 0x77, 0x88}
uint8_t arrayRead[4];

int main()
{
	OLED_Init();								// 初始化OLED
	W25Q64_Init();
	OLED_ShowString(1, 1, "MID:   DID:");
	OLED_ShowString(2, 1, "W:");
	OLED_ShowString(3, 1, "R:");
	
	W25Q64_ReadID(&MID, &DID);
	OLED_ShowHexNum(1, 5, MID, 2);
	OLED_ShowHexNum(1, 12, DID, 4);
	
	// 开始不注释这里,下载到板子,上面数组改成{0x55, 0x66, 0x77, 0x88}时注释,输出是00 22 44 88
	// 1010 1010    1011 1011    1100 1100   1101 1101
	// 0101 0101    0110 0110    0111 0111   1000 1000
	// 0000 0000    0010 0010    0100 0100   1000 1000
	// 原始数据&写入的数据=读出数据
//	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)
	{
		
	}
}

原始数据&写入的数据=读出数据。写入数据前要擦除。

测试3:写入数据不能跨页

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

uint8_t MID;
uint16_t DID;

uint8_t arrayWrite[] = {0x55, 0x66, 0x77, 0x88};
uint8_t arrayRead[4];

int main()
{
	OLED_Init();								// 初始化OLED
	W25Q64_Init();
	OLED_ShowString(1, 1, "MID:   DID:");
	OLED_ShowString(2, 1, "W:");
	OLED_ShowString(3, 1, "R:");
	
	W25Q64_ReadID(&MID, &DID);
	OLED_ShowHexNum(1, 5, MID, 2);
	OLED_ShowHexNum(1, 12, DID, 4);
	
	// 页地址的范围是xxxx00 ~ xxxxxFF
	W25Q64_SectorErase(0x000000);							// 擦除扇区
	// 一页的最后一个地址0x0000FF
	// 读取可以跨页,写入不能跨页(默认FF)
	W25Q64_PageProgram(0x0000FF, arrayWrite, 4);			// 写入
	
//	W25Q64_ReadData(0x0000FF, arrayRead, 4);				// 从0x0000FF读数据,读到的是 55 FF FF FF
	W25Q64_ReadData(0x000000, arrayRead, 4);				// 从0x000000读数据,读到的是 66 77 88 FF
	
	
	// 测试写
	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)
	{
		
	}
}

解释

3 SPI通信外设

STM32内部集成了硬件SPI收发电路,可以由硬件自动执行时钟生成、数据收发等功能,减轻CPU的负担

可配置8/16位数据帧、高位先行/低位先行

时钟频率: fPCLK / (2, 4, 8, 16, 32, 64, 128, 256)

一个SCK时钟交换一个bit。PCLK是外设时钟,APB1是36MHz,APB2是72MHz。

SPI1和SPI2挂载的总线不一样,SPI1挂在APB2上,SPI2挂在APB1上

串口低位先行

支持多主机模型、主或从操作

可精简为半双工/单工通信

支持DMA

兼容I2S协议

STM32F103C8T6 硬件SPI资源:SPI1SPI2

3.1 SPI框图

可以分为两部分,左上角和其余部分。左上角就是数据寄存器和移位寄存器打配合的过程,与串口/I2C相似。核心部分是移位寄存器,右边的数据低位,一位一位地从MOSI移出去;MISO的数据一位一位地,移入到左边的数据高位(图上是低位先行)。右下角有一个LSBFIRST控制位,可以控制高/低先行。

MSB是高位,LSB是低位

STM32做主机,左上角MOSI和MISO就不交叉,MOSI是MO主机输出,MISO是MI主机输入;当STM32做从机时,MOSI是SI从机输入,MISO是SO从机输出,两者交叉。上下两个缓冲区就是数据寄存器DR,下面是发送数据寄存器TDR,上面是接收数据寄存器RDR。写入的时候,数据写入到发送缓冲区TDR,读取DR时,数据从接收缓冲区RDR读出。

具体是:要连续发送一批数据,第一个数据写入到TDR,当移位寄存器没有数据移位时,TDR的数据会立刻转移到移位寄存器里,开始移位,这个转入时刻,会置状态寄存器的TXE为1,表示发送寄存器空,当检查TXE位为1时,紧跟着就可以将下个数据提前写到TDR中了,一旦上一个数据发完,下一个数据就立马跟进,实现不间断的连续传输。然后移位寄存器这里一旦有数据过来了,它就会自动产生时钟,将数据移出去,在移出的过程中MISO的数据也会移入,一旦数据移出完成,数据移入也就完成了。这时,移入的数据就会整体的从移位寄存器转入到接收缓冲区RDR中,这个时刻就会置状态寄存器的RXNE为1,表示接收寄存器非空,当检查RXNE为1时,就需要尽快把数据从RDR中读出来,在下一个数据到来之前,读出RDR,就可以实现连续接收;否则下一个数据已经收到了,上一个数据还没从RDR读取出来,数据就会被覆盖。简而言之:发送数据先写入TDR,再转运移位寄存器发送,发送的同时,接收数据,接收到的数据转到RDR,再从RDR读取

这里是全双工,I2C是半双工(收发不会同时进行),串口是全双工(异步)

波特率发生器主要用于产生SCK时钟的,内部是分频器,时钟和移位寄存器的时钟相同。BR2/1/0用来控制分频系数,

LSBFIRST是决定高/低位先行;SPE是SPI使能,也就是SPI_Cmd的配置位;

BR(Baud Rate)配置波特率,就是CSK的时钟频率;MSTR配置主从模式(1主0从);

CPOL和CPHA用来选择SPI的4中模式。SPI_SR最后两个就是TXE和RXNE寄存器。

3.2 SPI基本结构

移位寄存器高位移出到MOSI,移入的数据从MISO进来到低位。TDR数据整体转入到移位寄存器的时刻置TXE标志位、移位寄存器的数据整体转入到RDR的时刻,置RXNE标志位。数据控制器控制所有的电路。

3.3 主模式全双工连续传输

第一行是SCK时钟,CPOL=1,CPHA=1,示例用的是SPI模式3,所以SCK默认是高电平,在第一个下降沿,MOSI和MISO移出数据,之后上升沿移入数据,依次进行(图中低位先行)。上面是输出流程和现象,下面是输入流程和现象。发送数据1,发送数据2,接收数据1,发送数据3,接收数据2,...,比较复杂。

3.4 非连续传输

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

步骤:

(1)等待TXE为1;(2)写入发送的数据到TDR;(3)等待TXNE为1;(4)读取RDR接收的数据。交换第二个字节重复这四步......

3.5 软件/硬件波形对比

手册

4 软件SPI读写W25Q64

4.1 接线图

和软件的一样。使用SPI1

4.2 模块封装

库函数

void SPI_Init(SPI_TypeDef* SPIx, SPI_InitTypeDef* SPI_InitStruct);
void SPI_StructInit(SPI_InitTypeDef* SPI_InitStruct);
void SPI_Cmd(SPI_TypeDef* SPIx, FunctionalState NewState);

// 写DR数据寄存器
void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data);
// 读DR数据寄存器
uint16_t SPI_I2S_ReceiveData(SPI_TypeDef* SPIx);

// 获取状态标志位,清除状态标志位
FlagStatus SPI_I2S_GetFlagStatus(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG);
void SPI_I2S_ClearFlag(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG);
ITStatus SPI_I2S_GetITStatus(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT);
void SPI_I2S_ClearITPendingBit(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT);

MySPI.c

#include "stm32f10x.h"                  // Device header

// 从机选择,输出引脚,即写SS的引脚
void MySPI_W_SS(uint8_t bitValue)
{
	// CS对应PA4
	GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)bitValue);
}

// 初始化
void MySPI_Init(void)
{
	// 1开启SPI/GPIO时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);		// 开启GPIOA的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);		// 开启SPI1的时钟

	// 2初始化GPIO口,SCK和MOSI是由硬件外设控制的输出信号,配置成复用推挽输出,
	// MISO是硬件控制的输入信号,上拉输入;ss引脚通用推挽输出
	// SS从机选择
	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);
	// SCK和MOSI复用推挽输出
	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);
	// MISO上拉输入
	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);
	
	// 3配置SPI外设
	SPI_InitTypeDef SPI_InitStructure;
	SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;	// 双线全双工
	SPI_InitStructure.SPI_Mode = SPI_Mode_Master;						// SPI的模型,主/从模式
	SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;					// 8/16数据帧
	SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low;							// SPI模式0
	SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;
	SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;							// 软件NSS
	SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_128;// SCK时钟的频率72M/128
	SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;					// 高/低位先行
	SPI_InitStructure. SPI_CRCPolynomial =	7;							// CRC校验
	SPI_Init(SPI1, &SPI_InitStructure);
	
	// 4开关控制
	SPI_Cmd(SPI1, ENABLE);
	MySPI_W_SS(1);														// SS置为高电平
}

// 起始信号
void MySPI_Start(void)
{
	MySPI_W_SS(0);					// SS置为低电平
}

// 终止信号
void MySPI_Stop(void)
{
	MySPI_W_SS(1);					// SS置为高电平
}

// 交换一个字节,模式0,参数是主机要交换的数据,返回值是从机要交换的数据
// (1)等待TXE为1;(2)写入发送的数据到TDR;(3)等待TXNE为1;(4)读取RDR接收的数据。
uint8_t MySPI_SwapByte(uint8_t byteSend)
{
	while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) != SET);			// 等待TXE为1,不用手动清除
	SPI_I2S_SendData(SPI1, byteSend);										// 写入发送的数据到TDR
	while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) != SET);			// 等待TXNE为1,不用手动清除
	return SPI_I2S_ReceiveData(SPI1);										// 读取RDR接收的数据
}

4.3 主函数

同2.3

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

uint8_t MID;
uint16_t DID;

uint8_t arrayWrite[] = {0xA1, 0xB2, 0xC3, 0xD4};
uint8_t arrayRead[4];

int main()
{
	OLED_Init();								// 初始化OLED
	W25Q64_Init();
	OLED_ShowString(1, 1, "MID:   DID:");
	OLED_ShowString(2, 1, "W:");
	OLED_ShowString(3, 1, "R:");
	
	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)
	{
		
	}
}

  • 22
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值