江协科技STM32学习笔记(第10章 SPI通信)

第10章 SPI通信

10.1 SPI通信协议

10.1.1 SPI通信

SPI(Serial Peripheral Interface)是由Motorola公司开发的一种通用数据总线;

串行外设接口;

I2C无论是软件还是软件电路,设计的都还是比较复杂的,硬件上,我们要配置为开漏外加上拉的模式;软件上有很多功能和要求,比如一根通信线兼顾数据收发、应答位的收发、寻址机制的设计等等。通过这么多的设计,使得I2C的通信性价比非常高,I2C可以在消耗最低硬件资源的情况下,实现最多的功能。在硬件上,无论挂载多少个设备,都只需要两根通信线,在软件上,数据双向通信、应答位都可以实现,如果把通信协议比做人的话,那I2C就属于精打细算、思维灵活的人,既要实现硬件上最少的通信线,又要实现软件上最多的功能。I2C经过精心的设计,也确实实现了这么多功能。缺点就是由于I2C采用开漏外加上拉电阻的电路结构,使得通信线高电平的驱动能力比较弱,这就会导致,通信线由低电平变到高电平的时候,上升沿比较长,这会限制I2C的最大通信速度,所以I2C的标准模式,只有100KHz的时钟频率,I2C的快速模式,也只有400KHz;虽然I2C协议最后又通过改进电路的方式,设计出了高速模式,可以达到3.4MHz,但是高速模式目前普及模式不是很高,所以一般情况下,我们认为I2C的时钟速度最多就是400KHz,这个速度相比较I2C而言,还是慢了很多的。

SPI的优缺点:

(1)SPI传输更快,SPI协议并没有严格规定最大传输速度,这个最大传输速度取决于芯片厂商的设计需求,比如下图第一个图所示W25Q64存储器芯片,手册里写的SPI时钟频率,最大可达80Hz

,这比STM32F1的主频还要高;

(2)其次,SPI的设计比较简单粗暴,实现的功能没有I2C那么多,所以学习起来,SPI比I2C简单很多;

(3)SPI硬件开销比较大,通信线的个数比较多,并且通信过程中,经常会有资源浪费的现象,如果继续把通信协议比作一个人的话,SPI就属于富家子弟、有钱任性这类型的人。SPI不在乎花了多少钱,只在乎任务有没有最简单、最快速的完成。

四根通信线:SCK(Serial Clock)、MOSI(Master Output Slave Input)、MISO(Master Input Slave Output)、SS(Slave Select);

SCK:串行时钟线

MOSI:主机输出、从机输入

MISO:主机输入、从机输出

SS:从机选择

以上是SPI通信典型的引脚名称,当然在实际情况下,这些名称可能会有别的表述方式,比如SCK,有的地方可能叫做SCLK、CLK、CK;MOSI和MISO,有的地方可能直接叫做DO(Data Output)和DI(Data Input);SS有的地方也可能叫做NSS(Not Slave Select)、CS(Chip Select)。

同步,全双工;

首先既然是同步时序,肯定就得有时钟线了,SCK引脚就是用来提供时钟信号的,数据位的输出和输入,都是在SCK的上升沿或下降沿进行的,这样,数据位的收发时刻就可以明确的确定,并且,同步时序,时钟快点慢点,或者中途暂停一会儿,都是没问题的,这就是同步时序的好处。对照I2C总线,这个SCK,就相当于I2C的SCL,两者作用相同。

之后,SPI是全双工的协议,全双工,就是数据发送和数据接收单独各占一条线,发送用发送的线路,接收用接收的线路,两者互不影响,所以这里MOSI和MISO,就是分别用于发送和接收的两条线路,MOSI线,是主机输出、从机输入,如果是主机接在这条线上,那就是MO,主机输出;如果是从机接在这条线上,就是SI,从机输入。意思就是一条通信线,如果主机接在上面配置为输出,那从机肯定得配置为输入,才能接收主机得数据,主机和从机不能同时配置为输入或输出,不然就没法通信了,所以这条MOSI就是主机向从机发送数据的线路。MISO就是主机从从机接收数据的线路,这就是全双工通信的两根通信线,这两根线,加在一起就相当于I2C总线的SDA,当然I2C是一根线兼具发送和接收,是半双工,SPI是一根发送,一根接收,是全双工。全双工的好处就是简单高效,输出线就一直输出,输入线就一直输入,数据流的方向不会改变,也不用担心发送和接收没协调好冲突了。但是坏处就是多了一根线,会有通信资源的浪费。

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

