SPI通信
·SPI,(serial peripheral interface),字面翻译是串行外设接口,是一种通用的数据总线,适用于主控和外挂芯片之间的通信,与IIC应用领域非常相似。
·IIC无论是在硬件电路还是在软件时序设计的都是较为复杂的,硬件上需要配置成开漏外加上拉的模式,软件上需要一根数据线兼顾数据收发、应答位收发、寻址机制的设计等等,使得IIC的性价比极高,可以在消耗最低硬件资源的情况下实现最多的功能。在硬件上无论挂载多少个设备(128最多)都只需要两根通讯线,在软件上数据双向通信、应答位都可以实现。IIC既实现硬件上最少的通信线,又实现软件上最多的功能。
但是IIC的缺点在于:IIC开漏外加上拉电阻的电路结构,使得通讯线高电平的驱动能力较弱,会导致通信线由低电平到高电平的时候上升沿会比较长,会最大限制IIC的通信速度,所以IIC的标准模式只有100KHz的时钟频率,快速模式也只有400KHz,虽然后边改进电路设计出了高速模式可达3.4MHz,但是普及程度不高,一般情况下认为IIC时钟速度最高400KHz,相比SPI慢很多。
·SPI传输比IIC更快,SPI协议没有严格规定最大传输速度,最大传输速度取决于芯片厂商的设计需求,比如 W25Q64 存储器芯片,手册中写最大时钟频率可达80MHz,比stm32f1的主频还高。SPI硬件通信线比较多,通信过程中常有资源浪费的现象。
·SPI有四根通信线,分别是SCK(serial CLOCK串行时钟线)、MOSI(master output slave input主机输出从机输入)、MISO(master input slave output主机输入从机输出)、SS(slave select从机选择)
·通信线名字的额外表述方式:
SCK:SCLK、CLK、CK
MOSI:DO( DATA OUTPUT )
MISO:DI( DATA INPUT )
SS:NSS( NOT SLAVE SELET )、CS( CHIP SELECT )
·同步:SCK引脚用于提供时钟信号,数据位的输出和输入都是在SCK的上升沿或下降沿进行,数据位的收发时刻因此得以确定。同步时序下时钟快慢或中途暂停都没问题。SCK相当于IIC通信下的SCL,作用相同。
·全双工:数据发送和数据接收单独各占一条线,发送用发送的线路,接收用接收的线路,互不影响。MOSI如果是主机接在上面,那就是MO主机输出,如果是从机接在这条线上就是MI从机输入。一条通信线如果主机接在上面配置为输出,从机必须配置为输入才能接收数据,主机和从机不能同时配置为输出和输入,会造成无法通信。同理MISO是主机从从机接收数据的线路。这两根通信线加在一起相当于IIC通信的SDA,不过IIC是半双工。全双工的好处是简单高效,数据流方向不会改变,无需担心发送和接收没协调好冲突,但是多了一根线造成了资源的浪费。
·一主多从:SPI仅支持一主多从,IIC实现一主多从的方式是在起始条件之后主机必须先发送一个字节进行寻址,用于规定要和哪个从机进行通信,要设计分配地址和寻址的问题。SPI实现一主多从的方式通过开辟多条通信线用于和从机通信,有几个从机就对应有几条SS,需要找哪个从机的时候就控制信号线为低电平,不需要时信号线为高电平。SPI没有应答机制,是否具有接收方无法得知。
硬件电路
这个图是SPI的一个典型电路,左边是SPI主机,主导整个SPI总线,一般是用控制器做主机如stm32,由于有3个从机,所以SS有3根,加上SCK、MOSI、MISO共6根,由于SPI所有通信线都是单端信号,高低电平都是相对于GND的电压差,单端信号所有的设备需要共地,图中未画出,但是必须要接。如果从机没有独立供电的情况下,主机需引出VCC给从机供电。
SCK时钟线完全由主机掌控,对主机来说时钟线为输出,对于所有主机来说时钟线都为输入,主机的同步时钟将送到各个从机。
主机的SS线都是输出,从机的SS都是输入,SS线是低电平有效,主机在指定从机时只需将SS置低电平即可。主机初始化之后所有的SS都输出高电平,谁也不指定,当需要和从机1进行通信时就把SS1线输出低电平,此时主机在数据引脚进行的传输只有从机1会响应,当主机和从机1通信完成之后将会把SS1置回高电平,结束通信。
输出引脚配置为推挽输出,因为推挽输出在高低电平均具有较强的驱动能力,这将是的SPI引脚信号的下降沿非常迅速,上升沿也非常迅速,不像IIC下降沿迅速上升沿缓慢。得益于推挽输出的驱动能力,SPI信号变化的快,能达到更高的传输速度(MHz级别)。
IIC由于要实现半双工,经常需要切换输入输出,还要实现多主机的时钟同步和总线仲裁,这些功能都不允许IIC使用推挽输出,易造成电源短路。IIC选择了更多的功能,因此只能放弃更强的性能。
SPI的MISO引脚中主机一个是输入,三个从机都是输出,如果三个从机始终都是推挽输出将会导致冲突,因此SPI协议中有一个规定,当从机的SS引脚为高电平,即从机未被选中时,它的MISO引脚需切换为高组态(相当于引脚断开,不输出任何电平),以防止一条线有多个输出导致电平冲突,只有当SS为低电平时MISO才允许变为推挽输出。由于这个切换过程都在从机里进行,一般我们写的是主机的程序,主机的程序中也无需关心此问题。
移位示意图(核心)
SPI的基本收发电路使用了图示的移位模型,左边是SPI的主机,内置一个8为的移位寄存器,右边是SPI从机,同样是8位的移位寄存器。在移位寄存器下方有一个时钟输入端,SPI一般是高位先行,每来一个时钟移位寄存器都会向左进行移位,从机中的移位寄存器同理。移位寄存器的时钟源是由主机提供的,在这里叫做波特率发生器,产生的时钟驱动主机的移位寄存器进行移位,同时这个时钟通过SCK引脚进行输出,接到从机的移位寄存器里。
移位寄存器的接法是:主机移位寄存器左边移出去的数据通过MOSI引脚输入到从机移位寄存器的右边,从机移位寄存器左边移出去的数据通过MISO引脚输入到主机移位寄存器的右边,组成一个圈。
发送同时接收
电路的工作流程:首先规定波特率时钟的上升沿,所有移位寄存器向左移动一位,移出去的位放在引脚上;波特率时钟的下降沿时,引脚上的位采样输入到移位寄存器的最低位。
假设主机有个数据1010 1010要发送到从机,同时从机有个数据0101 0101要发送到主机,那么可以先驱动时钟产生一个上升沿,这时所有的位将会向左移动一次,从最高位移出去的数据将会放在通信线上,实际上是放在了输出寄存器,MOSI数据是1,即MOSI的电平是高电平,MISO的数据是0,所以MISO的电平是低电平。第一个时钟沿就是把主机和从机中移位寄存器的最高位分别放到MOSI和MISO的通信线上,这是数据的输出的部分。
之后时钟继续运行,上升沿之后下一个边沿就是下降沿,在下降沿时主机和从机内都会进行数据采样输入,也就是MOSI里的1会采样输入到从机里的最低位。
在下一个上升沿进行同样的操作,移位输出,主机现在的最高位,也就是原始数据的次高位输出到MOSI从机现在的最高位输出到MISO,随后下降沿数据采样输入
MISO数据到从机寄存器的最低位,MISO数据到主机寄存器的最低位
第三个时钟开始 一直到第八个时钟都是一样的过程,最终八个时钟过后得到如下结果,原本主机的1010 1010跑道从机里边,原来从机里的0101 0101跑到了主机里,实现了主机和从机一个字节数据的交换。
以上就是SPI的运行过程,SPI的数据收发都是基于字节交换这个基本单元进行,当主机需要发送一个字节同时接收一个字节时,就可以执行字节交换的时序,主机要发送的数据跑到从机,主机要从从机接收的数据跑到主机,完成了发送同时接收的目的。
只发送不接收
如果想只发送不接收,仍然可以调用交换字节的时序,发送同时接收,不过不看接收到的数据即可;指向接收不发送同理,调用交换字节的时序,发送同时接收,不过会随便发送一个数据,目的是为了将从机的数据置换过来即可,读取置换后的数据就相当于接收,随意发送的数据从机并不会进行读取,不过这个随便的数据并不是真的随便,会统一发送0x00或0xff去和从机换数据。
SPI时序基本单元
起始条件:SS从高电平切换到低电平,是左边的图,SS是低电平有效,SS从高变到低代表着选中了某一个从机,也是通信的开始
终止条件:SS从低电平切换到高电平,是右边的图,SS从低电平变到高电平,就是结束了从机的选中状态,通信结束。
在从机的选中的整个过程中,SS要一直保持低电平。
模式1
这个基本单元建立在上边的移位模型上。基本单元中在上升沿开始移位还是下降沿移位SPI并没有限制死,给予可配置的选择,以兼容更多的芯片。这里SPI有两个可以配置的位,分别是CPOL(Clock Polarity时钟极性),CPHA(Clock phase时钟相位),每一位可配置为1或0,
总共组合起来有模式0、1、2、3共四种模式,模式虽多但功能一样,模式1接近于上边的知识内容。
时序的基本内容是交换一个字节,CPOL=0代表空闲状态时SCK为低电平,在SS未被选中时SCK默认低电平;CPHA=1表示SCK第一个边沿移出数据第二个边沿移入数据。
时序图中SS从机选择,通信开始之前SS为高电平,通信过程中SS始终保持低电平,通信结束SS恢复高电平
最下边的MISO主机输入从机输出,多个从机连在一起,如果同时开启输出会造成冲突,解决方法是在SS未被选中的状态下,从机的MISO引脚需关断输出,即配置为高阻态输出状态,如图位于中间电平的为高阻态,SS下降沿之后从机的MISO被允许开启输出,SS上升沿之后从机的MISO必须置回高阻态。
移位传输:因为CPHA=1,SCK第一个边沿移出数据,图中可见SCK第一个触发沿是上升沿,主机和从机同时移出数据,主机通过MOSI移出最高位,此时MISO的电平就代表了主机要发送的数据B7,从机通过MOSI移出最高位,此时MISO表示从机要发送的数据B7。时钟运行,产生下降沿,此时主机和从机同时移入数据,即进行数据采样。主机移出的B7进入从机移位寄存器的最低位,从机移出的B7进入主机移位寄存器的最低位,当一个时钟脉冲产生完毕时一个数据传输完毕,后边的数据交换同理,直到最后一个下降沿,数据B0传输完成,主机和从机完成一个字节的数据交换,如果只想交换一个字节数据,接下来就可以将SS置高电平结束通信。
在SS的上升沿MISO还可以再进行变化,将MOSI置默认的高电平or低电平,也可以不管,因为SPI没有规定MOSI的默认电平,MISO从机必须置回高阻态。如果此时主机的MISO位上拉输入的话,那MISO的引脚就是默认的高电平,如果主机的引脚为浮空输入,那MISO的引脚电平不确定。如果主机还想进行数据交换,则不必把SS置回高电平,直接重复交换一个字节的时序即可。
模式3(与模式1对比)
模式1和模式3的区别,模式1的CPOL=0,模式3的CPOL=1,两者的波形区别在于SCK的极性取反,其他地方没区别。
模式0(应用最多)
模式0的CPHA=0,在时序上的区别在于模式0的数据移出移入的时机,会提前半个时钟,也就是相位提前。第一个边沿移入数据,第二个边沿移出数据,但是数据是需要先移出才能移入的,所以SCK在第一个边沿之前就要提前移出数据,或者说是在第0个边沿移出第一个边沿移入。
首先SS下降沿开始通信,SCK还没有变化,但是SCK一旦开始变化就要移入数据。趁SCK还没变化,在SS下降沿时就要立刻触发移位输出,所以图中MISO和MOSI的输出是对齐到SS的下降沿,或者说SS的下降沿也被当做时钟的一部分。
SS下降沿触发了输出,SCK上升沿就可以采样输入数据了,这样B7传输完毕。接下来SCK下降沿移出B6,SCK上升沿移入B6,后边持续下降沿移出数据上升沿移入数据,最终在第八个上升沿时B0位移入完成,整个字节交换完成。之后SCK还有一个下降沿,如果主机只需要交换一个字节就结束,那么在下降沿时MOSI可以置回低电平或者不管,MISO也会变化一次,变化的那个地方对应的是下一个字节的B7,因为相位提前了所以下一个字节的B7会提前露头,如果不需要转换多个字节的话,SS上升沿之后从机置回高阻态,交换一个字节结束。如果需要转换多个字节的话就继续调用交换字节的时序,在最后一个下降沿主机和从机都放下一个字节的B7,SCK上升沿正好接着采样第二个字节的B7,拼接上时序。
模式0和模式1的区别在于:模式0把数据变化的时机给提前了,实际应用中模式0应用最多,后续程序均以模式0为主。
模式2(与模式0对比)
模式2与模式0的区别在于模式0的CPOL=0,模式2的CPOL=1,两者的波形就是SCK的极性取反,剩下的流程完全一致
总结
CPHA表示的是时钟相位,决定是第一个时钟采样移入还是第二个时钟采样移入,并不是规定上升沿采样还是下降沿采样。在CPOL确定的情况下,CPHA会改变采样时刻的上升沿和下降沿,比如模式0的时候是SCK上升沿采样移入,模式1的时候是SCK下降沿采样移入。CPHA决定是第几个边沿采样,但不能单独决定是上升沿还是下降沿。模式0和3都是上升沿采样,模式1和2都是下降沿采样。
SPI发送单字节时序(基于W25Q64的SPI)
SPI对字节流控的规定与IIC不同,IIC规定一般是有效数据流第一个字节是寄存器地址,之后依次是读写的数据,使用的是读写寄存器的模型。
在SPI中通常使用的是指令吗加读写数据的模型,在SPI起始后第一个交换发送给从机的数据一般叫做指令码,在从机中一般会定义一个指令集,当需要发送指令时,可以在起始后的第一个字节发送指令集里边的数据,指导从机完成相应的功能,不同的指令有不同的数据个数,有的指令仅需一个指令码即可完成,比如W25Q64的写使能和失能的指令;有的指令后面需要跟读写的数据,比如W25Q64的写数据和读数据等。
对于上边的时序图,功能是发送0x06,具体意思由芯片厂商自定义,在W25Q64里边代表的是写使能。使用SPI模式0,在空闲状态时SS为高电平,SCK为低电平,MOSI和MISO的默认电平没有严格规定。SS产生下降沿,时序开始。在下降沿时刻MISO和MOSI开始交换数据,MOSI由于指令码最高位仍然是0,所以保持低电平不变;MISO从机暂无数据发送给主机,引脚电平无变化。当W25Q64不需要回传数据时,手册规定MISO仍然为高阻态,从机未开启输出。
stm32的MISO是上拉输入,所以MISO呈现的是高电平,之后SCK第一个上升沿进行数据采样,从机采样得到0,主机采样得到1;第二个时钟时,主机数据仍然是0,所以波形没变化;后边一位一位的发送接收,直到第七位才开始变化,主机发送数据1,下降沿将数据移出,主机将1移动到MOSI,MOSI变为高电平,图中为软件模拟的时序,有延迟,并没有紧贴SCK的下降沿,不过并不影响,只需在下一个SCK上升之前完成即可。SCK上升沿数据采样输入,在最后一位下降沿数据变化,MOSI变为0,上升沿数据采样,从机接收0。SCK低电平是变化的,高电平是读取的时期,时序SCK最后一个上升沿结束,一个字节交换完毕。
写使能是单独的指令,不需要跟随指令,SPI只需要交换一个字节即可。在SCK下降沿之后,SS置回高电平结束通信。主机用0x06换来从机的0xff,但是实际上从机并没有输出,0xff是默认的高电平,并没有实际意义。
整个时序的功能是发送指令,指令码是0x06,从机一比对事先定义好的指令集,发现0x06是写使能的指令,从机就会控制硬件写使能,完成一个指令从发送到执行的过程
SPI指定地址写时序
W25Q64这个芯片有8M字节的存储空间,一个字节的8位地址不够,所以这里的地址是24位的,分3字节传输。
第一个小图(1/5)
SS下降沿开始时序,MOSI空闲时是高电平,在下降沿之后SCK第一个时钟之前,MOSI变换数据,由高电平变换为低电平,SCK上升沿数据采样输入,后边全部一样,下降沿变换数据,上升沿采集数据,八个时钟之后一个字节交换完成,用0x02换来0x0f,其中0x02是一条指令,代表这是一个写数据的时序,接受到了0xff不需要看。
写数据的时序后边必然跟着写的地址和数据,在最后一个下降沿时刻需要将下一个字节的最高位放到MOSI上,方便后续的交换字节。
第二个小图(2/5)
下一个字节的最高位仍然是0,数据没变化,后续还是一样的流程交换字节,用0x12交换0xff,根据数据手册的规定写指令后的字节定义为地址高位,0x12代表发送地址的23-16位。
第三、四个小图(3/5、4/5)
交换一个字节,发送0x34,表示发送地址的15-8位,之后还是交换一个字节,发送的是0x56,表示发送地址的0-7位, 这三个字节的交换,24位的地址交换完毕,从机收到的24位地址是0x12345。
第五个小图(5/5)
三位地址结束之后,要发送指定地址的内容,继续调用交换一个字节,发送数据0x55,表示要在0x12345这个地址下写入0x55这个数据,如果只想写入一个数据的话,SS置高电平结束通信;也可以继续发送数据。SPI里面也有像IIC一样的地址指针,每读写一个字节,地址指针自动加1,如果发送一个字节后不终止,继续发送的字节就会依次存储到后续的存储空间里,实现指定一个地址开始写入多字节的功能。
由于SPI没有应答机制,交换完一个字节之后就立刻交换下一个字节。从上述指令可以看出,在这个过程中由于我们只实现发送的功能,并没有接受的需求,所以MISO这条接收的线路始终处于挂机的状态,我们并没有用到。
W25Q64的容量大需要用到三个字节写地址,其他的小容量一个字节的地址即可,有的芯片会将地址融合到指令码之中。
SPI指定地址读时序
时序的用法和之前一模一样,观察交换的数据。起始后第一个字节 ,主机发送0x03表示要读取数据;之后主机依次交换三个字节,分别是0x12、0x34、0x56,组合到一起就是0x123456,代表24位地址;区别在于最后部分的时序,由于是读时序,指定地址后显然要进行接收数据,在三个字节的地址交换完成之后,需要用交换数据的方法把从机的数据交换过来,给从机0xff,从机将0x123456地址下的数据通过MISO发送给主机,波形表示指定地址下的数据是0x55,主机实现了指定地址读数据的目的;如果继续交换,那么从机内部的地址指针自动+1,从机将会把指定地址的下一个位置的数据发送给主机,多次运行之后便可实现指定地址接收多个字节的目的。最后数据传输完毕,SS置回高电平时序结束。
由于MISO是硬件控制的波形,数据变化都可以紧贴时钟的下降沿。MISO的最高位实际上是在上一个字节,最后一个下降沿提前发生的,因为这是SPI模式0,数据需要提前半个周期。
W25Q64简介
非易失性存储器:数据不容易失去的存储器,数据掉电不丢失,存储在芯片中的数据在断电重启之后数据仍然保持原样。
固件程序存储:相当于直接把程序文件下载到外挂芯片里,需要执行程序的时候,直接读取外挂芯片的程序文件来执行,这就是XIP(eXecute In Place)就地执行,比如电脑里的BIOS固件,就可以存储在这个W25Q系列的芯片里。
芯片的存储介质是Nor Flash(闪存),像stm32的程序存储器、u盘、电脑里的固态硬盘等都是使用Flash闪存。闪存分为Nor Flash和Nand Flash。
时钟频率80MHz比stm32快,后边写程序的时候翻转引脚无需加延时,因为不延时GPIO的频率也达不到80MHz,可放心使用。
160MHz是双重SPI模式等效的频率,320MHz是四重SPI等效的频率。MISO和MOSI在全双工通信时只发或只收会有资源浪费,W25Q64的厂商为了防止资源浪费,设计了在收的时候可以同时使用MOSI和MISO接收,发送也可以同时发送,同时兼具发送和接收的功能,一个SCK时钟同时发送或接收2位数据。一个时钟收发两位,相比于一位一位的普通SPI,数据传输率就是2倍,即在双重SPI模式下,等效的时钟频率就是80MHz的2倍160MHz,实际上的SCK最大频率还80MHz,知识一个时钟发2位而已。
四重SPI模式,很显然就是一个时钟发送或接收4位,等效的频率就是80*4=320MHz。在芯片中除了SPI通信引脚,还有两个引脚,一个是WP写保护,一个是HOLD,这两个引脚如果不需要的话,也可以拿来充当数据传输引脚,加上MISO和MOSI就可以四个数据位同时收发。相当于并行传输,串行传输是根据时钟一位一位的发送,并行是一个时钟8位同时发送,这个四重SPI模式其实就是4位并行的模式。
容量习惯以字节为单位,一个字节对应8个二进制位,实际容量是64Mbit除以8也就是8MByte,bit对应到byte要除以8。
芯片使用的是24位的地址,就是3个字节。在进行读写的时候每个字节都得分配一个地址。在指定地址时需要一次性指定3个字节即24位的地址,24位地址最大可分配的字节是: = 1677216位数,除以1024 = 16384kb,再除以1024 = 16MB,所以24位地址的最大寻址空间是16MB。W25Q40~W25Q128使用3字节24位的地址都是足够的。
但是W25Q256不一样,24位地址对于32MB来说是不够的,W25Q256分为3字节地址模式和4字节地址模式,在3字节地址模式下,只能读写前16MB的数据,后面16MB,3个字节的地址够不着,要想读写到所有存储单元,可以进入4字节地址模式。
硬件电路
4脚和8脚是电源引脚,供电电压是3.3V,不能直接接入5V电压。
1号脚CS 左边画了斜杠,代表的是低电平有效,或者CS上边画了横杠也是低电平有效,CS对应前边的就是SS片选引脚
6号脚CLK对应的就是SCK时钟线
5号脚DI对应的是MOSI主机输出从机输入,
2号脚DO对应的是MISO主机输入从机输出,
3号脚WP(Write Project)写保护,配合内部的寄存器配置,可以实现硬件的写保护,写保护低电平有效,WP接低电平,保护住不让写;WP接高电平,不保护,可以写。
7号脚HOLD,数据保持,低电平有效。如果在进行读写时突然产生中断,想用SPI通信线去操控其他器件,这时将CS置回高电平,时序终止。如果此时不想终止总线,又想操作其他器件,这时候可以HOLD引脚置低电平,将芯片HOLD住,芯片释放总线,但是芯片的时序不会终止,会记住当前的状态,操作完其他器件时可以回过来将HOLD置高电平,继续HOLD之前的时序,相当于SPI进了一次中断。在中断里SPI可以做其他的事。
DI、DO、WP和HOLD旁边都有括号,写了IO0、IO1、IO2、IO3,对应前边提及的双重SPI和四重SPI;如果是普通的SPI,括号中的不用看;如果是双重SPI那么DI和DO就变成IO0和IO1,也就是数据同时收和同时发的两个数据位;如果是四重SPI,那就再加上WP当做IO2,HOLD当做IO3,四个引脚都当做数据收发引脚,一个时钟4个数据位。
图中的硬件电路中,HOLD和WP都接到了VCC,都是低电平有效,接到VCC意为着这两个功能不用。C1接VCC和GND,显然是电源滤波,R1和LED也接到VCC和GND,起电源指示灯的作用,通电就亮,
W25Q64框图
W25Q64的容量是8MB,如果不划分按照一整块使用的话容量太大不利于管理,后续设计到Flash擦除或写入的时候,会有个基本单元,得以基本单元为单位进行,因此需要对这8MB的存储空间进行划分。
将这一整块存储空间划分成若干的块Block
在这一个矩形空间里,是所有的存储器,存储器以字节为单位,每一个字节都有唯一的地址。W25Q64的地址宽度是3个字节24位,左下角第一个字节,地址是00 00 00h,h代表16进制,之后的空间地址依次递增,直到最后一个字节地址是7f ff ff h。最后一个地址是7F开头而不是FF开头,因为24位的地址最大的寻址范围是16MB,这个芯片只有8MB,所以地址空间只用了一半,8MB的空间排到最后一个字节就是7F FF FF。
在这整个空间里,以64KB为一个基本单元,划分为若干的块block,从下往上依次是块0、块1、块2......一直到块127(8 * 1024 / 64 = 128,一共有128个,从0开始,到127)。
块0的起始地址是00 00 00,结束地址是00 ff ff;块31起始地址是1f 00 00,结束地址是1f ff ff,观察后续的地址可以发现规律,在每一块内地址变化范围是最低的两个字节,每个块的起始是xx 00 00,结束是xx ff ff 。
其中每一块再划分成若干的扇区Sector
对每一块进行更细的划分,这里的虚线指向了右边的各个块,意思是每个块里边都是这样子,每个块中起始地址是xx 00 00,结束是xx ff ff,对应图中的左下角和右上角的地址。在块里以4KB为一个单元进行切分,分成16(64 / 4 = 16)个扇区,扇区0~扇区15,每个扇区内的地址范围是xx x0 00到xx xf ff。
每一个扇区内部可以分成很多页page
页是对扇区进行划分,页的大小是256个字节,可分为16页(4 * 1024 / 256 = 16),每一行就是一页。在一页中地址的变化范围是xx xx 00到xx xx ff,一页内的地址变化仅限于地址的最低一个字节
SPI控制逻辑
SPI C.. & C.. logic是SPI的控制逻辑,芯片内部进行地址锁存、数据读写等操作,都是由控制逻辑自动完成。控制逻辑的左边是SPI的通信引脚,这些引脚和主控芯片相连,主控芯片通过SPI协议,把指令和数据发送给控制逻辑,控制逻辑就会自动取操作内部电路实现功能。
控制逻辑上边的状态寄存器Status Register,芯片是否处于忙状态、是否写使能、是否写保护,都可以在这个状态寄存器中体现。
状态寄存器上边是写控制逻辑Write Control Register,和外部的WP引脚相连,配合WP引脚实现硬件写保护。
控制逻辑右边高电压生成器High Voltage Generators,是配合Flash进行编程的,Flash掉电不丢失,要想产生掉电不丢失的状态,一般需要一个比较高的电压去刺激,需要一个高压源,这里内部集成了一个高电压发生器,就不需要外接高电压,比较方便。
右边是页地址锁存/计数器Page Address Latch Counter,字节地址锁存/计数器Byte Address Latch Counter,这两个地址锁存计数器用于指定地址。通过SPI发送过来三个字节的地址,由于一页是256个字节,所以一页内的字节地址取决于最低一个字节,高位的两个字节对应的是页地址,所以发送的三个字节的地址中前两个地址会进入页地址锁存计数器,最后一个字节会进入字节地址锁存计数器中,页地址通过写保护和行解码选择操作的是哪一页,字节地址通过列解码和256字节页缓存,进行指定字节的读写操作。由于地址锁存都是有计数器的,所以地址指针在读写之后会自动加1,利于实现从指定地址开始写多个字节的目的。
页地址缓存区
右边有个256字节的页缓存区Column Decode And 256-Byte Page Buffer,实际上是个256字节的RAM存储器,数据读写是通过这个RAM缓存区进行的,写入数据的时候会先放入缓存区里,在时序结束之后芯片再将缓存区的数据复制到对应的Flash里边,进行永久保存。
设置缓存区的原因是,SPI写入频率极高,Flash写入需要掉电不丢失,写入较慢。所以写入的数据先在页缓存区存着,缓存区是RAM速度较快,跟得上SPI总线的速度,但是问题在于缓存区只有256字节,写入的时序有限制条件,写入一个时序的连续写入量不能超过256个字节,写完后芯片再将数据从缓存区转移到Flash存储器里边。
数据转移到Flash需要一定的时间,在写入时序结束之后芯片会进入一段忙的状态,所以在页缓存区的左边有一条线通往状态寄存器,给状态寄存器的BUSY位置1,表示芯片当前正在忙,此时芯片不会响应新的读写时序。
读数据:
图中读数据也是从缓存区进行读取,但是区别在于,读取只需要看一下电路的状态即可,基本不花时间,限制非常的少
Flash操作注意事项
写入操作
·写入操作前先进行写使能,为了防止误操作,写使能通过SPI发送写使能的指令。
·Flash不和RAM一样具备直接覆盖改写的能力,比如在某一个字节的存储单元里存储了0xaa这个数据,对应的二进制位是1010 1010,如果在这个存储单元再次写入数据0x55二进制位是0101 0101,当新的0101 0101要覆盖原来的1010 1010时,会受到限制(每个数据位只能由1变0不能由0变1)。改写数据的过程中,最高位的1改写为0是ok的,写入后新的最高位就是0;在第二位原来是0现在想改写成1,违反规则,仍然是0;第三位原来的1改成0ok,第四位0无法变1仍然为0.以此类推最后转换成0000 0000,并不是我们需要储存的结果。
因此有了新的规则,在写入数据前将数据进行擦除,就可以写入正确的数据。数据位为1的数据拥有单向改成0的权利,一旦写成0之后不能改回1,除非先擦除,所有的位变1重新写入。如果不擦除就写入的话,基本上数据会出现错误。有时候读取Flash发现数据全是ff,有可能是写入数据的空白空间。
·擦除必须按最小擦除单元进行,如果想在00地址下进行写入数据,需要先把00地址擦除再写入到00地址。但是Flash的擦除有最小擦除单元限制,不能指定某一个字节去擦除,要擦需要一大片一起擦,在芯片中可以选择整个芯片擦除,也可以选择按快擦除或按扇区擦除,最小的擦除单元是一个扇区。一个扇区是4096个字节,每次擦除最少得4096个字节一起擦。如果在擦除数据时想不丢失数据,只能先把4096个字节读出来,再把4096个字节的扇区擦掉,改写完读出来的数据之后,再把改写完的全部写回去。
也有另一种方法可以优化这个流程,比如上电之后先把Flash的数据读出来放到RAM里,当有数据变动时再统一把数据备份到Flash里;或者把使用频繁的扇区放在RAM里,当使用频率降低时再把整个扇区备份到Flash里,或者数据量非常少,只想存几个字节的参数而已,那可以选择一个字节占一个扇区。
·一个写入时序最多只能写入一页的数据,也就是256个字节,因为前边提及的页缓存区只有256个字节。Flash写入速度太慢跟不上SPI,缓存区的是写入的数据放在RAM里暂存,等时序结束后芯片再慢慢把数据写入Flash里。所以每个时序只能写入256字节数据,如果非要写超过256字节的数据,则超过页尾的数据会回到页首覆盖写入。
页缓存区是和Flash的页对应的,必须得从页起始位置开始才能写入256字节,如果从页中间开始写,那写到页尾时地址会跳回页首,导致地址错乱。所以在进行多字节写入时,注意地址范围不能跨越界的边沿,否则会地址错乱。
·写入结束后芯片进入忙状态,写入操作是对缓存区进行的,在时序结束之后芯片还需要搬运一段时间,写入操作之后会有一段时间的忙状态,在这个状态下不进行读写操作,芯片不会响应。要想知道芯片忙状态是否结束,可以使用读取状态寄存器的指令,查看状态寄存器的BUSY位是否为1,BUSY位为0时芯片不忙再进行操作。
包括擦除指令也会使芯片进入忙状态,需要等忙状态结束后才能进行后续操作。
可以在每次写操作时序结束之后调用一下WaitBusy,分为事前等待和事后等待:
事后等待:可以选择在每次写入后等待Busy清零之后再退出,这样较为保险,函数结束之后芯片处于不忙的状态,
事前等待:写入时序后不进行等待,而是在每次操作之前进行等待,不忙的时候写入。
两者区别在于:事后等待最保险,事前等待效率会高,写完之后不等可以执行其他代码,可以利用执行代码的时间消耗等待的时间。事后等待只需要在写入操作之后调用,而事前等待在写入操作和读写操作之前都得调用,因为在忙的时候不能读取
读取操作
·读取时较为宽松,无需使能,对应写入的第一条
·无需额外操作,对应写入的234条
·没有页的限制,对应第五条,想读多少读多少,没有页的限制
·读取后不会进入忙状态,可以立刻执行下一条指令,但是不能再忙状态下进行读取,对应写入的第六条,忙状态时不会响应新的读写操作,在读取之前需要注意一下芯片的状态是否处于忙状态。Flash这种非易失性存储器,虽然有诸多不方便,但是可以使用软件来弥补,他的优点是其他存储器无法比,容量大价格低,速度虽然比RAM低,但是在非易失性存储器中非常快。
软件SPI读取W25Q64
接线图
在SS下降沿之后开始交换字节,SS下降沿和数据移出是同时发生的,SCK下降沿和数据移出也是同时的,但是不代表程序上要同时执行两条代码,实际上是存在先后顺序的,先SS下降沿或SCK下降沿,再数据移出,其中下降沿时触发数据移出的条件,先有下降沿才会有数据移出的动作。对于硬件SPI的话由于使用的是硬件的移位寄存器电路,所以两个动作几乎是同时发生的。对于软件SPI无法同时完成两个动作。软件SPI的流程是:先SS下降沿再移出数据,再SCK上升沿再移入数据,再SCK下降沿在移出数据。
SPI交换一个字节 使用模式0
使用掩码依次提取数据每一位
//SPI交换一个字节 使用模式0
uint8_t MySPI_SwapByte(uint8_t ByteSend) //使用掩码依次提取数据每一位 好处是不会改变传入参数本身
{uint8_t i,ByteReceive = 0x00;//ByteSend是传进来的参数 要通过交换一个字节的时序发送出去 //返回值是ByteReceive 是通过交换一个字节接收到的数据for(i = 0;i < 8;i++)//相当于最高位 次高位 依次移位读写{// 1 . 主机和从机的数据同时放到MOSI和MISO上 这里写主机即可MySPI_W_MOSI(ByteSend & 0x80 >> i);//发送的是ByteSend的最高位 要保证函数是非0即1的特征 否则&0x80之后要右移到最低位才可以// 2 . SCK上升沿 主机和从机同时移入数据 从机会自动读取无需操心 MySPI_W_SCK(1);//上升沿 自动读取从机放在MISO的数据位if (MySPI_R_MISO()==1) {ByteReceive |= 0x80 >> i;}//将最高位存在ByteReceive里MySPI_W_SCK(0);//产生下降沿}return ByteReceive;
}
使用移位数据本身进行操作
uint8_t MySPI_SwapByte(uint8_t ByteSend)//使用移位数据本身进行操作 好处是效率更高 契合前边的移位模型 坏处是ByteSend在移位过程改变了 执行完循环原始参数消失
{for(i = 0;i < 8;i++){MySPI_W_MOSI(ByteSend & 0x80);//第一步输出最高位ByteSend <<= 1;//这两句就是把byteSend的最高位移出到MOSI byteSend左移一位之后最低位会自动补0//最低位空出来 后边接收就不需要ByteReceive这个变量MySPI_W_SCK(1);if (MySPI_R_MISO()==1) {ByteSend |= 0x01;}//直接把收到的数据放在ByteSend的最低位MySPI_W_SCK(0);}//第二次循环 由于上一次最高位左移一次了 所以新的最高位就是原始数据的次高位 //数据继续左移 收到数据仍然放在最低位 依次进行8次 数据交换完成return ByteSend;
}
SPI交换一个字节 使用模式1
SPI模式1是先SCK上升沿再移出数据、SCK下降沿再移入数据
只需要对程序的相位进行修改即可切换模式1
//SPI交换一个字节 使用模式1
//SPI模式1是先SCK上升沿再移出数据、SCK下降沿再移入数据//只需要对程序的相位进行修改即可切换模式1
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{uint8_t i,ByteReceive = 0x00;for(i = 0;i < 8;i++){MySPI_W_SCK(1);MySPI_W_MOSI(ByteSend & 0x80 >> i);MySPI_W_SCK(0);if (MySPI_R_MISO()==1) {ByteReceive |= 0x80 >> i;}}return ByteReceive;
}
SPI交换一个字节 使用模式3
在 void MySPI_Init(void) 这个函数中 MySPI_W_SCK(1); 也需要修改
//SPI交换一个字节 使用模式3
//SPI3 和 SPI1模式的区别是时钟极性不一样 将SCK极性进行翻转即可
//将所有出现SCK的地方1变0 0变1
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{uint8_t i,ByteReceive = 0x00;for(i = 0;i < 8;i++){MySPI_W_SCK(0);MySPI_W_MOSI(ByteSend & 0x80 >> i);MySPI_W_SCK(1);if (MySPI_R_MISO()==1) {ByteReceive |= 0x80 >> i;}}return ByteReceive;
}
SPI交换一个字节 使用模式2
SPI2 同理 就是在SPI1 的基础上将SCK0变1 1变0即可
//SPI交换一个字节 使用模式2
//SPI2 同理 就是在SPI1 的基础上将SCK0变1 1变0即可
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{uint8_t i,ByteReceive = 0x00;for(i = 0;i < 8;i++){MySPI_W_MOSI(ByteSend & 0x80 >> i);MySPI_W_SCK(0);if (MySPI_R_MISO()==1) {ByteReceive |= 0x80 >> i;}MySPI_W_SCK(1);}return ByteReceive;
}
指令集表格
比如先实现获取ID号的时序,先把ID号读出观察是否正确,验证底层的SPI写的是否有问题。由表格可见读取ID号的时序是先交换发送指令9F,随后连续交换接收三个字节停止。在下方note的第一行写了圆括号括起来的是需要交换接收的数据,第一个字节是厂商ID,后两个字节是设备ID,其中设备ID的高八位表示存储器类型,低八位表示容量。
代码-读取W25Q64芯片的地址
main.c部分
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "W25Q64.h"uint8_t MID;
uint16_t DID;int main()
{OLED_Init(); W25Q64_Init();W25Q64_ReadID(&MID,&DID); //将两个变量的地址传递进去 函数执行完拿到IDOLED_ShowHexNum(1,1,MID,2);OLED_ShowHexNum(2,1,DID,4);while(1){}
}
W25Q64.c部分
#include "stm32f10x.h"
#include "MySPI.h"
#include "W25Q64_Ins.h"void W25Q64_Init(void)
{MySPI_Init();
}void W25Q64_ReadID(uint8_t *MID,uint16_t *DID) //计划有两个返回值 使用指针实现多返回值
{ //MID厂商ID DID设备IDMySPI_Start(); //SS引脚置低 开始传输MySPI_SwapByte(W25Q64_JEDEC_ID); //参数是交换发送的 接收的数据没有意义 不读取返回值//发出读ID号的指令 下一条代码的目的是接收 所以发出的东西无意义 一般使用FF进行置换*MID = MySPI_SwapByte(W25Q64_DUMMY_BYTE); //用指针接收厂商的ID*DID = MySPI_SwapByte(W25Q64_DUMMY_BYTE); //用指针接收设备ID的高八位*DID <<= 8;//要想将数据分别放在高八位和低八位 需要把第一次读到数据运到DID高八位*DID |= MySPI_SwapByte(W25Q64_DUMMY_BYTE); //用指针接收设备ID的低八位 需要|=,否则会被置零MySPI_Stop();
}//WriteEnable写使能 发送06即可
void W25Q64_WriteEnable(void)
{MySPI_Start();MySPI_SwapByte(W25Q64_WRITE_ENABLE);MySPI_Stop();
}//读状态寄存器1 指令码05 发完指令码即可接收状态寄存器 判断是否在忙状态
//最低位busy 1表示芯片在忙 0表示不忙
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)//接收数据 返回值是状态寄存器1 // & 与上0x01 用掩码取出最低位 等于0x01就是busy为1 一直读到busy为0 跳出循环{TimeOut --;if (TimeOut == 0){break;}}MySPI_Stop();
}//页编程 格式是 先发送指令码02 再发送3个字节24位地址 最后发数据(最大是DataByte256 超过覆盖DataByte1)
void W25Q64_PageProgram(uint32_t Address,uint8_t *DataArray,uint16_t Count)
{ //写入数据的数量范围是0-256 count要定义为16位的 8位只能存255位W25Q64_WriteEnable();MySPI_Start();MySPI_SwapByte(W25Q64_PAGE_PROGRAM);MySPI_SwapByte(Address >> 16); //如果地址0x123456 右移16位就是0x12MySPI_SwapByte(Address >> 8 ); //右移8为就是0x1234 但是交换字节只能接收8位数据 所以高位舍弃 实际发送0x34MySPI_SwapByte(Address); //如果地址是0x123456 舍弃高位 实际发送0x56uint16_t i;//根据指令规定 地址发完 就可以依次发送写入的数据for(i = 0;i < Count ;i ++){MySPI_SwapByte(DataArray[i]); }MySPI_Stop();W25Q64_WaitBusy();
}//实现擦除的功能 扇区擦除 需要先发送指令20 再发送3个字节地址 指定地址所在的扇区将会被擦除
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();
}//读取数据 交换发送指令03 再发送3个字节地址 随后转入接收
//在发送完3个字节之前 时序处于高阻态 发送之后DO开启输出 主机可以接受有用数据DataOut1
//接收时DI的波形是xxx表示数据无所谓 读取没有限制 可以跨页一直连续读
void W25Q64_ReadData(uint32_t Address,uint8_t *DataArray,uint32_t Count)
{ //读取的数据没有页的限制 读取的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.c部分代码
#include "stm32f10x.h"void MySPI_W_SS(uint8_t BitValue) //写SS的引脚
{GPIO_WriteBit(GPIOA,GPIO_Pin_4,(BitAction)BitValue);//BitAction表示非0即1
}void MySPI_W_SCK(uint8_t BitValue) //写SCK的引脚
{GPIO_WriteBit(GPIOA,GPIO_Pin_5,(BitAction)BitValue);//BitAction表示非0即1
}void MySPI_W_MOSI(uint8_t BitValue) //写MOSI的引脚
{GPIO_WriteBit(GPIOA,GPIO_Pin_7,(BitAction)BitValue);//BitAction表示非0即1
}uint8_t MySPI_R_MISO(void) //写MISO的引脚
{return GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_6);
}void MySPI_Init(void)
{//对于主机来说 CS MOSI SCK都是输出引脚 配置为推挽输出 剩下MISO是输入引脚 配置为上拉输入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);//默认不选中从机MySPI_W_SCK(0);//计划使用SPI0 所以默认低电平//MOSI没有明确规定 不用置电平 MISO是输入 不需要输出电平
}//SPI的起始时序
void MySPI_Start(void)
{//根据PPT中的起始条件:将SS从高电平切换成低电平MySPI_W_SS(0);
}
//SPI的终止时序
void MySPI_Stop(void)
{//根据PPT中的终止条件:将SS从低电平切换成高电平MySPI_W_SS(1);
}//SPI交换一个字节 使用模式0
uint8_t MySPI_SwapByte(uint8_t ByteSend) //使用掩码依次提取数据每一位 好处是不会改变传入参数本身
{uint8_t i,ByteReceive = 0x00;//ByteSend是传进来的参数 要通过交换一个字节的时序发送出去 //返回值是ByteReceive 是通过交换一个字节接收到的数据for(i = 0;i < 8;i++)//相当于最高位 次高位 依次移位读写{// 1 . 主机和从机的数据同时放到MOSI和MISO上 这里写主机即可MySPI_W_MOSI(ByteSend & 0x80 >> i);//发送的是ByteSend的最高位 要保证函数是非0即1的特征 否则&0x80之后要右移到最低位才可以// 2 . SCK上升沿 主机和从机同时移入数据 从机会自动读取无需操心 MySPI_W_SCK(1);//上升沿 自动读取从机放在MISO的数据位if (MySPI_R_MISO()==1) {ByteReceive |= 0x80 >> i;}//将最高位存在ByteReceive里MySPI_W_SCK(0);//产生下降沿}return ByteReceive;
}
W25Q64_Ins.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
代码-W25Q64读写操作
main.c部分代码
#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] ;int main()
{OLED_Init(); W25Q64_Init();OLED_ShowString(1,1,"MID: DID:");OLED_ShowString(2,1,"W:");OLED_ShowString(3,1,"R:");W25Q64_ReadID(&MID,&DID); //将两个变量的地址传递进去 函数执行完拿到IDOLED_ShowHexNum(1,5,MID,2);OLED_ShowHexNum(1,12,DID,4);//先擦除扇区W25Q64_SectorErase(0x000000);//表示0地址 每个扇区的地址都是xxx000到xxxfff 只要末尾3个十六进制数为0则定为扇区的起始地址 后三位是扇区内的地址 无论怎么变都是在同一个扇区中的 随便写都是擦除同一个扇区 但是最好指定扇区起始地址进行擦除 意义更加明确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){}
}