STM32学习笔记09-SPI通信

目录

SPI通信简介

硬件电路

移位示意图

SPI基本时序单元

SPI时序

W25Q64简介

硬件电路

W25Q64框图

Flash操作注意事项

SPI外设简介

SPI框图

SPI基本结构

主模式全双工连续传输

非连续传输

软件/硬件波形对比

SPI应用

软件SPI读写W25Q64

硬件SPI读写W25Q64


SPI通信简介

  • SPI(Serial Peripheral Interface)是由Motorola公司开发的一种通用数据总线
  • 四根通信线:SCK(Serial Clock,串行时钟线)、MOSI(Master Output Slave Input,主机输出从机输入)、MISO(Master Input Slave Output,主机输入从机输出)、SS(Slave Select,从机选择)
  • 同步,全双工
  • 支持总线挂载多设备(一主多从)

SPI相对于I2C来说,SPI传输更快,SPI协议没有严格规定最大传输速度,最大传输速度取决于芯片设计厂商设计需求,比如W25Q64芯片手册里写的SPI时钟频率最大可达80MHz,比STM32主频还高,其次SPI的设计比较简单粗暴,功能没有I2C那么多学习起来比I2C简单,最后SPI的硬件开销比较大,通信线的个数比较多,并且通信过程中经常有资源浪费现象。

SCK有些地方也叫SCLK、CLK、CK。MOSI和MISO有的地方可能直接叫DO和DI。SS有的地方也叫NSS、CS。

SCK引脚提供时钟信号,数据位的输出和输入都是在SCK的上升沿或者下降沿进行。

SS从机选择线,有几个从机就开几条SS,需要哪个从机就控制接到对应从机的SS,给个低电平说明要找这个从机了。

硬件电路

  • 所有SPI设备的SCK、MOSI、MISO分别连在一起
  • 主机另外引出多条SS控制线,分别接到各从机的SS引脚
  • 输出引脚配置为推挽输出,输入引脚配置为浮空或上拉输入

单端信号需要共地图上GND没画出来,但是是必须要接的。从机没有独立供电主机还要引出电源VCC给从机供电。

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

MOSI主机输出从机输入,数据传输方向是主机通过MOSI输出,所有从机通过MOSI 输入,MISO主机输入从机输出,数据传输方向是三个从机通过MISO输出,主机通过MISO输入。

主机的SS线都是输出,从机的SS线都是输入,SS线低电平有效,主机想指定谁就把对应SS输出置低电平就行了。同一时间主机只能置一个SS为低电平,只能选中一个从机,否则会数据冲突。

推挽输出高低电平都有较强驱动能力,这将使得SPI引脚信号的下降沿和上升沿都非常迅速,不像I2C那样下降沿非常迅速上升沿比较缓慢,得益于推挽输出的驱动能力SPI信号变化的快,那自然它就能达到更高的传输速度。

I2C并不是不想使用更快的推挽输出,而是I2C要实现半双工,经常要切换输入输出,而且I2C又要实现多主机的时钟同步和总线仲裁,这些功能都不允许I2C使用推挽输出要不然一不小心就电源短路了,所以I2C选择了更多的功能自然要放弃更强的性能了。

对于SPI来说首先不支持多主机,然后又是全双工,SPI的输出引脚始终是输出,输入引脚始终是输入,基本不会冲突,所以可以大胆使用推挽输出,不过SPI其实还是有一个冲突点,就是图上的MISO引脚,主机一个是输入,三个从机全都是输出,如果三个从机都始终是推挽输出那势必会导致冲突,所以SPI协议有一条规定:当从机的SS引脚为高电平也就是从机未被选中时它的MISO引脚必须切换为高阻态,高阻态就相当于引脚断开不输出任何电平,这样就可以防止一条线有多个输出而导致电平冲突的问题了,在SS为低电平时MISO才允许变为推挽输出,这个切换过程一般在从机里,我们一般都是写主机的程序,所以我们主机的程序不需要关注这个问题。