SPI仅支持一主多从,不支持多主机。这一点,SPI没有I2C强大。

I2C实现一主多从的方式是,在起始条件之后,主机必须先发送一个字节进行寻址,用来指定我要跟哪个从机进行通信,所以I2C这里,要涉及分配地址和寻址的问题,但是SPI表示,你这太麻烦了,SPI直接再开辟了一条通信线,专门用来指定我要跟哪个从机进行通信,所以这条专门用来指定从机的通信线,就是这里的SS,从机选择线。并且这个SS可能不止一条,SPI的主机表示,我有几个从机,我就开几条SS,所有从机一人一根,我需要的时候,就控制接到你那根SS线。

SPI没有应答机制的设计,发送数据就是发送,接收数据就是接收,至于对面是不是存在,SPI是不管的。

第1个图是W25Q64,是一个Flash存储器, 可以看到这个模块的引脚,和刚才说的SPI通信典型引脚名称并不一样,这里CLK就是CK、DI和DO就是MOSI和MISO,DI到底是MOSI还是MISO,要看一下这个芯片的身份,这个芯片接在STM32上,应该是从机,所以这里的DI数数据输入,就是从机的数据输入SI,对应需要接在主机的MO上,所以这里的DI就是MOSI,另一个DO就是MISO了。一般在这种始终作为从机的设备上,可能会用DI和DO的简写,像STM32这种,可以进行身份转换的设备,一般都会把MOSI、MISO的全称写完整。CS片选就是SS从机选择了。

第2个图是利用SPI通信的OLED屏幕,上面的引脚也不是标准的名称。所以这个引脚需要查一下手册,手册里有些。

第3个图是一个2.4G无线通信模块,芯片型号是NRF24L01,这个芯片使用的就是SPI通信协议,要想使用这个芯片来进行无线通信,就需要用SPI来读写这个芯片。

第4个图就是常见的MicroSD卡了,这个SD卡官方的通信协议是SDIO,但是它也是支持SPI协议的,我们可以利用这个SPI,对这个SD卡进行读写操作。

10.1.2 SPI硬件电路

所有SPI设备的SCK、MOSI、MISO分别连在一起;

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

MOSI:主机输出从机输入,左边是主机,所以对应MO主机输出,下面三个都是从机,所以就对应SI,从机输入;数据传输方向是,主机通过MOSI输出,所有从机通过MOSI输出。

MISO:主机输入从机输出,左边是主机对应MI,下面三个从机对应SO,数据传输方向是,三个从机通过MISO输出,主机通过MISO输入。

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

主机的SS都是输出,从机的SS都是输入,SS线是低电平有效的,主机想指定谁就把对应的SS输出线置低电平就行了。比如主机初始化之后,所有的SS都输出高电平,这样就是谁也不指定,当主机需要和比如从机1进行通信了,主机就把SS1线输出低电平,这样从机1就知道主机在找我,然后主机在数据引脚进行的传输,就只有从机1会响应。其它从机的SS线是高电平,所以它们都会保持默认,当主机和从机1通信完成后,就会把SS1置回高电平,这样从机1就知道,主机结束了和我的通信。同一时间,主机只能置一个SS为低电平,只能选中一个从机否则,如果主机选中多个从机,就会导致数据冲突,这就是SPI总线选择从机的方式。

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

