当前位置:   article > 正文

江协科技STM32学习笔记(第10章 SPI通信)_w25q256 16m之后的地址

w25q256 16m之后的地址

第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

  1. #include "stm32f10x.h" // Device header
  2. /*从机选择函数*/
  3. void MySPI_W_SS(uint8_t BitValue)
  4. {
  5. GPIO_WriteBit(GPIOA,GPIO_Pin_4,(BitAction)BitValue); //SS端接在PA4引脚上
  6. }
  7. /*SCK控制函数*/
  8. void MySPI_W_SCK(uint8_t BitValue)
  9. {
  10. GPIO_WriteBit(GPIOA,GPIO_Pin_5,(BitAction)BitValue); //SCK端接在PA5引脚上
  11. }
  12. /*MOSI控制函数*/
  13. void MySPI_W_MOSI(uint8_t BitValue)
  14. {
  15. GPIO_WriteBit(GPIOA,GPIO_Pin_7,(BitAction)BitValue); //MOSI端接在PA7引脚上
  16. }
  17. /*MISO控制函数*/
  18. uint8_t MySPI_R_MISO(void)
  19. {
  20. return GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_6); //MISO端接在PA6引脚上,STM32读取W25Q64数据
  21. }
  22. /*软件SPI的初始化函数*/
  23. void MySPI_Init(void)
  24. {
  25. RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
  26. GPIO_InitTypeDef GPIO_InitStruct;
  27. GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP; //输出引脚配置为推挽输出
  28. GPIO_InitStruct.GPIO_Pin = GPIO_Pin_4|GPIO_Pin_5|GPIO_Pin_7;
  29. GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
  30. GPIO_Init(GPIOA,&GPIO_InitStruct);
  31. GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU; //输入引脚配置为上拉输入
  32. GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6;
  33. GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
  34. GPIO_Init(GPIOA,&GPIO_InitStruct);
  35. MySPI_W_SS(1); //初始化时给ss置高电平,默认不选中从机
  36. MySPI_W_SCK(0); //使用模式0,默认是低电平。
  37. }
  38. /*起始条件函数*/
  39. void MySPI_Start(void)
  40. {
  41. MySPI_W_SS(0);
  42. }
  43. /*终止条件函数*/
  44. void MySPI_Stop(void)
  45. {
  46. MySPI_W_SS(1);
  47. }
  48. /*交换字节函数,这种方法使用掩码依次提出每一位,不会改变传入参数本身*/
  49. uint8_t MySPI_SwapByte(uint8_t ByteSend)
  50. {
  51. uint8_t i,ByteReceive = 0x00; //用来接收字节
  52. for(i=0;i<8;i++)
  53. {
  54. MySPI_W_MOSI(ByteSend & (0x80 >> i)); //发送第i位
  55. MySPI_W_SCK(1); //产生上升沿,程序把MOSI总线上的数据(ByteSend & 0x80)读走
  56. if (MySPI_R_MISO()==1){ByteReceive |= (0x80>> i);}
  57. MySPI_W_SCK(0); //产生下降沿,主机发送下一位
  58. }
  59. return ByteReceive;
  60. }
  61. /*交换字节函数,这种方法效率高,但是ByteSend在移位过程中改变了*/
  62. //uint8_t MySPI_SwapByte(uint8_t ByteSend)
  63. //{
  64. // uint8_t i,ByteReceive = 0x00; //用来接收字节
  65. // for(i=0;i<8;i++)
  66. // {
  67. // MySPI_W_MOSI(ByteSend & 0x80); //发送最高位
  68. // ByteSend <<=1; //次高位向左移位,变成最高位,准备下一次发送
  69. // MySPI_W_SCK(1); //产生上升沿,程序把MOSI总线上的数据(ByteSend & 0x80)读走
  70. // if (MySPI_R_MISO()==1){ByteSend |= 0x01;}
  71. // MySPI_W_SCK(0); //产生下降沿,主机发送下一位
  72. // }
  73. // return ByteReceive;
  74. //}