移位示意图

接下来演示电路如何工作:

首先我们规定波特率发生器时钟的上升沿所有移位寄存器向左移动一位,移出去的数据位放到引脚上,波特率发生器时钟的下降沿引脚上的位采样输入到移位寄存器的最低位。

接下来假设主机有个数据1010 1010要发送到从机,同时从机有个数据0101 0101要发送到主机,那我们就可以驱动时钟,先产生一个上升沿,这时所有的为向左移动一次,从最高位移出去的数据放到通信线上,实际就是放到了输出数据寄存器:

上图就是第一个时钟上升沿执行的结果,就是把主机和从机中移位寄存器的最高位分别放到MOSI和MISO的通信线上。

之后时钟继续运行,上升沿之后下一个边沿就是下降沿,下降沿时主机和从机内都会进行数据采样输入:

这就是第一个时钟结束后的现象。那时钟继续运行,下一个上升沿同样的操作移位输出:

随后下降沿数据采样输入:

最终8个时钟之后,主机原来的1010 1010跑到从机里了,原来从机里的0101 0101跑到主机里了,这就实现了主机和从机一个字节的数据交换

SPI的数据收发都是基于字节交换这个基本单元来进行的,当主机需要发送一个字节并且同时需要接收一个字节时就可以执行一下字节交换的时序,如果只想发送不想接收仍然调用交换字节的时序,发送同时接收,只是这个接收到的数据我们不看它就行了 ,如果只想接收不想发送还是调用交换字节的时序,发送同时接收,只是我们会随便发送一个数据,只要能把从机的数据置换过来就好了,我们读取置换过来的数据就是接收了,这里我们随便发过去的数据从机也不会看它,一般这个随便的数据是0x00或者0xFF。

SPI基本时序单元

  • 起始条件:SS从高电平切换到低电平
  • 终止条件:SS从低电平切换到高电平

接下来就是数据传输的基本单元了,这个基本单元建立在我们刚才说的移位模型上,并且什么时候开始移位,上升沿移位还是下降沿移位SPI并没有限定死,给了我们可以配置的选择,这样SPI就能兼容更多芯片。

SPI有两个可配置的位,分别是CPOL(Clock Polarity)时钟极性,CPHA(Clock Phase)时钟相位,每一位可以配置为1或0,总共组合起来就有模式0、模式1、模式2、模式3四种模式。

先看模式1,这个模式与前文提到的移位模型是对应的:

主机通过MOSI移出最高位,此时MOSI的电平就表示主机想要发送的数据的B7,从机通过MISO移出最高位,此时MISO表示从机想要发送的数据的B7,然后时钟运行产生下降沿,此时主机和从机同时移入数据也就是进行数据采样,这里主机移出的B7进入从机移位寄存器最低位,从机移出的B7进入主机移位寄存器最低位,这样一个时钟脉冲产生完毕,一个数据位传输完毕。接下来就是同样的过程,最后一个下降沿B0传输完成,自此主机和从机完成了一个字节的数据交换,如果主机只想交换一个字节那这时就可以置SS为高电平结束通信了,在SS上升沿MOSI还可以在变化一次,将MOSI置到一个默认高电平或者低电平,当然也可以不管它因为SPI没有硬性规定MOSI的默认电平,然后MISO从机必须得置回高阻态,此时如果主机的MISO为上拉输入那MISO引脚电平就是默认的高电平,如果主机MISO为浮空输入那MISO引脚电平不确定,如果主机还想继续交换字节,在此时主机就不必把SS置回高电平,直接重复一下交换字节的时序。

之后我们看一下模式0:

SS下降沿触发了输出,SCK上升沿就可以采样输入数据了,这样B7就传输完毕,之后SCK下降沿移出B6,SCK上升沿移入B6,最终在第8个上升沿B0位移入完成,整个字节交换完成,之后SCK还有个下降沿,如果主机只需要交换一个字节就结束那在整个下降沿MOSI可以置回默认电平或者不管它,MISO也会变化一次,这一位实际上是下一个字节的B7,因为这个相位提前了,所以下一个字节的B7会露个头,如果不需要的话SS上升沿之后从机MISO置回高阻态。如果主机想交换多个字节,那就继续调用时序,在最后一个下降沿主机放下一个字节的B7,从机也放下一个字节的B7,SCK上升沿正好接着采样第二个字节的B7。

模式0和模式1的区别就在于模式0把这个数据变化的时机提前了,在实际应用中模式0是最常用的。

模式2和模式3:

模式0和模式2的区别就是模式0的CPOL=0,模式2的CPOL=1,两者的波形就是SCK的极性取反一下,剩下的流程完全一致;模式1和模式3的区别,也是模式1的CPOL=0,模式3的CPOL=1,两者的波形也是SCK的极性取反一下其他地方没有变化。

模式0和模式3都是SCK上升沿采样,模式1和模式2都是SCK下降沿采样。

SPI时序

接下来看几个完整的SPI时序波形,以W25Q64为例。SPI对字节流功能的规定不像I2C,I2C规定一般是有效数据流第一个字节是寄存器地址,之后依次是读写的数据,使用的是读写寄存器的模型。而SPI通常采用指令码加读写数据的模型,这个过程就是SPI起始后,第一个交换发送给从机的数据一般叫指令码,在从机中对应会有一个指令集,当我们需要发送什么指令时,就可以在起始后第一个字节发送指令集里面的数据,这样就能指导从机完成相应的功能了,不同的指令可以有不同的数据个数。

整个时序的功能就是发送指令,指令码是0x06,从机一比对事先定义好的指令集发现0x06是写使能的指令,那从机就会控制硬件进行写使能。

地址24位,分3个字节传输,时序首先SS下降沿开始时序,第一个字节用0x02换来了0xFF,其中发送的0x02是一条指令代表这是一个写数据的时序,接收的0xFF不需要看。后面跟着写的地址和数据,第二个字节用0x12换来了0xFF,W25Q64规定写指令之后的字节定义为地址高位,所以这个0x12就表示地址的23~16位,之后同理,三个字节交换24位地址发送完毕,从机收到的24位地址是0x123456,之后发送写入的数据。

W25Q64简介

  • W25Qxx系列是一种低成本、小型化、使用简单的非易失性存储器,常应用于数据存储、字库存储、固件程序存储等场景
  • 存储介质:Nor Flash(闪存)
  • 时钟频率:80MHz / 160MHz (Dual SPI) / 320MHz (Quad SPI)
  • 存储容量(24位地址):

W25Q40: 4Mbit / 512KByte

W25Q80: 8Mbit / 1MByte

W25Q16: 16Mbit / 2MByte

W25Q32: 32Mbit / 4MByte

W25Q64: 64Mbit / 8MByte

W25Q128: 128Mbit / 16MByte

W25Q256: 256Mbit / 32MByte

硬件电路

W25Q64框图

数据从缓冲区到Flash里需要一定时间,所以在写入时序结束后芯片会进入一段忙的状态,会有一条线通往状态寄存器的BUSY位,将该位置1,忙的时候芯片就不会响应新的读写时序了。

Flash操作注意事项

写入操作时:

  • 1)写入操作前,必须先进行写使能
  • 2)每个数据位只能由1改写为0,不能由0改写为1
  • 3)写入数据前必须先擦除,擦除后,所有数据位变为1
  • 4)擦除必须按最小擦除单元进行
  • 5)连续写入多字节时,最多写入一页的数据,超过页尾位置的数据,会回到页首覆盖写入
  • 6)写入操作结束后,芯片进入忙状态,不响应新的读写操作
  • 读取操作时:直接调用读取时序,无需使能,无需额外操作,没有页的限制,读取操作结束后不会进入忙状态,但不能在忙状态时读取