推挽输出,高低电平均有很强的驱动能力,这将使得SPI引脚信号的下降沿,非常迅速,上升沿,也非常迅速,不想I2C那样,下降沿非常迅速,但是上升沿就比较缓慢了,得益于推挽输出的驱动能力,SPI的信号变化得快,自然就能达到更高得传输速度,一般SPI信号都能轻松地达到MHz的速度级别。I2C并不是不想使用更快的推挽输出,而是I2C要使用半双工,经常要切换输入输出,另外I2C又要实现多主机的时钟同步和总线仲裁,这些功能都不允许I2C使用推挽输出,不然I2C一不小心就短路了。所以I2C选择了实现更多的功能,自然就要放弃更强的性能了。对于SPI来说,首先SPI不支持多主机,然后SPI又是全双工,SPI的输出引脚始终是输出,输入引脚始终是输入,基本不会出现冲突,所以SPI可以大胆地使用推挽输出。不过SPI还是有一个冲突点的,就是MISO引脚,在这个引脚上可以看到主机一个是输入,但是三个从机全都是输出,如果三个从机都始终是推挽输出,势必会导致冲突,所以在SPI协议里,有一条规定,就是当从机的SS引脚为高电平,也就是从机未被选中时,它的MISO引脚,必须切换为高阻态,高阻态就相当于引脚断开,不输出任何电平,这样就能防止一条线上有多个输出,而导致的电平冲突的问题了,在SS为低电平时,MISO才允许变为推挽输出,这就是SPI对这个可能的冲突做出的规定。当然这个切换过程都是在从机里,我们一般都是写主机的程序,所以我们主机的程序中,并不需要关注这个问题。

SPI主机主导整个SPI总线,主机一般都是控制器来作,比如STM32,下面的SPI从机1、2、3就是挂载在主机上的从设备,比如存储器、显示屏、通信模块、传感器等等。左边SPI主机实际上引出了6根通信线,因为有3个从机,所以SS线需要3根,再加SCK、MOSI、MISO,就是6根通信线,当然SPI所有通信线都是单端信号,它们的高低电平都是相对GND的电压差。所以单端信号,所有的设备还需要共地,这里GND的线没画出来,但是是必须要接的,如果从机没有独立供电的话,主机还需要再额外引出电源正极VCC,给从机供电,这两根电源线,VCC和GND也要注意接好。

10.1.3 移位示意图

这个移位示意图是SPI硬件电路设计的核心,只要把这个移位示意图搞懂了,无论是硬件电路还是软件时序,理解起来都会更加轻松。

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

首先,我们规定,波特率发生器时钟的上升沿、所有移位寄存器向左移动一位,移出去的位放在引脚上;波特率发生器时钟的下降沿,引脚上的位,采样输入到移位寄存器的最低位,接下来,假设主机有个数据10101010要发送到从机,同时从机有个数据01010101要发送到主机,那我们就可以驱动时钟,先产生一个上升沿,这时,所有的位,就会往左移动一位,从最高位移出去的数字,就会放到通信线上(实际上是放到了输出数据寄存器),可以看到,此时MOSI数据是1,所以MOSI的电平就是高电平;MISO数据是0,所以MISO的电平就是低电平,这就是第一个时钟上升沿执行的结果。就是把主机和从机中,移位寄存器的最高位,分别放到MISO和MOSI的通信线上,这就是数据的输出。

之后时钟继续运行,上升沿之后,下一个边沿就是下降沿。在下降沿时,主机和从机内,都会进行数据采样输入, 也就是MOSI的1,会采样输入到从机这里的最低位,MISO的0,会采样输入到主机这里的最低位,这就是第一个时钟结束后的现象。

时钟继续运行,同样的操作。

8个时钟以后,就实现了主机和从机一个字节的数据交换。实际上SPI的运行过程就是这样,SPI的数据收发,都是基于字节交换,这个基本单元来进行的,当主机需要发送一个字节,并且同时需要接收一个字节时,就可以执行以下字节交换的时序,这样主机要发送的数据跑到从机,主机要从从机接收的数据,跑到主机,这就完成了发送同时接收的目的。

如果只想发送,不想接收,仍然调用交换字节的时序,发送,同时接收,只是这个接收到的数据,我们不看它就行了。如果只想接收,不想发送,也是同理,调用交换字节的时序,发送,同时接收,只是我们回随便发送一个数据,只要能把从机的数据置换过来就行了,我们读取置换过来的数据就是接收到了,随便塞过去的数据,从机也不会去看它,当然这个随便的数据不会真的随便发,一般在接收的时候,统一发送0x00或0xFF,去跟从机换数据。

10.1.4 SPI时序基本单元

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

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

