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

发布于:2024-08-20 ⋅ 阅读:(118) ⋅ 点赞:(0)

第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不在乎花了多少钱,只在乎任务有没有最简单、最快速的完成。

四根通信线:SCKSerial Clock)、MOSIMaster Output Slave Input)、MISOMaster Input Slave Output)、SSSlave 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设备的SCKMOSIMISO分别连在一起;

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=0SCK第一个边沿移入数据,第二个边沿移出数据

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 硬件电路

引脚

功能

VCCGND

电源(2.7~3.6V

CSSS

SPI片选

CLKSCK

SPI时钟

DIMOSI

SPI主机输出从机输入

DOMISO

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

#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

#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

#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

#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

#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

#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位数据帧,高位先行。

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

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

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

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

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

支持DMA;

兼容I2S协议;

音频传输协议。

STM32F103C8T6 硬件SPI资源:SPI1SPI2。

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”,其它文件不变。

#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
}


网站公告

今日签到

点亮在社区的每一天
去签到