SPI外设简介

  • STM32内部集成了硬件SPI收发电路,可以由硬件自动执行时钟生成、数据收发等功能,减轻CPU的负担
  • 可配置8位/16位数据帧、高位先行/低位先行
  • 时钟频率: fPCLK / (2, 4, 8, 16, 32, 64, 128, 256)
  • 支持多主机模型、主或从操作
  • 可精简为半双工/单工通信
  • 支持DMA
  • 兼容I2S协议
  • STM32F103C8T6 硬件SPI资源:SPI1、SPI2

我们SPI最常用的配置是8位数据帧高位先行。I2S是数字音频信号传输的协议,和I2C区别很大不要搞混了,了解。

SPI框图

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

SPI基本结构

主模式全双工连续传输

非连续传输

主模式全双工连续传输比较复杂,也不太方便封装,在实际过程中如果对性能没有极致的追求,我们更倾向于使用这个非连续传输的,非连续传输更加简单实际用的话只需要4行代码就能完成任务。

按照这个,我们的流程就是,第一步等待TXE为1,第二步写入发送的数据到TDR,第三步等待RXNE为1,第四步读取RDR接收到的数据,之后交换第二个字节重复这四步。

非连续传输缺点就是没有及时把下一个数据写入TDR等着,所以等待上一个字节时序完成后下一个字节还没有来,那这个数据传输就会在这里等,会拖慢数据传输速度,这个间隙在SCK频率低的时候影响不大,SCK频率非常高时影响就比较大。

软件/硬件波形对比

SPI应用

软件SPI读写W25Q64

软件模拟的SPI的CS、DO、CLK、DI线可以接到STM32的任意GPIO口。

程序框架如下:

首先建立MySPI模块,在MySPI_Init函数里初始化通信引脚,接线图中我们使用PA4、5、6、7引脚,开启GPIOA时钟,前文提到输出引脚配置为推挽输出,输入引脚配置为浮空输入或者上拉输入,对于主机来说时钟、主机输出和片选都是输出引脚,所以这3个脚配置为推挽输出,剩下一个主机输入是输入引脚我们选择上拉输入。

cpp 复制代码
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);

接下来把置引脚高低电平的函数都封装换个名字:

cpp 复制代码
/*
* 写SS的引脚
*/
void MySPI_W_SS(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue);
}

/*
* 写SCK的引脚
*/
void MySPI_W_SCK(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOA, GPIO_Pin_5, (BitAction)BitValue);
}

/*
* 写MISO的引脚
*/
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);
}

MySPI_Init函数里我们还有一个工作要做就是置一下初始化之后引脚的默认电平,SS高电平不选中从机,SCK我们计划使用SPI模式0所以默认低电平:

cpp 复制代码
/* 置默认电平 */
MySPI_W_SS(1);//SS默认高电平,不选中从机
MySPI_W_SCK(0);//计划用SPI模式0,所以默认低电平

接下来我们开始写SPI的三个时序基本单元:

cpp 复制代码
/*起始信号
* 直接把SS置低电平
*/
void MySPI_Start(void)
{
	MySPI_W_SS(0);
}

/*终止信号
* 直接把SS置高电平
*/
void MySPI_Stop(void)
{
	MySPI_W_SS(1);
}

/*交换一个字节(模式0)
* 参数 ByteSend:通过交换一个字节发送出去的数据
* 返回值:通过交换一个字节接收到的数据
*/
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{
	uint8_t i,ByteReceive = 0x00;//用于接收
	
	for(i = 0;i < 8;i++)
	{
		MySPI_W_MOSI(ByteSend & (0x80 >> i));//初始SCK低电平,主机移出一位数据到MOSI(调用写MOSI),从机移出一位到MISO(从机的事不归我们管)
		MySPI_W_SCK(1);//SCK产生上升沿,主机和从机同时移入数据,从机自动把MOSI的数据读走(不归我们管),主机读取MISO的数据(上升沿后进行)
		if (MySPI_R_MISO() == 1){ByteReceive |= (0x80 >> i);}//主机把从机刚才放在MISO的数据位读进来
		MySPI_W_SCK(0);//SCK产生下降沿,主机从机移出下一位数据
	}
	
	return ByteReceive;
}