数据传输的基本单元是建立在移位模型上的,并且这个模型什么时候移位?是上升沿移位还是下降沿移位?SPI并没有限定死,给了我们可以配置的选择,这样的话SPI就可以兼容更多的芯片。SPI有两个可以配置的位,分别叫做CPOL(Clock Polarity)、时钟极性 和**CPHA(Clock Phase),**每一位都可以配置为1或0,总共组合起来,就有模式0、模式1、模式2、模式3这4中模式。模式虽然多,但功能都是一样的。

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

交换一个字节(模式0)

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

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

MISO起始和终止位高阻态。

交换一个字节(模式1)

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

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

交换一个字节(模式2)

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

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

交换一个字节(模式3)

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

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

10.1.5 SPI时序

SPI中,通常使用的是指令码加读写数据的模型,这个过程就是SPI起始后,第一个交换发送给从机的数据,一般叫做指令码,在从机中,对应的会定义一个指令集,当我们需要发送什么指令时,就可以在起始后第一个字节,发送指令集里面的数据,这样就是指导从机完成相应的功能了。不同的指令,可以有不同的数据个数,有的指令,只需要一个字节的指令码就可以完成,比如W25Q64的写使能、写失能等指令。而有的指令,后面就需要再跟要读写的数据,比如W25Q64的写数据、读数据等。写数据指令后面就得跟上,我要在哪里写,我要写什么;读数据指令后面就得跟上我要在哪里读,我要读到的是什么。这就是指令码加读写数据的模型,在SPI从机的芯片手册里,都会定义好指令集,什么指令对应什么功能;什么指令后面得跟上什么数据。

发送指令

向SS指定的设备,发送指令(0x06)

指定地址写

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

指定地址读

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

10.2 W25Q64简介

10.2.1 W25Q64简介

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

SPI串行通信,通信引脚比较少,协议也很简单,这个芯片的硬件接线也不麻烦,就VCC、GND接上电,剩下的全都可以接GPIO,基本不需要其它电路;

存储器分为易失性存储器和非易失性存储器,易失性存储器一般就是SRAM、DRAM等,非易失性存储器一般就是E2PROM、Flash等,它们最主要的区别,简而言之,就是存储的数据是否掉电不丢失,非易失性存储器就是数据不容易丢失的存储器,也就是数据掉电不丢失。所以存储在W25Qxx芯片里的数据,在断电重启后,数据仍然保持原样。

字库存储;可以用这个数据来存储汉字字库的点阵数据,在显示某个数据之前,先读取芯片查询字库,再在显示屏上显示对应的点阵数据,这样就能让显示屏任意显示中文了。

固件程序存储就相当于直接把程序文件下载到外挂芯片里,需要执行程序的时候,直接读取外挂芯片的程序文件来执行。这就是XIP(eXecute In Place),就地执行。比如我们电脑里的BIOS固件,就可以存储在这个W25Q系列芯片里。

存储介质:Nor Flash(闪存)

Flash就是闪存存储器,像我们STM32里的程序存储器、U盘、电脑里的固态硬盘等,使用的都是Flash闪存,闪存分为Nor Flash和Nand Flash,两者各有优势和劣势,适用领域不同。

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

我们这个芯片使用的SPI通信,其中SPI的SCK线,就是时钟线,这个时钟线的最大频率是80MHz,这个频率相比较STM32,是非常快了。所以我们之后写程序的时候,翻转引脚,就不需要加延时了,即使不掩饰,这个GPIO的翻转频率,也不可能达到80MHz,所以可以放心使用了。

160MHz是双重SPI模式等效的频率,320MHz是四重SPI模式等效的频率。

双重SPI和四重SPI:MOSI用于发送,MISO用于接收,是全双工通信,在只发或只收的时候有资源浪费,但是在这个W25Q芯片厂商不忍心浪费,所以就对SPI做出了一些改进,就是我在发的时候,我可以同时用MOSI和MISO发送,在收的时候,也可以同时用MOSI和MISO接收,MOSI和MISO同时兼具发送和接收的功能。一个SCK时钟,同时发送或接收2位数据,这就是双重SPI模式,一个时钟收发两位,相比较一位一位的普通SPI,数据传输率就是二倍了,所以在双重SPI模式下,等效的时钟频率就是160MH。但实际的SCK频率,最大还是80MHz,只是一个时钟发两位而已。

