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