接下来按照计划写下一个模块,在SPI通信层基础上,建立W25Q64驱动层。同理先在W25Q64_Init函数进行初始化, 由于不需要初始化其他东西所以只调用SPI_Init即可。

cpp 复制代码
void W25Q64_Init(void)
{
	MySPI_Init();
	
}

现在我们来实现业务代码也就是拼接完整时序,这个我们要参考手册,主要参考指令集表格:

每个指令对应一个指令码,直接写数字意义不太明显,可读性不高,所以我们单独建立一个头文件W26Q64_Ins.s存放宏定义:

cpp 复制代码
#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

前文提到写操作前必须进行写使能,涉及写使能1的时序有扇区擦除和页编程,为了方便使用我们直接在函数里自带写使能。还有一个就是写入操作后芯片进入忙状态,所以我们在每次写操作结束后调用一下等待BUSY清零的函数。

cpp 复制代码
/*读取8位厂商ID(MID)和16位设备ID(DID)
*/
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号置换过来,随便发一个0xFF,这个返回值是我们要的MID
	*DID = MySPI_SwapByte(W25Q64_DUMMY_BYTE);//这个返回值是我们要的DID高8位
	*DID <<= 8;
	*DID |= MySPI_SwapByte(W25Q64_DUMMY_BYTE);//这个返回值是我们要的DID低8位
	MySPI_Stop();
}

/*写使能
*/
void W25Q64_WriteEnable(void)
{
	MySPI_Start();
	MySPI_SwapByte(W25Q64_WRITE_ENABLE);
	MySPI_Stop();
}

/*等待忙函数,读状态寄存器1,一般读最低位BUSY判断是不是忙状态,1表示芯片还在忙
*/
void W25Q64_WaitBusy(void)
{
	uint32_t TimeOut;
	MySPI_Start();
	MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1);
	TimeOut = 100000;
	while ((MySPI_SwapByte(W25Q64_DUMMY_BYTE) & 0x01) == 0x01)//等待BUSY为0
	{
		TimeOut --;
		if(TimeOut == 0)//超时退出
		{
			break;
		}
	};
	MySPI_Stop();
}

/*页编程
*/
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个字节的地址,例如0x123456,>>16就是0x12,>>8就是0x1234舍弃高位就是0x34,不移动舍弃高位就是0x56
	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();//事后等待
}

/*擦除(扇区擦除)
*/
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();//事后等待
}

/*读取数据
*/
void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count)
{
	uint32_t i;
	
	MySPI_Start();
	MySPI_SwapByte(W25Q64_READ_DATA);
	//指定地址
	MySPI_SwapByte(Address >> 16);
	MySPI_SwapByte(Address >> 8);
	MySPI_SwapByte(Address);
	//接收
	for(i = 0;i < Count;i++)
	{
		DataArray[i] = MySPI_SwapByte(W25Q64_DUMMY_BYTE);
	}
	MySPI_Stop();
}

完整代码:

MySPI.c:

cpp 复制代码
#include "stm32f10x.h"                  // Device header

/*
* 写SS的引脚
*/
void MySPI_W_SS(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue);
}

/*
* 写SCK的引脚
*/
void MySPI_W_SCK(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOA, GPIO_Pin_5, (BitAction)BitValue);
}

/*
* 写MISO的引脚
*/
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);
	
	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);//SS默认高电平,不选中从机
	MySPI_W_SCK(0);//计划用SPI模式0,所以默认低电平
	
}

/*起始信号
* 直接把SS置低电平
*/
void MySPI_Start(void)
{
	MySPI_W_SS(0);
}