在我们的芯片里还有两位引脚,一位是WP写保护,另一个是HOLD,这两个引脚如果不需要的话,也可以拉过来充当数据传输引脚,加上MOSI和MISO,这就可以4个数据位同时收发了。

存储容量(24位地址):

W25Q40: 4Mbit / 512KByte

W25Q80: 8Mbit / 1MByte

W25Q16: 16Mbit / 2MByte

W25Q32: 32Mbit / 4MByte

W25Q64: 64Mbit / 8MByte

W25Q128: 128Mbit / 16MByte

W25Q256: 256Mbit / 32MByte

这个芯片使用的是24位地址,是3个字节,因为我们在进行读写的时候,肯定得把每个字节都分配一个地址,这样才能找到它们。

24位地址能够提供(2^24/1024/1024=16MB)的寻址空间。

W25Q256分为3字节地址模式和4字节地址模式,在3字节地址模式下,只能读取前16MB的数据,后面16MB,3个字节的地址够不着,要想读写到所有鵆单元,可以进入4字节地址的模式。

10.2.2 硬件电路

|----------|---------------|
| 引脚 | 功能 |
| VCC、GND | 电源(2.7~3.6V) |
| CS(SS) | SPI片选 |
| CLK(SCK) | SPI时钟 |
| DI(MOSI) | SPI主机输出从机输入 |
| DO(MISO) | SPI主机输入从机输出 |
| WP | 写保护 |
| HOLD | 数据保持 |

WP(Write Protect): 配合内部的寄存器配置,可以实现硬件的写保护,写保护低电平有效。WP接低电平,保护住,不让写,WP接高电平,不保护,可以写。

HOLD:如果在进行正常读写时, 突然产生中断,然后想用SPI通信线去操控其它器件,这时如果把CS置回高电平,那时序就终止了,但如果又不想终止总线,又想操作其它器件,这就可以HOLD引脚置低电平,这样芯片就HOLD住了。芯片释放总线,但是芯片时序也不会终止,它会记住当前的状态。当操作完其它器件时,可以回过来,HOLD置回高电平,然后继续HOLD之前的时序。相当于SPI总线进了一次中断,并且还在中断里,还可以用SPI干别的事情。

10.2.3 W25Q64框图

10.2.4 Flash操作注意事项

写入操作时:

写入操作前,必须先进行写使能;

这是一种保护操作,防止误操作,就像手机一样,先解锁再操作。

每个数据位只能由1改写为0,不能由0改写为1;

Flash并没有像RAM那样的直接完全覆盖改写的能力,比如在某个字节的存储单元里,存储了0xAA这个数据,对应的二进制位就是1010 1010,如果我直接在在这个存储单元写入一个新的数据,比如我再次写入一个0x55,写完之后这个存储单元里存的并不是0x5.因为0x55的二进制是0101 0101,当这个0101 0101要覆盖原来的1010 1010时,就会受到这条规定的限制,所以这里写入0101 0101之后,一次来看,最高位由原来的1改写为0是可以的,所以写入之后,新的最高位就是0,但是第二位原来是0,现在想改成1,这是不行的,所以写入之后新的第二位还是0,这样最终就会变成0x00,为了弥补这个缺陷,因为有了下一条规定。

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

因此,在Flash中,空白部分是0xFF。如果读取的是0xFF,那说明这部分有可能是还没有写入数据的空白空间。

擦除必须按最小擦除单元进行;

这个应该是为了成本而做的妥协,Flash不能指定某一个字节单元进行擦除,要擦就得一片一起擦,在我们这个芯片里可以选择整个芯片一起擦除,也可以选择按块擦除或者按扇区擦除。再小就没有了,所以最小的擦除单元,是一个扇区。一个扇区是4Kb,就是4096个字节。擦除时,如果不想丢失数据,只能先把这4096个字节的数据读取出来,再把4096个字节的扇区擦掉,改写完读出来的数据之后,再把4096个字节全部写回去。实际情况下,我们还有别的方法来优化这个流程,比如,上电后,我们先把Flash的数据读出来,放到RAM里,当有数据变动时,我们统一把数据备份到Flash里。或者我把使用频繁的扇区,放在RAM里,当使用频率降低时,我再把整个扇区被分到Flash里。或者如果数据量确实非常少,只想存几个字节的参数就行了,那直接1个字节占一个扇区就行。

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