(4)MySPI.h

  1. #ifndef __MYSPI_
  2. #define __MYSPI_
  3. void MySPI_Init(void);
  4. void MySPI_Start(void);
  5. void MySPI_Stop(void);
  6. uint8_t MySPI_SwapByte(uint8_t ByteSend);
  7. #endif

(5)W25Q64_lns.h

  1. #ifndef __W25Q64_INS_H
  2. #define __W25Q64_INS_H
  3. #define W25Q64_WRITE_ENABLE 0x06
  4. #define W25Q64_WRITE_DISABLE 0x04
  5. #define W25Q64_READ_STATUS_REGISTER_1 0x05
  6. #define W25Q64_READ_STATUS_REGISTER_2 0x35
  7. #define W25Q64_WRITE_STATUS_REGISTER 0x01
  8. #define W25Q64_PAGE_PROGRAM 0x02
  9. #define W25Q64_QUAD_PAGE_PROGRAM 0x32
  10. #define W25Q64_BLOCK_ERASE_64KB 0xD8
  11. #define W25Q64_BLOCK_ERASE_32KB 0x52
  12. #define W25Q64_SECTOR_ERASE_4KB 0x20
  13. #define W25Q64_CHIP_ERASE 0xC7
  14. #define W25Q64_ERASE_SUSPEND 0x75
  15. #define W25Q64_ERASE_RESUME 0x7A
  16. #define W25Q64_POWER_DOWN 0xB9
  17. #define W25Q64_HIGH_PERFORMANCE_MODE 0xA3
  18. #define W25Q64_CONTINUOUS_READ_MODE_RESET 0xFF
  19. #define W25Q64_RELEASE_POWER_DOWN_HPM_DEVICE_ID 0xAB
  20. #define W25Q64_MANUFACTURER_DEVICE_ID 0x90
  21. #define W25Q64_READ_UNIQUE_ID 0x4B
  22. #define W25Q64_JEDEC_ID 0x9F
  23. #define W25Q64_READ_DATA 0x03
  24. #define W25Q64_FAST_READ 0x0B
  25. #define W25Q64_FAST_READ_DUAL_OUTPUT 0x3B
  26. #define W25Q64_FAST_READ_DUAL_IO 0xBB
  27. #define W25Q64_FAST_READ_QUAD_OUTPUT 0x6B
  28. #define W25Q64_FAST_READ_QUAD_IO 0xEB
  29. #define W25Q64_OCTAL_WORD_READ_QUAD_IO 0xE3
  30. #define W25Q64_DUMMY_BYTE 0xFF
  31. #endif