/*终止信号
* 直接把SS置高电平
*/
void MySPI_Stop(void)
{
	MySPI_W_SS(1);
}

/*交换一个字节(模式0)
* 参数 ByteSend:通过交换一个字节发送出去的数据
* 返回值:通过交换一个字节接收到的数据
*/
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{
	uint8_t i,ByteReceive = 0x00;//用于接收
	
	for(i = 0;i < 8;i++)
	{
		MySPI_W_MOSI(ByteSend & (0x80 >> i));//初始SCK低电平,主机移出一位数据到MOSI(调用写MOSI),从机移出一位到MISO(从机的事不归我们管)
		MySPI_W_SCK(1);//SCK产生上升沿,主机和从机同时移入数据,从机自动把MOSI的数据读走(不归我们管),主机读取MISO的数据(上升沿后进行)
		if (MySPI_R_MISO() == 1){ByteReceive |= (0x80 >> i);}//主机把从机刚才放在MISO的数据位读进来
		MySPI_W_SCK(0);//SCK产生下降沿,主机从机移出下一位数据
	}
	
	return ByteReceive;
}

W25Q64.c:

cpp 复制代码
#include "stm32f10x.h"                  // Device header
#include "MySPI.h" 
#include "W25Q64_Ins.h" 

void W25Q64_Init(void)
{
	MySPI_Init();
	
}

/*读取8位厂商ID(MID)和16位设备ID(DID)
*/
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号置换过来,随便发一个0xFF,这个返回值是我们要的MID
	*DID = MySPI_SwapByte(W25Q64_DUMMY_BYTE);//这个返回值是我们要的DID高8位
	*DID <<= 8;
	*DID |= MySPI_SwapByte(W25Q64_DUMMY_BYTE);//这个返回值是我们要的DID低8位
	MySPI_Stop();
}

/*写使能
*/
void W25Q64_WriteEnable(void)
{
	MySPI_Start();
	MySPI_SwapByte(W25Q64_WRITE_ENABLE);
	MySPI_Stop();
}

/*等待忙函数,读状态寄存器1,一般读最低位BUSY判断是不是忙状态,1表示芯片还在忙
*/
void W25Q64_WaitBusy(void)
{
	uint32_t TimeOut;
	MySPI_Start();
	MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1);
	TimeOut = 100000;
	while ((MySPI_SwapByte(W25Q64_DUMMY_BYTE) & 0x01) == 0x01)//等待BUSY为0
	{
		TimeOut --;
		if(TimeOut == 0)//超时退出
		{
			break;
		}
	};
	MySPI_Stop();
}

/*页编程
*/
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个字节的地址,例如0x123456,>>16就是0x12,>>8就是0x1234舍弃高位就是0x34,不移动舍弃高位就是0x56
	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();//事后等待
}

/*擦除(扇区擦除)
*/
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();//事后等待
}

/*读取数据
*/
void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count)
{
	uint32_t i;
	
	MySPI_Start();
	MySPI_SwapByte(W25Q64_READ_DATA);
	//指定地址
	MySPI_SwapByte(Address >> 16);
	MySPI_SwapByte(Address >> 8);
	MySPI_SwapByte(Address);
	//接收
	for(i = 0;i < Count;i++)
	{
		DataArray[i] = MySPI_SwapByte(W25Q64_DUMMY_BYTE);
	}
	MySPI_Stop();
}

main.c:

cpp 复制代码
#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];

uint8_t Key_Num;

int main(void)
{
	OLED_Init();
	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)
	{
		
	}
}

硬件SPI读写W25Q64

硬件SPI引脚就不能任意选择了:

NSS我们继续使用软件模拟的方式实现,NSS没必要必须接在PA4,其他三个引脚就必须是PA5、6、7了。我们计划用SPI1,所以SCK接PA5,MISO接PA6,MOIS接PA7。