在写入的时候,一次性不能写太多,一个写入时序,最多只能写一页的数据,也就是256字节。这是因为有一个页缓存区,它只有256字节。为什么会有缓存区呢?是因为Flash的写入太慢了,跟不上SPI的频率,所以写如的数据,会先放到RAM里暂存,等时序结束之后,芯片再慢慢地把数据写入到Flash里,所以这里会有一个限制,每个时序,最多写入一页的数据。这个页缓存区,是和Flash的页对应的,必需得从页的起始位置开始,才能最大写入256字节。如果从页中间的地址开始写,那写到页尾时,这个地址就会跳回到页首,这会导致地址错乱。所以在进行多字节写入时,一定要注意这个地址范围,不能跨越页的边沿,否则会地址错乱。

写入操作结束后,芯片进入忙状态,不响应新的读写操作。

我们的写入操作都是对缓存区进行的,等时序结束后,芯片还要搬砖一段时间,所以每次写入操作后,都有一段时间的忙状态,在这个状态下,我们不要进行新的读写操作,否则,芯片是不会相应我们的,要想知道芯片什么时候结束忙状态了。我们可以使用读状态寄存器的指令,看一下状态寄存器的BUSY位是否为1,BUSY位为0时,芯片就不忙了,我们再进行操作。

另外,这个写入操作包括上面的擦除,在发出擦除指令后,芯片也会进入忙状态,我们也得等忙状态结束后,才能进行后续操作。

读取操作时:

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

Flash作为一种掉电不丢失的存储器,为了保证掉电不丢失这个特性,同时还要保证存储容量足够大、成本足够低,所以Flash存储器会在其它地方,比如操作的便捷性等做一些妥协和让步。Flash的写入和读取并不像RAM那样简单直接,RAM是指哪打哪,想在哪写就在哪写,想写多少就写多少,并且RAM是可以覆盖写入的。比如原来RAM里有个数据0xAA,之后我直接再写入一个新的数据0x55,那RAM的数据就变成0x55了。

10.2.5 器件手册

(1)芯片引脚定义及描述

(2)芯片系统框图

(3)SPI操作

(4)写保护逻辑

(5)状态寄存器

状态寄存器示意图:

(6) 写保护配置表

(7)指令集

|----------------------------|-------------|
| 指令 | 翻译 |
| Write Enable | 写使能 |
| Write Disable | 写失能 |
| Read Status Register-1 | 读状态寄存器1 |
| Page Progam | 页编程 |
| Block Erase(64KB) | 按64KB的块擦除 |
| Block Erase(32KB) | 按32KB的块擦除 |
| Sector Erase(4KB) | 扇区擦除 |
| Chip Erase | 整片擦除 |
| JEDEC ID | 读ID号 |

|---------------|----------|
| 指令 | 翻译 |
| Read Data | 读取数据 |

10.3 软件SPI读写W25Q64

10.3.1 硬件电路

10.3.2 软件部分

(1)复制《OLED显示屏》工程并改名为《软件SPI读写W25Q64》

(2)添加驱动文件

(3)MySPI.c

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

/*从机选择函数*/
void MySPI_W_SS(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOA,GPIO_Pin_4,(BitAction)BitValue);           //SS端接在PA4引脚上
}

/*SCK控制函数*/
void MySPI_W_SCK(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOA,GPIO_Pin_5,(BitAction)BitValue);           //SCK端接在PA5引脚上
	
}

/*MOSI控制函数*/
void MySPI_W_MOSI(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOA,GPIO_Pin_7,(BitAction)BitValue);           //MOSI端接在PA7引脚上
}

/*MISO控制函数*/
uint8_t MySPI_R_MISO(void)
{
	return GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_6);                  //MISO端接在PA6引脚上,STM32读取W25Q64数据
}