(6)W25Q64.c

  1. #include "stm32f10x.h" // Device header
  2. #include "MySPI.h"
  3. #include "W25Q64_lns.h"
  4. /*W25Q64初始化函数*/
  5. void W25Q64_Init(void)
  6. {
  7. MySPI_Init();
  8. }
  9. /*读取ID号函数*/
  10. void W25Q64_ReadID(uint8_t *MID,uint16_t *DID)
  11. {
  12. MySPI_Start();
  13. MySPI_SwapByte(W25Q64_JEDEC_ID); //发送读ID号的指令
  14. *MID = MySPI_SwapByte(W25Q64_DUMMY_BYTE); //随便给从机发一个东西,没有意义,目的就是把从机的数据置换过来,获取到厂商ID
  15. *DID = MySPI_SwapByte(W25Q64_DUMMY_BYTE); //获取设备ID的高8位
  16. *DID <<= 8;
  17. *DID |= MySPI_SwapByte(W25Q64_DUMMY_BYTE); //获取设备ID的低8位
  18. MySPI_Stop();
  19. }
  20. /*写使能*/
  21. void W25Q64_WriteEnable(void)
  22. {
  23. MySPI_Start();
  24. MySPI_SwapByte(W25Q64_WRITE_ENABLE);
  25. MySPI_Stop();
  26. }
  27. /*状态获取函数*/
  28. void W25Q64_WaitBusy(void)
  29. {
  30. uint32_t Timeout;
  31. MySPI_Start();
  32. MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1); //获取寄存器状态指令
  33. Timeout = 100000;
  34. while((MySPI_SwapByte(W25Q64_DUMMY_BYTE) & 0x01) == 0x01) //等待Busy状态结束
  35. {
  36. Timeout--;
  37. if(Timeout == 0)
  38. {
  39. break; //超时退出
  40. }
  41. }
  42. MySPI_Stop();
  43. }
  44. /*页编程函数*/
  45. void W25Q64_PageProgram(uint32_t Address,uint8_t *DataArray,uint16_t Count)
  46. {
  47. uint16_t i;
  48. W25Q64_WriteEnable(); //写入操作前,都必须进行写使能
  49. MySPI_Start();
  50. MySPI_SwapByte(W25Q64_PAGE_PROGRAM);
  51. MySPI_SwapByte(Address >> 16);
  52. MySPI_SwapByte(Address >> 8);
  53. MySPI_SwapByte(Address);
  54. for(i=0;i<Count;i++)
  55. {
  56. MySPI_SwapByte(DataArray[i]);
  57. }
  58. MySPI_Stop();
  59. W25Q64_WaitBusy();
  60. }
  61. /*扇区擦除函数*/
  62. void W25Q64_SectorErase(uint32_t Address)
  63. {
  64. W25Q64_WriteEnable(); //写入操作前,都必须进行写使能
  65. MySPI_Start();
  66. MySPI_SwapByte(W25Q64_SECTOR_ERASE_4KB);
  67. MySPI_SwapByte(Address >> 16);
  68. MySPI_SwapByte(Address >> 8);
  69. MySPI_SwapByte(Address);
  70. MySPI_Stop();
  71. W25Q64_WaitBusy();
  72. }
  73. /*读取数据函数*/
  74. void W25Q64_ReadData(uint32_t Address,uint8_t *DataArray,uint32_t Count)
  75. {
  76. uint32_t i;
  77. MySPI_Start();
  78. MySPI_SwapByte(W25Q64_READ_DATA);
  79. MySPI_SwapByte(Address >> 16);
  80. MySPI_SwapByte(Address >> 8);
  81. MySPI_SwapByte(Address);
  82. for(i=0;i<Count;i++)
  83. {
  84. DataArray[i] = MySPI_SwapByte(W25Q64_DUMMY_BYTE);
  85. }
  86. MySPI_Stop();
  87. }

(7)W25Q64.h

  1. #ifndef __W25Q64_
  2. #define __W25Q64
  3. void W25Q64_Init(void);
  4. void W25Q64_ReadID(uint8_t *MID,uint16_t *DID);
  5. void W25Q64_PageProgram(uint32_t Address,uint8_t *DataArray,uint16_t Count);
  6. void W25Q64_SectorErase(uint32_t Address);
  7. void W25Q64_ReadData(uint32_t Address,uint8_t *DataArray,uint32_t Count);
  8. #endif