在上一个代码的MySPI底层修改即可,先看一下SPI相关库函数:

cpp 复制代码
void SPI_I2S_DeInit(SPI_TypeDef* SPIx);//恢复缺省配置
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);//外设使能
void SPI_I2S_ITConfig(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT, FunctionalState NewState);//中断使能
void SPI_I2S_DMACmd(SPI_TypeDef* SPIx, uint16_t SPI_I2S_DMAReq, FunctionalState NewState);//DMA使能
cpp 复制代码
void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data);//写DR数据寄存器,写TDR
uint16_t SPI_I2S_ReceiveData(SPI_TypeDef* SPIx);//读DR数据寄存器,返回RDR的值
cpp 复制代码
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);//清除中断标志位

第一步开启SPI和GPIO时钟。

cpp 复制代码
/* 第一步初始化GPIO与SPI时钟 */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1,ENABLE);

第二步,第二步初始化GPIO口。

cpp 复制代码
/* 第二步初始化GPIO口,SCK和MOSI是硬件外设控制的输出信号配置为复用推挽输出,MISO是硬件外设输入信号配置为上拉输入 */
/* 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);
	
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);
	
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);

第三步配置SPI外设。SPI_Init第一个参数SPI1,第二个参数结构体:

  • SPI_Mode,SPI模式,决定当前设备是SPI的主机还是从机
  • SPI_Direction,配置SPI裁剪引脚
  • SPI_DataSize,配置8位还是16位数据帧
  • SPI_FirstBit,配置高位先行还是低位先行
  • SPI_BaudRatePrescaler,波特率预分频器配置SCK时钟频率
  • SPI_CPOL,时钟极性
  • SPI_CPHA,时钟相位,第几个边沿采样(移入)
  • SPI_NSS,NSS引脚,
  • SPI_CRCPolynomial,CRC校验多项式
cpp 复制代码
/* 第三步配置SPI外设 */
SPI_InitTypeDef SPI_InitStructure;
SPI_InitStructure.SPI_Mode = SPI_Mode_Master;//SPI模式,决定当前设备是SPI的主机还是从机,我们选主机
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;//配置SPI裁剪引脚,我们用标准模式双线全双工
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;//配置8位还是16位数据帧,我们选8位
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;//配置高位先行还是低位先行,我们选高位先行
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_128;//波特率预分频器配置SCK时钟频率
SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low;//时钟极性,我们用SPI模式0也就是默认低电平
SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;//时钟相位,第几个边沿采样(移入),我们用SPI模式0所以我们选第一个开始采样
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;//NSS引脚我们计划使用GPIO模拟,外设的NSS我们不用,选软件NSS就行了
SPI_InitStructure.SPI_CRCPolynomial = 7;//CRC校验多项式,我们不用,填默认值7
SPI_Init(SPI1, &SPI_InitStructure);

第四步开关控制SPI使能。

cpp 复制代码
/* 第四步开关控制SPI使能 */
SPI_Cmd(SPI1, ENABLE);

最后默认SS输出高电平,不选中从机。

cpp 复制代码
MySPI_W_SS(0);//默认SS输出高电平,不选中从机

这样SPI初始化函数就写好了,SPI外设就绪,我们就可以完成交换字节的函数了。调用交换字节函数,硬件SPI外设就要自动控制SCK、MOSI、MISO引脚生成时序。根据前文提到的四步。

cpp 复制代码
/*起始信号
* 把SS置低电平
*/
void MySPI_Start(void)
{
	MySPI_W_SS(0);
}

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

/*交换一个字节(模式0)
* 参数 ByteSend:通过交换一个字节发送出去的数据
* 返回值:通过交换一个字节接收到的数据
*/
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{
	while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) != SET);//等待TXE为1
	
	SPI_I2S_SendData(SPI1, ByteSend);//软件写入数据到SPI_DR,ByteSend写入到TDR,
	//之后ByteSend自动转入移位寄存器一旦移位寄存器有数据时序波形就会自动产生
	
	while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) != SET);//等待RXNE为1
	
	return SPI_I2S_ReceiveData(SPI1);//读取RDR接收的数据,就是置换接收的一个字节
}