/*软件SPI的初始化函数*/
void MySPI_Init(void)
{	
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);        
	GPIO_InitTypeDef GPIO_InitStruct;
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;     //输出引脚配置为推挽输出
	GPIO_InitStruct.GPIO_Pin = GPIO_Pin_4|GPIO_Pin_5|GPIO_Pin_7;
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStruct);
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;        //输入引脚配置为上拉输入
	GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6;
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStruct);
	MySPI_W_SS(1);                                    //初始化时给ss置高电平,默认不选中从机
	MySPI_W_SCK(0);                                   //使用模式0,默认是低电平。
}	

/*起始条件函数*/
void MySPI_Start(void)
{
	MySPI_W_SS(0); 
}	
/*终止条件函数*/
void MySPI_Stop(void)
{
	MySPI_W_SS(1); 
}	
/*交换字节函数,这种方法使用掩码依次提出每一位,不会改变传入参数本身*/
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{
	uint8_t i,ByteReceive = 0x00;                 //用来接收字节
	for(i=0;i<8;i++)
	{
		MySPI_W_MOSI(ByteSend & (0x80 >> i));       //发送第i位
		MySPI_W_SCK(1);                           //产生上升沿,程序把MOSI总线上的数据(ByteSend & 0x80)读走
		if (MySPI_R_MISO()==1){ByteReceive |= (0x80>> i);}
		MySPI_W_SCK(0);                           //产生下降沿,主机发送下一位
	}
	return ByteReceive;
}

/*交换字节函数,这种方法效率高,但是ByteSend在移位过程中改变了*/
//uint8_t MySPI_SwapByte(uint8_t ByteSend)
//{
//	uint8_t i,ByteReceive = 0x00;                 //用来接收字节
//	for(i=0;i<8;i++)
//	{
//		MySPI_W_MOSI(ByteSend & 0x80);            //发送最高位
//		ByteSend <<=1;                            //次高位向左移位,变成最高位,准备下一次发送         
//		MySPI_W_SCK(1);                           //产生上升沿,程序把MOSI总线上的数据(ByteSend & 0x80)读走
//		if (MySPI_R_MISO()==1){ByteSend |= 0x01;}
//		MySPI_W_SCK(0);                           //产生下降沿,主机发送下一位
//	}
//	return ByteReceive;
//}

(4)MySPI.h

cpp 复制代码
#ifndef __MYSPI_
#define __MYSPI_

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

#endif

(5)W25Q64_lns.h

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

(6)W25Q64.c

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

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

/*读取ID号函数*/
void W25Q64_ReadID(uint8_t *MID,uint16_t *DID)
{
	MySPI_Start();
	MySPI_SwapByte(W25Q64_JEDEC_ID);             //发送读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();
}

/*状态获取函数*/
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状态结束
	{
		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);
	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();
}

(7)W25Q64.h

cpp 复制代码
#ifndef __W25Q64_
#define __W25Q64
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

(8)main.c

cpp 复制代码
#include "stm32f10x.h"                  // Device header
#include "Delay.h"                      // 调用延时头文件
#include "OLED.h"
#include "W25Q64.h"


uint8_t MID;       //厂商ID
uint16_t DID;      //设备ID
uint8_t ArrayWrite[] = {0x01,0x02,0x03,0x04};
uint8_t ArrayRead[4];

int main(void)
{
	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,MID,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)        
	{	
                     
	}
}

10.4 STM32 SPI通信外设

10.4.1 SPI外设简介

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

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

最常用的是8位数据帧,高位先行。

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

时钟频率一般体现的是传输速度、单位是Hz或者bit/s。PSI的时钟,就是由****分频得来的,

PCLK(Peripheral Clock)就是外设时钟,APB2的PCLK就是72MHz,APB1的PCLK就是36MHz。

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

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

支持DMA;

兼容I2S协议;

音频传输协议。

STM32F103C8T6 硬件SPI资源:SPI1、SPI2。

SPI1是APB2的外设,SPI2是APB1的外设。

10.4.2 SPI框图

10.4.3 SPI基本结构

核心部分就是数据寄存器和移位寄存器了, 上图所画的是左移,高位移出去,通过GPIO,到MOSI,从MOSI输出,显然就是SPI的主机,之后移入的数据,从MISO进来,通过GPIO到移位寄存器的低位,这样循环8次,就能实现主机和从机交换一个字节,然后TDR行业RDR的配合,可以实现连续的数据流。另外,TDR数据整体转入移位寄存器的时刻,置TXE标志位;移位寄存器整体转入RDR的时刻,置RXNE标志位。