(8)main.c

  1. #include "stm32f10x.h" // Device header
  2. #include "Delay.h" // 调用延时头文件
  3. #include "OLED.h"
  4. #include "W25Q64.h"
  5. uint8_t MID; //厂商ID
  6. uint16_t DID; //设备ID
  7. uint8_t ArrayWrite[] = {0x01,0x02,0x03,0x04};
  8. uint8_t ArrayRead[4];
  9. int main(void)
  10. {
  11. OLED_Init(); // 初始化OLED屏幕
  12. W25Q64_Init();
  13. OLED_ShowString(1,1,"MID: DID:");
  14. OLED_ShowString(2,1,"W:");
  15. OLED_ShowString(3,1,"R:");
  16. W25Q64_ReadID(&MID,&DID);
  17. OLED_ShowHexNum(1,5,MID,2);
  18. OLED_ShowHexNum(1,12,MID,4);
  19. // W25Q64_SectorErase(0x000000);
  20. // W25Q64_PageProgram(0x000000,ArrayWrite,4);
  21. W25Q64_ReadData(0x000000,ArrayRead,4);
  22. OLED_ShowHexNum(2,3,ArrayWrite[0],2);
  23. OLED_ShowHexNum(2,6,ArrayWrite[1],2);
  24. OLED_ShowHexNum(2,9,ArrayWrite[2],2);
  25. OLED_ShowHexNum(2,12,ArrayWrite[3],2);
  26. OLED_ShowHexNum(3,3,ArrayRead[0],2);
  27. OLED_ShowHexNum(3,6,ArrayRead[1],2);
  28. OLED_ShowHexNum(3,9,ArrayRead[2],2);
  29. OLED_ShowHexNum(3,12,ArrayRead[3],2);
  30. while(1)
  31. {
  32. }
  33. }

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

  1. #include "stm32f10x.h" // Device header
  2. /*从机选择函数*/
  3. void MySPI_W_SS(uint8_t BitValue)
  4. {
  5. GPIO_WriteBit(GPIOA,GPIO_Pin_4,(BitAction)BitValue); //SS端接在PA4引脚上
  6. }
  7. /*软件SPI的初始化函数*/
  8. void MySPI_Init(void)
  9. {
  10. /*初始化GPIO*/
  11. RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
  12. RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);
  13. GPIO_InitTypeDef GPIO_InitStruct;
  14. GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP; //输出引脚配置为推挽输出
  15. GPIO_InitStruct.GPIO_Pin = GPIO_Pin_4;
  16. GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
  17. GPIO_Init(GPIOA,&GPIO_InitStruct);
  18. GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP; //输出引脚配置为复用推挽输出
  19. GPIO_InitStruct.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7;
  20. GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
  21. GPIO_Init(GPIOA,&GPIO_InitStruct);
  22. GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU; //输出引脚配置为上拉输入模式
  23. GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6;
  24. GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
  25. GPIO_Init(GPIOA,&GPIO_InitStruct);
  26. /*初始化GPIO外设*/
  27. SPI_InitTypeDef SPI_InitStructure;
  28. SPI_InitStructure.SPI_Mode = SPI_Mode_Master; //指定当前设备为主机
  29. SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; //配置双线全双工模式
  30. SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; //配置8位数据帧
  31. SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB ; //选择高位先行
  32. SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_128; // 配置SCK的时钟频率
  33. SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; //时钟极性空闲时默认为低电平
  34. SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; //设置第一个边沿开始采样,上面两个参数将SPI配置成模式0
  35. SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; //这个外设的NSS引脚一般不会用到,所以一般选择软件NSS就可以了
  36. SPI_InitStructure.SPI_CRCPolynomial = 7; //CRC校验的默认参数
  37. SPI_Init(SPI1,&SPI_InitStructure);
  38. SPI_Cmd(SPI1,ENABLE); //使能SPI外设
  39. MySPI_W_SS(1); //默认给SS输出高电平,不选中从机
  40. }
  41. /*起始条件函数*/
  42. void MySPI_Start(void)
  43. {
  44. MySPI_W_SS(0);
  45. }
  46. /*终止条件函数*/
  47. void MySPI_Stop(void)
  48. {
  49. MySPI_W_SS(1);
  50. }
  51. /*交换字节函数,这种方法使用掩码依次提出每一位,不会改变传入参数本身*/
  52. uint8_t MySPI_SwapByte(uint8_t ByteSend)
  53. {
  54. while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_TXE)!= SET); //监测TXE标志位是否为1,直到其等于1,卡死几率不大
  55. SPI_I2S_SendData(SPI1,ByteSend); //ByteSend发送到TDR,之后转运到移位寄存器,生成波形自动完成
  56. while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_RXNE)!= SET); //RXNE为1,表示大收到1个字节,同时也表示发送的时序产生完成了
  57. return SPI_I2S_ReceiveData(SPI1); //读取RDR
  58. }

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Guff_9hys/article/detail/1015376
推荐阅读
相关标签
  

闽ICP备14008679号