完整代码:

MySPI.c:

cpp 复制代码
#include "stm32f10x.h"                  // Device header

/*
* 写SS的引脚
*/
void MySPI_W_SS(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue);
}


void MySPI_Init(void)
{
	/* 第一步初始化GPIO与SPI时钟 */
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1,ENABLE);
	
	/* 第二步初始化GPIO口,SCK和MOSI是硬件外设控制的输出信号配置为复用推挽输出,MISO是硬件外设输入信号配置为上拉输入 */
	/* 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);
	
	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);
	
	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);
	
	/* 第三步配置SPI外设 */
	SPI_InitTypeDef SPI_InitStructure;
	SPI_InitStructure.SPI_Mode = SPI_Mode_Master;//SPI模式,决定当前设备是SPI的主机还是从机,我们选主机
	SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;//配置SPI裁剪引脚,我们用标准模式双线全双工
	SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;//配置8位还是16位数据帧,我们选8位
	SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;//配置高位先行还是低位先行,我们选高位先行
	SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_128;//波特率预分频器配置SCK时钟频率
	SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low;//时钟极性,我们用SPI模式0也就是默认低电平
	SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;//时钟相位,第几个边沿采样(移入),我们用SPI模式0所以我们选第一个开始采样
	SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;//NSS引脚我们计划使用GPIO模拟,外设的NSS我们不用,选软件NSS就行了
	SPI_InitStructure.SPI_CRCPolynomial = 7;//CRC校验多项式,我们不用,填默认值7
	SPI_Init(SPI1, &SPI_InitStructure);
	
	/* 第四步开关控制SPI使能 */
	SPI_Cmd(SPI1, ENABLE);
	
	MySPI_W_SS(0);//默认SS输出高电平,不选中从机
	
}

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

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

/*交换一个字节(模式0)
* 参数 ByteSend:通过交换一个字节发送出去的数据
* 返回值:通过交换一个字节接收到的数据
*/
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{
	while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) != SET);//等待TXE为1
	
	SPI_I2S_SendData(SPI1, ByteSend);//软件写入数据到SPI_DR,ByteSend写入到TDR,
	//之后ByteSend自动转入移位寄存器一旦移位寄存器有数据时序波形就会自动产生
	
	while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) != SET);//等待RXNE为1
	
	return SPI_I2S_ReceiveData(SPI1);//读取RDR接收的数据,就是置换接收的一个字节
}
相关推荐
陌夏微秋1 小时前
51单片机基础02 动态数码管显示-并串转换
arm开发·单片机·嵌入式硬件·51单片机·硬件工程·信息与通信·信号处理
面包板扎1 小时前
51单片机应用开发---LCD1602显示应用
单片机·嵌入式硬件·51单片机
面包板扎1 小时前
51单片机应用开发(进阶)---定时器应用(电子时钟)
单片机
好想有猫猫1 小时前
【51单片机】LCD1602液晶显示屏
c语言·单片机·嵌入式硬件·51单片机·1024程序员节
陌夏微秋1 小时前
51单片机基础01 单片机最小系统
单片机·嵌入式硬件·51单片机·硬件工程·信息与通信
B20080116刘实3 小时前
CTF攻防世界小白刷题自学笔记13
开发语言·笔记·web安全·网络安全·php
静止了所有花开4 小时前
SpringMVC学习笔记(二)
笔记·学习
红中马喽8 小时前
JS学习日记(webAPI—DOM)
开发语言·前端·javascript·笔记·vscode·学习
追梦少年时10 小时前
STM32-Flash闪存
stm32·单片机·嵌入式硬件·51单片机
huangkj-henan10 小时前
DA217应用笔记
笔记