10.4.4 主模式全双工连续传输

连续传输、传输更快,但是操作起来相对复杂。

10.4.5 非连续传输

10.4.6 软件/硬件波形对比

10.5 硬件SPI读写W25Q64

10.5.1 硬件电路

10.5.2 软件部分

(1)复制《软件SPI读写W25Q64》并更改工程名为《硬件SPI读写W25Q64》

(2)修改"MySPI.c",其它文件不变。

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

/*从机选择函数*/
void MySPI_W_SS(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOA,GPIO_Pin_4,(BitAction)BitValue);           //SS端接在PA4引脚上
}

/*软件SPI的初始化函数*/
void MySPI_Init(void)
{	
	/*初始化GPIO*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); 
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE); 
	
	GPIO_InitTypeDef GPIO_InitStruct;
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;     //输出引脚配置为推挽输出
	GPIO_InitStruct.GPIO_Pin = GPIO_Pin_4;
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStruct);
	
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;     //输出引脚配置为复用推挽输出
	GPIO_InitStruct.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7;
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStruct);
	
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;     //输出引脚配置为上拉输入模式
	GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6;
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStruct);
	
	/*初始化GPIO外设*/
	SPI_InitTypeDef SPI_InitStructure;
	SPI_InitStructure.SPI_Mode = SPI_Mode_Master;     //指定当前设备为主机
	SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;       //配置双线全双工模式
	SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;  //配置8位数据帧
	SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB ;  //选择高位先行
	SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_128;   // 配置SCK的时钟频率
	SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low;       //时钟极性空闲时默认为低电平
	SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;     //设置第一个边沿开始采样,上面两个参数将SPI配置成模式0
	SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;        //这个外设的NSS引脚一般不会用到,所以一般选择软件NSS就可以了
	SPI_InitStructure.SPI_CRCPolynomial = 7;         //CRC校验的默认参数
	SPI_Init(SPI1,&SPI_InitStructure);
	SPI_Cmd(SPI1,ENABLE);                            //使能SPI外设
	MySPI_W_SS(1);                                   //默认给SS输出高电平,不选中从机
}	

/*起始条件函数*/
void MySPI_Start(void)
{
	MySPI_W_SS(0); 
}	
/*终止条件函数*/
void MySPI_Stop(void)
{
	MySPI_W_SS(1); 
}	
/*交换字节函数,这种方法使用掩码依次提出每一位,不会改变传入参数本身*/
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{
	while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_TXE)!= SET);        //监测TXE标志位是否为1,直到其等于1,卡死几率不大
	SPI_I2S_SendData(SPI1,ByteSend);             //ByteSend发送到TDR,之后转运到移位寄存器,生成波形自动完成
	while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_RXNE)!= SET);   //RXNE为1,表示大收到1个字节,同时也表示发送的时序产生完成了
	return SPI_I2S_ReceiveData(SPI1);            //读取RDR
}
相关推荐
EterNity_TiMe_3 分钟前
【论文复现】(CLIP)文本也能和图像配对
python·学习·算法·性能优化·数据分析·clip
sanguine__7 分钟前
java学习-集合
学习
lxlyhwl7 分钟前
【STK学习】part2-星座-目标可见性与覆盖性分析
学习
nbsaas-boot8 分钟前
如何利用ChatGPT加速开发与学习:以BPMN编辑器为例
学习·chatgpt·编辑器
日晨难再39 分钟前
嵌入式:STM32的启动(Startup)文件解析
stm32·单片机·嵌入式硬件
CV学术叫叫兽1 小时前
一站式学习:害虫识别与分类图像分割
学习·分类·数据挖掘
我们的五年1 小时前
【Linux课程学习】:进程程序替换,execl,execv,execlp,execvp,execve,execle,execvpe函数
linux·c++·学习
一棵开花的树,枝芽无限靠近你1 小时前
【PPTist】添加PPT模版
前端·学习·编辑器·html
VertexGeek2 小时前
Rust学习(八):异常处理和宏编程:
学习·算法·rust
二进制_博客2 小时前
Flink学习连载文章4-flink中的各种转换操作
大数据·学习·flink