目录
一、SPI通信
1.SPI通信简介
2.硬件电路
3.移位示意图
4.SPI基本时序图
(1)起始和终止
(2)交换一个字节
模式0:
模式1:编辑
模式2:编辑
模式3:编辑
5.SPI时序
(1)发送指令
(2)指定地址写
(3)指定地址读编辑
二、SPI外设
1.SPI外设简介
2.SPI框图
3.SPI基本结构
4.主模式传输
(1)连续传输
(2)非连续传输
三、W25Q64
1.简介
2.硬件电路
3.框图
4.Flash操作注意事项
四、软件SPI读写W25Q64
五、硬件SPI读写W25Q64
一、SPI通信
1.SPI通信简介
2.硬件电路
3.移位示意图
- 所有SPI设备的SCK、MOSI、MISO分别连在一起
- 主机另外引出多条SS控制线,分别接到各从机的SS引脚
- 输出引脚配置为推挽输出,输入引脚配置为浮空或上拉输入
- 首先SCL时钟线,时钟线完全由主机掌控,所以对于主机来说,时钟线为输出,对于所有从机来说,时钟线都为输入,这样主机的同步时钟就能送到各个从机了。
- MOSI主机输出从机输入,这边左边是主机,所以就对应MO主机输出,下面三个都是从机,所以就对应SI从机输入,数据传输方向是主机通过MOSI输出,所有从机通过MOSI输入。
- MISO主机输入从机输出,左边是主机对应MI,下面三个是从机对应SO,数据传输方向是三个从机通过MISO输出,主机通过MISO输入。
- 从机选择,为了确定通信的目标,主机就要另外引出多条SS控制线,分别接到各从机的SS引脚下面。下面这里有三个从机,需要主机另外引出三根SS选择线,主机的SS线都是输出,从机的SS线都是输入,SS线是低电平有效,主机想指定谁,就把对应的SS输出线置低电平就行了,比如主机初始化之后,所有的SS都输出高电平,这样就是谁也不指定,当主机需要和比如从机1进行通信了,主机就把SS1线输出低电平,不需要像I2C一样进行寻址。
- 输出引脚配置为推挽输出,输入引脚配置为浮空或上拉输入,对输出我们配置推挽输出,高低电平均有很强的驱动能力,这将使得SPI引脚信号的下降沿非常迅速,上升沿也非常迅速,不像I2C那样下降沿非常迅速,但是上升沿就比较缓慢了,那得益于推挽输出的驱动能力,SPI信号变化的快,那自然它就能达到更高的传输速度,一般SPI信号都能轻松达到兆赫兹的速度级别,I2C并不是不想使用更快的推挽输出,而是I2C要实现半双工,经常要切换输入输出,而且I2C又要实现多主机的时钟同步和总线仲裁,这些功能都不允许I2C使用推挽输出,要不然你不小心就电源短路了,所以I2C选择了更多的功能,自然就要放弃更强的性能了,对SPI来说,首先SPI不支持多主机,然后SPI就是全双工,SPI的输出引脚始终是输出,输入引脚始终是输入,基本不会出现冲突,所以SPI可以大胆的使用推挽输出,不过SPI还是有一个冲突点的,就是图上的MISO引脚,主机一个是输入但是三个从机全都是输出,如果三个引脚都始终是推挽输出,势必会导致冲突,所以在SPI协议里有一条规定,就是当从机的SS引脚为高电平,也就是从机未被选中时,他的MISO引脚必须切换为高阻态,高阻态就相当于引脚断开,不输出任何电平,这样就可以防止一条线有多个输出,而导致的电平冲突的问题了,在SS为低电平时,MISO才允许变为推挽输出,这是SPI对这个可能的冲突做出的规定,当然这个切换过程都是在从机里,我们一般都写主机的程序,所以我们主机的程序中并不需要关注这个问题。
4.SPI基本时序图
(1)起始和终止
(2)交换一个字节
模式0:
模式0和模式1的区别,就是模式0的CPHA等于0,模式1的CPHA等于1。
在时序上的区别对比一下,模式0的数据移出移入的时机会提前半个时钟,也就是相位提前了,我们看一下模式0的CPHA等于0,表示SCK第一个边沿移入数据,第二个边沿移出数据,模式0在SCK第一个边缘就要移入数据,但数据总得先移出才能移入对吧,所以在模式0的配置下,SCK第一个边沿之前就要提前开始移出数据了,或者把它称作是在第零个边沿移出在第一个边缘移入,看一下时序,首先SS下降沿开始通信,现在SCK还没有变化,但是SCK一旦开始变化,就要移入数据了,所以此时趁SCK还没有变化,SS下降沿时就要立刻触发移位输出,所以这里MOSI和MISO的输出,是对齐到SS的下降沿的,或者说这里把SS的下降沿也当作时钟的一部分了,那SS下降沿触发的输出,SCK上升沿就可以采样输入数据了,这样b7 就传输完毕,之后SCK下降沿移出b6 ,SCK上升沿移入b6 ,然后继续,下降沿移出数据,上升沿移入数据,最终在第八个上升沿时,b0位移入完成整个字节交换完成,之后SCK还有一个下降沿,如果主机只需要交换一个字节就结束,那在这个下降沿时MOSI可以置回默认电平或者不去管它,MISO也会变化一次,这一位实际上是下一个字节的b7,因为这个相位提前了,所以下一个字节的b7会露个头,如果不需要的话,SS上升沿之后从机MISO置回高阻态,这是交换一个字节就结束。
模式1:
CPOL等于零,表示空闲状态时SCK为低电平,可以看到在SS未被选中时,SCK默认是低电平的,然后CPHA等于1,表示SCK第一个边沿移出数据,第二个边缘移入数据,但这句话也有不同的描述方式,有的地方写的是CPHA等于1表示SCK的第二个边沿进行数据采样,或者是SCK的偶数边缘进行数据采样,这些不同的描述意思都是一样,我这里为了照应刚才的移位模型,我就写的是SCK第一个边缘移出数据,第二个边沿移入数据,来看一下时序图,第一个SS从机选择,在通信开始前,SS为高电平,在通信过程中SS始终保持低电平,通信结束SS恢复高电平,然后最下面一个MISO,这是主机输入从机输出,刚才说了这里因为有多个从机输出连在了一起,如果同时开启输出会造成冲突,所以我们的解决方法是在SS未被选中的状态,从机的MISO引脚必须关断输出,即配置输出为高阻状态,那在这里SS高电平时MISO用一条中间的线表示高阻态,SS下降沿之后,从机的MISO被允许开启输出,SS上升沿之后,从机的MISO必须置回高阻态,这是这一块的设计啊,然后我们看一下移位传输的操作,因为CPHA等于1,SCK第一个边沿移出数据,所以这里可以看出来,SCK第一个边缘就是上升沿,主机和从机同时移出数据,主机通过MOSI移出最高位,此时MOSI的电平就表示了主机要发送数据的b7 ,重新通过MISO移出最高位,此时MISO表示从机要发送数据的b7 ,然后时钟运行产生下降沿,此时主机和从机同时移入数据,也就是进行数据采样,这里主机移出的b7进入从机移位寄存器的最低位,从机移出的b7进入主机移位寄存器的最低位,这样一个时钟脉冲产生完毕,一个数据位传输完毕,接下来就是同样的过程,上升沿主机和从机同时输出当前移位寄存器的最高位,第二次的最高位就是原始数据的b6,然后下降沿主机和从机移入数据,b6 传输完成之后时钟继续运行,数据依次移出移入移出移入,,最后一个下降沿数据b0传输完成,至此主机和从机就完成了一个字节的数据交换,如果主机只想交换一个字节,那这时候就可以置SS为高电平结束通信了,在SS的上升沿MOSI还可以再变化一次,将MOSI制造一个默认的高电平或低电平,当然也可以不去管它,因为spi没有硬性规定MOSI的默认电平,然后MISO从机必须得置回高组态,此时如果主机的MISO为上拉输入的话,那MISO引脚的电平就是默认的高电平,如果主机MISO为浮空输入,那MISO引脚的电平不确定,这是交换一个字节就结束了流程,那如果主机还想继续交换,在此时主机就不必把SS置回高电平(继续是低电平),直接重复一下从这里到这里交换一个字节的时序,这样就可以交换多个字节了,就是SPI传输数据的流程。
模式2:
模式3:
这个CPHA表示的是时钟相位,决定是第一个时钟采样移入,还是第二个时钟采样移入,并不是规定上升沿采样还是下降沿采样的,当然在CPOL确定的情况下,CPHA确实会改变采样时刻的上升沿和下降沿,比如模式0的时候是SCK上升沿采样移入,模式1的时候是SCK下降沿采样移入,CPHA决定的是第几个边沿采样,并不能单独决定是上升沿还是下降沿,在这四种模式里,模式零和模式三都是SCK上升沿采样,模式一和模式二都是SCK下降沿采样。
5.SPI时序
(1)发送指令
在W25Q64里,这个0x06 代表的是写使能,我们看一下这个模型,在这里我们使用的是spi模式0,在空闲状态是ss为高电平,sck为低电平,mosi和miso的默认电平没有严格规定,然后ss产生下降沿时序开始,在这个下降沿时刻,mosi和miso就要开始变换数据了,mosi由于指令码最高位仍然是0(0000 0110),所以这里保持低电平不变,miso从机现在没有数据发给主机,引脚电平没有变化,实际上W25Q64不需要回弹数据时,手册里规定的是miso仍然是高阻态,从机并没有开启输出,不过这也没问题,反正这个数据我们也不要看,那这里因为STM32的miso是上拉输入,所以这里miso呈现高电平,之后sck第一个上升沿进行数据采样,我这里画了一条绿线,从机采样输入得到零,主机采样输入得到一,之后继续第二个时钟,主机数据仍然是零,所以波形仍然没有变化,然后这样一位一位的发送接收发送接收,到这一位数据才开始变化,主机要发送数据一,下降沿数据移出,主机将一移出到mosi,mosi变为高电平,这里因为是软件模拟的时序,所以mosi的数据变化有些延迟,没有紧贴sck的下降沿,不过这也没关系,时钟是主机控制的,我们只要在下一个sck上升沿之前完成变化就行了,然后sck上升沿数据采样输入,在最后一位呢下降沿数据变化mosi变为零,上升沿数据采样,从机接收数据0,sck低电平是变化的时期,高电平是读取的时期,这一块是不是和I2C差不多,那时序sck最后一个上升沿结束,一个字节就交换完毕了,因为写使能是单独的指令,不需要跟随数据,spi只需要交换一个字节就完事了,所以最后在sck下降沿之后,ss置回高电平结束通信,那这个交换我们统计一下,mosi和miso的电平,总结一下就是,主机用0x06换来了从机的0xff,但实际上从机并没有输出,这个0xff是默认的高电平,不过这个0xff没有意义,我们不用管,那整个时序的功能就是发送指令,指令码是0x06 ,从机一比对事先定义好的指令集,发现0x06是写使能的指令,那从机就会控制硬件进行写使能,这样一个指令从发送到执行就完成了,就是发送单字节指令的时序 。
(2)指定地址写
W25Q64芯片有8M字节的存储空间,一个字节的八位地址肯定不够,所以这里地址是24位的分三个字节传输,我们看一下时序,首先ss下降沿开始时序,mosi空闲时是高电平,所以在下降沿之后,sck第一个时钟之前可以看到mosi变换数据由高电平变为低电平,然后sck上升沿数据采样输入,后面还是一样下降沿变换数据上升沿采样数据,八个时钟之后一个字节交换完成,我们用0x02换来了0xff,其中发送的0x02是一条指令,代表这是一个写数据的时序,接收到0xff不需要看,那既然是写数据的时序,后面必然还要跟着写的地址和数据,所以在最后一个下降沿时刻,因为我们后续还需要继续交换字节,所以在这个下降沿,我们要把下一个字节的最高位放到mosi上,当然下一个字节的最高位仍然是零,所以这里数据没有变化,最后还是同样的流程交换一个字节,第二个字节我们用0x12 换来了0xff,根据W25Q64芯片的规定,写指令之后的字节定义为地址高位,所以这个0x12 就表示发送地址的23~16位,继续看一下交换一个字节,发送的是0x34 这个就表示发送地址的15~8位,最后还是交换一个字节发送的是0x56 ,这个表示发送地址的7~0位,通过三个字节的交换,24位的地址就发送完毕了,从机收到的24位地址是0x123456 ,那三位地址结束后,就要发送写入指定地址的内容了,我们继续调用交换一个字节,发送数据这里的波形是0x55 ,这个表示我要在0x123456 地址下,写入0x55 这个数据,最后如果只想写出一个数据的话,就可以ss置高电平结束通信了,当然这里也可以继续发送数据,spi里也会有和I2C一样的地址指针,每读写一个字节地址指针自动加一,如果发送一个字节之后不终止继续发送的字节就会依次写入到后续的存储空间里,这样就可以实现从指定地址开始写入多个字节了,这就是spi写入的时序,由于spi没有应答机制,所以交换一个字节后,就立刻交换下一个字节就行。
(3)指定地址读
功能是向ss指定的设备先发送读指令,这里芯片定义0x03为读指令,随后在指定地址下读取从机数据,我们看一下时序,起始之后第一个字节主机发送指令0x03 ,表示我要读取数据了,最后还是一样,主机在依次交换三个字节,分别是0x12 0x34 0x56 ,组合到一起就是0x123456代表24位地址,最后这个地方就是关键点,因为我们是读取数据,指定地址之后,显然我们就要开始接收数据,所以这里三个字节的地址交换完之后,我们要把从机的数据搞过来,怎么搞过来呢,我们还是交换一个数据来个抛砖引玉,我们随便给从机一个数据,一般给ff就行了,从机就会把0x123456地址下的数据通过miso发给主机,可以看到这样的波形就表示指定地址下的数据是0x55 ,这样主机就实现了指定地址读一个字节的目的,然后如果我们继续抛砖引玉,那么从机内部的地址指针自动加一,从机就会继续把指定地址下一个位置的数据发过来,这样依次进行,就可以实现指定地址接收多个字节的目的了,最后数据传输完毕,ss置回高电平,时序结束,当然时序这里也会有些细节,比如由于miso是硬件控制的波形,所以它的数据变化都可以紧贴时钟的下降沿,另外我们可以看到miso数据的最高位实际上是在上一个字节,最后一个下降沿提前发生的,因为这是spi模式零,所以数据变化都要提前半个周期。
二、SPI外设
1.SPI外设简介
时钟频率就是sck波形的频率,一个sck时钟交换一个bit,所以时钟频率一般体现的是传输速度,单位是Hz或者bit/s,那这里的时钟频率是fPCLK除以一个分频系数,分频系数可以配置为2、4、8、16、32、64、128、256,所以可以看出来,spi的时钟其实就是由pclk分频得来的,pclk就是外设时钟,APB2的pclk就是72MHz,APB1的pclk是36MHz,比如我们的spi1是APB2的外设,pclk等于72MHz,那它的spi时钟频率最大就是只进行二分频=36MHz,像我们之前I2C的频率最大就只有400KHz,所以这里spi的最大频率比I2C快了90倍,然后这里频率有些注意事项,一是这个频率数值并不是任意指定的,它只能是pclk执行分频后的数值就只有这八个选项,最低频率是pclk的256分频,二是spi1和spi2 挂载的总线是不一样的,spi1挂载在APB2,pclk是72MHz,spi1挂载在APB1,pclk是36MHz,所以同样的配置,spi1的时钟频率要比spi2的大一倍。
2.SPI框图
3.SPI基本结构
4.主模式传输
(1)连续传输
- 第一行是sck时钟线,这里cpol等于1,cpha等于1,示例使用的是spi模式三,所以sck默认是高电平,然后在第一个下降沿mosi和miso移出数据,之后上升沿引入数据,依次这样来进行,那下面第二行是mosi和miso输出的波形,跟随sck时钟变化,数据位依次出现,这里从前到后依次出现的是b0b1一直到b7 ,所以这里示例演示的是低位先行的模式,实际spi高位先行用的多一些,最后第三行是TXE发送寄存器空标志位,下面发送缓冲器括号写入SPI_DR,实际上就是这里的TDR然后BSY(busy)是由硬件自动设置和清除的,当有数据传输时,busy置1那上面演示的就是输出的流程和现象,然后下面是输入的流程和现象,第一个是miso/mosi的输入数据,之后是RXNE接收数据寄存器非空标志位最后是接收,缓冲器读出SPI_DR,显然这里就是RDR。
- 首先ss置低电平开始时序,这个没画但是必须得有的,在刚开始时TXE为1,表示TDR空可以写入数据开始传输,然后下面指示的第一步就是软件写入0xf1至SPI_DR,0xf1就是要发送的第一个数据,之后可以看到写入之后TDR变为0xf1 ,同时txe变为0,表示tdr已经有数据了,那此时dr是等候区,移位寄存器才是真正的发送区,移位寄存器刚开始肯定没有数据,所以在等候区TDR里的f1 ,就会立刻转入移位寄存器开始发送转入瞬间置txe标志位为1,表示发送寄存器空,然后移位寄存器有数据了,波形就自动开始生成,当然我感觉这里画的数据波形时机可能有点早,应该是在这个时刻b0的波形才开始产生,在这之前数据还没有转入移位进器,所以感觉b0出现的可能过早了,不过这个也不影响我们理解,大家知道这意思就行好了,这样数据转入移位寄存器之后,数据F1的波形就开始产生了,在移位产生f1波形的同时,等候区tdr是空的,为了移位完成时,下一个数据能不间断的跟随,这里我们就要提早把下一个数据写入到TDR里等着了,所以下面只是第二步的操作,是写入F1之后,软件等待TXE等于1,在这个位置,一旦tdr空了,我们就写入F2至SPI_DR,写入之后可以看到tdr的内容就变成F2了,也就是把下一个数据放到tdr里,后者之后的发送流程也是同理,最后在这里如果我们只想发送三个数据,F3转入移位寄存器之后,TXE等于1,我们就不需要继续写入了,txe之后一直是1,注意在最后一个TXE等于1之后,还需要继续等待一段时间,f3的波形才能完整发送完,等波形全部完整发送之后,busy的标志由硬件清除,这才表示波形发送完成了,那这些就是发送的流程,然后继续看一下下面接收的流程,SPI是全双工,发送的同时还有接收,所以可以看到在第一个字节发送完成后,第一个字节的接收也完成了,接收到的数据1是A1 ,这时移位寄存器的数据整体转入RDR,RDR随后存储的就是A1 ,转入的同时按RXNE标志位也置1,表示收到数据了,我们的操作是下面这里写的,软件等待RXNE等于1,=1表示收到数据了,然后从SPI_DR也是RDR读出数据A1 ,这是第一个接收到的数据,接收之后软件清除RXNE标志位,然后当下一个数据2收到之后,RXNE重新置1,我们监测到RXNE等于1时就继续读出RDR,这是第二个数据A2 ,最后在最后一个字节时序完全产生之后,数据三才能收到,所以数据3,直到这里才能读出来,然后注意,一个字节波形收到后,移位寄存器的数据自动转入RDR,会覆盖原有的数据,所以我们读出rdr要及时,比如A1这个数据收到之后,最迟你也要在这里把它读走,否则下一个数据A2覆盖A1,就不能实现连续数据流的接收了。
(2)非连续传输
配置还是spi模式三,sck默认高电平,我们想发送数据时如果检测到TXE等于1了,TDR为空,就软件写入0xF1至SPI_DR,这时TDR的值变为F1,TXE变为0,目前移位寄存器也是空,所以这个F1会立刻转入移位寄存器,开始发送,波形产生并且,TXE置回1,表示你可以把下一个数据放在tdr里侯着了,但是现在区别就来了,在连续传输这里一旦,TXE等于1了,我们就会把下个数据写到tdr里侯着这样是为了连续传输数据衔接更紧密,但是刚才说了,这样的话,流程就比较混乱,程序写起来比较复杂,所以在非连续传输这里,TXE等于1了,我们不着急把下一个数据写进去,而是一直等待,等第一个字节时序结束,在这个位置时序结束了,意味着接收第一个字节也完成了,这时接收的RXNE会置一,我们等待RXNE置1后,先把第一个接收到的数据读出来,之后再写入下一个字节数据,也就是这里的软件等待TXE等于1,但是较晚写入0xf2SPI_DR,较晚写入TDR后,数据二开始发送,我们还是不着急写数据三,等到了这里,先把接收的数据二收着,再继续写入数据3,数据3时序结束后,最后再接收数据三置换回来的数据,你看按照这个流程的话,我们的整个步骤就是第一步等待TXE为一,第二步写入发送的数据至TDR,第三步等待RXNE为一,第四步读取RDR接收的数据,之后交换第二个字节,重复这四步,那这样我们就可以把这四部分装到一个函数,调用一次交换一个字节,这样程序逻辑是不是就非常简单了,和之前软件spi的流程基本上是一样的,我们只需要稍作修改,就可以把软件spi改成硬件spi,那非连续传输的缺点就是在这个位置没有及时把下一个数据写入TDR侯着,所以等到第一个字节时序完成后,第二个字节还没有送过来,那这个数据传输就会在这里等着,所以这里时钟和数据的时序,在字节与字节之间会产生间隙,拖慢了整体数据传输的速度这个间隙在sck频率低的时候影响不大,但是在sck频率非常高时隙拖后腿的现象就比较严重了。
三、W25Q64
1.简介
时钟频率,我们这个芯片使用的是spi通信,其中spi的sck线就是时钟线,这个时钟线的最大频率是80MHz,这个频率相比较STM32是非常快的,所以我们在写程序的时候翻转引脚就不用再加延时了,即使不延时这个GPIO的翻转频率也不可能达到80MHz,所以可以放心使用,然后后面这还有两个频率,分别是160MHz这个是双重spi模式等效的频率,320MHz这个是四重spi模式等效的频率,这个双重spi和四重spi大家了解一下即可,我们本课程不会用到,那他们是什么意思呢,就是我们之前说的mosi用于发送,miso用于接收,是全双工通信,在只发或只收时有资源浪费,但是这个w25q芯片的厂商不忍心浪费,所以就对spi做出了一些改进,就是我在发的时候,可以同时用mosi和miso发送,在收的时候也可以同时用mosi和miso接收,mosi和miso同时兼具发送和接收的功能,一个sck时钟我同时发送或接收两位数据,就是双重spi模式,那你一个时钟收发两位相比较一位一位的普通spi数据传输率就是二倍了,所以这里写的是在双重spi模式下,等效的时钟频率就是80MHz的二倍就是16MHz,但实际上这个频率最大还是80MHz,只是我一个时钟发两位而已,然后四重spi模式,很显然就是一个时钟发送或接收四位,等效的频率就是80x4=320MHz,在我们这个芯片里啊,除了spi通信引脚,还有两个引脚,一个是wp写保护,另一个是hold,这两个引脚如果不需要的话,也可以拉过来充当数据传输引脚,加上mosi和miso就可以四个数据位同时收发了,就是四重spi,其实这就有点并行传输的意思了,串行是根据时钟一位一位的发,并行是一个时钟八位同时发送,所以这个四重spi模式,其实就是四位并行的模式,这个大概了解一下就行。
2.硬件电路
- 首先看一下引脚定义,VCC、GND是电源供电,引脚供电电压是2.7~3.6V,是一个典型的3.3V供电设备不能直接接入5V电压,然后1号脚cs,这个cs左边画了个斜杠代表是低电平有效,或者这边cs上面画了个横线也是低电平有效,那这里cs对应之前我们讲spi的名称就是SS,意思是spi的片选引脚,6号脚clk对应就是sck,是spi的时钟线,然后5号引脚di对应mosi,是spi主机输出从机输入,2号do对应miso,是spi主机输入从机输出,这四个引脚就是spi通信的四个引脚。
- 3号引脚wp,他的意思是写保护配合内部的寄存器器配置,可以实现保硬件的写保护,写保护低电平有效,wp接低电平保护住不让写,wp接高电平不保护可以,最后7号hold意思就是数据保持哈,低电平有效,这个用的不多了解一下,就是如果你在进行正常读写时突然产生中断,然后想用spi通信线去操控其他器件,这时如果把cs置回高电平,那时序就终止,但如果你又不想终止总线,又想操作其他器件,这就可以hold引脚置低电平,这样芯片就hold住了,芯片释放总线,但是芯片时序也不会终止,它会记住当前的状态,当你操作完其他器件时,可以回过来哈,hold置回高电平,然后继续hold之前的时序,相当于spi总线进来一次中断,并且在中断里还可以用spi干别的事情,这就是hold的功能。
- di、do、wp、和HOLD,旁边都有括号,写了lO0、lO1、lO2、lO3 ,这个就对应我们刚才这里说的双重spi和四重spi,如果是普通的spi模式,那括号里的都不用看,如果是双重spi,那di和do就变成lO0和lO1,也就是数据同时收和同时发的两个数据位,如果是四重spi,那就再加上wp当做lO2 ,HOLD当做lO3 ,这四个引脚都作为数据收发引脚,一个时钟四个数据位。
3.框图
- 首先右边这一整个矩形空间里是所有的存储器,存储器以字节为单位,每个字节都有唯一的地址,这样说了w25q64的地址宽度是24位3个字节,所以可以看到左下角第一个字节,它的地址是00 00 00h,h代表16进制,之后的空间地址依次自增,直到最后一个字节,地址是7F FF FF h,那最后一个字节为啥是7f开头,不是f f开头呢,因为24位地址最大寻址范围是16MB,我们这个芯片只有8MB,所以地址空间我们只用了一半,8MB排到最后一个字节,就是7F FF FF h,那这是整个地址空间,从000000~7F FF FF,然后在这整个空间里,我们以64kb为一个基本单元,把它划分为若干的块block,从前往后依次是块0块1块2等等,一直分到最后一块,那整块蛋糕是8MB,以64kb为一块进行划分,最后分得的快数就是8MB除以64kB,这里可以分得128块(8*1025/64=128),那块序号就是块0一直到最后一个是块127,然后观察一下块内地址值的变化规律,比如块0的起始地址是000000,结束地址是00f f f f,之后块31起始是1f0000 ,结束是1f f f f f,之后的都观察一下,可以发现在每一块内,它的地址变化范围就是最低的两个字节,每个块的起始地址是XX0000,结束是XXf f f f,这是块内地址的变化规律,到这里这一块大蛋糕我们就分好块了,64kb为一块总共128块,之后看一下左边这个示意图,我们还要再对每一块进行更细的划分,分为多个扇区sector,这里的虚线指向了右边的各个块,也就是告诉你每一块里面都是这个样子的,那在每个块里,它的起始地址是XX0000,结束地址是XXf f f f,在一块里我们再以4kb为一个单元进行切分,一块是64kb,我4kb一切总共16份,所以在每一块里都可以分为扇区0一直到扇区15,观察一下地址规律,可以发现每个扇区内的地址范围是XXX000到XXXf f f,地址划分啊到扇区就结束了,但是当我们在写入数据时啊,还会有个更细的划分,就是页Page,页是对整个存储空间划分的,当然你也可以把它看作在扇区里再进行划分都一样,那页的大小是256个字节,一个扇区是4kb,所以一个扇区里可以分为16页(4*1025/256=16),然后页的地址规律呢我们也看一下,在这里每一行就是一页,左边这里指了个箭头,写的是页地址的开始,右边这里也指了个箭头,写的是页地址的结束,在一页中,地址变化范围是XXXX00到XXXXFF,一页内的地址变化,仅限于地址的最低一个字节,这就是页的划分,那这个存储器的地址划分啊我就讲完了,我们需要记住的是一整个存储空间,首先划分为若干块,对于每一块又划分为若干扇区,然后对于整个空间会划分为很多很多页,每页256字节,这个我们需要记住。
- 控制逻辑左边就是spi的通信引脚,有wp、HOLD、CLK、CS、DI和DO,这些引脚就和我们的主控芯片相连,主控芯片通过spi协议,把指令和数据发给控制逻辑,控制逻辑就会自动去操作内部电路来完成我们想要的功能,然后去看控制逻辑上面有个状态寄存器器,这个状态寄存器是比较重要的,比如芯片是否处于忙状态,是否写使能,是否写保护,都可以在这个状态寄存器里体现,可以通过看手册分析,然后上面是写控制逻辑和外部的wp引脚相连,显然这个是配合wp引脚实现硬件写保护的,然后继续右边这里是一个高电压生成器,这个是配合flash进行编程的,因为flash是掉电不丢失的,如何实现掉电不丢失呢,比如你点亮一个led表示1,熄灭led表示0,但如果整个系统电都没有,那1和0就无从说起了,所以要想掉电不丢失,就要我们在存储器里产生一些刻骨铭心的变化,比如一个led我给他加很高的电压,那led就烧坏了,我们用烧坏的led表示1没烧坏的led表示0然后再断电,烧坏的led还是烧坏的,有电没电它都是坏的,这个烧没烧坏的状态,不受有电还是没电的影响,所以它就是掉电不丢失,那对于我们的非易失性存储器来说也是一样,我们要让它产生即使断电也不会消失的状态,一般都需要一个比较高的电压去刺激它,所以这种掉电不丢失的存储器,一般都需要一个高压源,那这里芯片内部集成了高电压发生器,所以就不需要我们在外接高电压了,比较方便哈,当然我这里只是举例简单描述一下掉电不丢失的存储原理。
- 然后继续看下面,这里是页地址锁存计数器,然后下面还有一个字节地址锁存计数器,这两个地址锁存和记数器就是用来指定地址的,我们通过spi总共发过来三个字节的地址,因为一页是256字节,所以一页内的字节地址就取决于最低一个字节,而高位的两个字节就对应的是页体质,所以在这里我们发的三个字节地址,前两个字节会进到这个页地址锁存计数器里,最后一个字节会进到这个字节地址锁存计数器里,然后页地址通过这个写保护和行解码来选择我要操作哪一页,字节地址通过这个列解码和256字节页缓存,来进行指定地址的读写操作,那就因为我们这个地址锁存,都是有个计数器的,所以这个地址指针在读写之后可以自动加1,这样就可以很容易实现从指定地址开始,连续读写多个字节的目的了,那最后右边这里有个256字节的页缓存区,它其实是一个256字节的ram存储器,这个稍微留个印象,等会儿还会提到,然后我们数据读写,就是通过这个ram缓冲区域来进行的,我们写入数据会先放到缓存区里,然后在时序结束后,芯片再将缓冲区的数据复制到对应的flash里,进行永久保存,那为啥要弄个缓冲区呢,我们直接往flash里写不好吗,那这是因为我们的spi写入的频率是非常高的,而flash的写入由于需要掉电不丢失,留下刻骨铭心的印象,他就比较慢,所以这个芯片的设计思路就是你写入的数据,我先放在缓存区里存着,因为缓存区是ram,所以它的速度非常快啊,可以跟得上spi总线的速度,但这里有个小问题,就这个缓冲区只有256字节,所以写入的时序有个限制条件,就是写入了一个时序,连续写入的数据量不能超过256字节,然后等你写完了,我芯片再慢慢的把数据从缓冲区转移到flash存储器里,那么数据从缓存区转到flash里,需要一定的时间哈,所以在写入时序结束后,芯片会进入一段忙的状态,在这里它就会有一条线哈,通往状态寄存器给状态接容器的busy位置1表示芯片当前正忙,那在忙的时候,芯片就不会响应新的读写时序了哈,就是写入的执行流程,然后我们读取数据,虽然这里画的话应该也是会通过缓冲区来读句,但是由于读取只看一下电路的状态就行了,它基本不花时间,所以读取的限制就很少了,速度也非常快。
4.Flash操作注意事项
- 第一点写入操作前必须先进行写使能,这个是一种保护措施,防止你误操作的,就像我们使用手机一样,先解锁再操作,这样可以防止手机在你裤兜里到处点点点对吧,写使能的话我们就使用spi发送一个写使能的指令,就可以完成了。
- 每个数据位只能由1改写为0,不能由0改写为1,这个意思就是说,flash并没有像ram那样的直接完全覆盖改写的能力,比如在某一个字节的存储单元里面,存储了0xAA这个数据,对应的二进制位就是10101010,如果我直接再次在这个存储单元写入一个新的数据,比如我再次写入一个0x55 ,那写完之后这个存储单元里存的是x55,实际上并不是,因为0x55的二进制是01010101,当这个01010101要覆盖原来的10101010时,就会受到这里第二条规定的限制,每个数据位只能由一改写为零,不能由零改写为一,你要问为啥会有这个限制,那只能说是成本原因或者技术原因,所以这里写入01010101之后,依次来看啊,最高位由原来的1改写为0是可以的,所以写出之后新的最高位就是零,但是第二位原来是零,现在我要改写成1,这是不行的,所以写入之后,新的第二位仍然是零,之后第三位要改写为零,可以,结果为零,第四位零改写为1,不可以,结果仍然是零,那以这个规律进行下去,0xaa在覆盖写入0x55 之后,这个存储单元最终的数据是什么啊,0x00也就是八位全为零,这就出现问题了对吧,所以为了弥补这个只能1改0,不能0改1的缺陷,我们就引出了第三条规定,就是写入数据前必须先擦除,擦除后所有数据位变为一,在这里flash是有一个擦除的概念的,擦除会有专门的擦除电路进行,我们只要给他发送擦除的指令就行了,那通过擦除电路擦除之后,所有的数据位都变成一,这样我们是不是就可以弥补第二条限制的缺陷了,当我们写出一个数据之前,无论原来存的是什么,我直接给它擦除掉,擦除之后所有的位变成1,也就是16进制的f f,这样我无论再写入什么样的数据,就都可以正确的写入了。
- 擦除必须按最小拆除单元进行,这个应该也是为了成本而做出的妥协,就是说你写入前要进行擦除,这我知道,所以如果我想在00这个地址下写入数据,那我就先把00地址擦除,再写入数据到00地址不就行了吗,但是这个方案有个问题啊,flash的擦除有最小擦除单元的限制,你不能指定某一个直接去擦除,要擦就得一大片一起擦,那在我们这个芯片里,你可以选择整个芯片擦除,也可以选择按块擦除或者按扇区擦除,然后再小就没有了,所以最小的擦除单元就是一个扇区,刚才我们看了一个扇区是4kb就是4096个字节,所以你擦除最少就得4096个字节一起擦,我只想查出某一个字节怎么办呢,这没办法你只能把那个字节所在扇区的4096个字节全都擦掉,那你又说这个扇区其他的地方我还存的有数据怎么办呢,这也没办法,要想不丢失数据,你只能先把4096个字节都读出来,再把4096个字节的扇区擦掉,改写完读出来的数据后,再把4096个字节全都写回去。
- 连续写入多字节时,最多写入一页的数据,超过页尾位置的数据会回到页首覆盖写入,这个意思就是说你在写入的时候,一次性不能写太多了,一个写入时序最多只能写一页的数据也就是256字节,为什么有这个限制呢,这是因为在这里有一个页缓冲区,它只有256字节,为什么有缓冲区呢,这是因为flash的写入太慢了,跟不上spi的频率,所以写入的数据会先放在ram里暂存,等时序结束后,芯片再慢慢的把数据写入到flash里,所以这里会有个限制,每个时序最多写入一页的数据,你再写多缓冲区存不下了,如果你非要写,那超过页尾位置的数据会回到页首覆盖写入,另外我们这个页缓存区是和flash的页对应的,你必须得从页起始位置开始写,才能最大写入256字节,如果你从页中间的地址开始写,那写到页尾时,这个地址就会跳回到页首,这会导致地址错乱哈,所以我们在进行多字节写入时,一定要注意这个地址范围不能跨越页的边缘,否则会地址错乱。
- 然后写入操作结束后,芯片进入忙状态,不响应新的读写操作,我们的写入操作都是对缓存区进行的,等时序结束后芯片还要搬砖一段时间,所以每次写入操作后,都有一段时间的忙状态,在这个状态下不要进行新的读写操作,否则芯片是不会响应我们的,要想知道芯片什么时候结束盲状态,我们可以使用读状态寄存器的指令,看一下状态寄存器的busy位是否为1,为0时芯片就不忙了,我们再进行操作,另外注意这个写入操作,包括上面的擦除,在发出擦除指令后,芯片也会进入忙状态,我们也得等忙状态结束后才能进行后续操作。
四、软件SPI读写W25Q64
面包板接线:
MySPI.c
#include "stm32f10x.h" // Device header/*引脚配置层*//*** 函 数:SPI写SS引脚电平* 参 数:BitValue 协议层传入的当前需要写入SS的电平,范围0~1* 返 回 值:无* 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SS为低电平,当BitValue为1时,需要置SS为高电平*/
void MySPI_W_SS(uint8_t BitValue)
{GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue); //根据BitValue,设置SS引脚的电平
}/*** 函 数:SPI写SCK引脚电平* 参 数:BitValue 协议层传入的当前需要写入SCK的电平,范围0~1* 返 回 值:无* 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SCK为低电平,当BitValue为1时,需要置SCK为高电平*/
void MySPI_W_SCK(uint8_t BitValue)
{GPIO_WriteBit(GPIOA, GPIO_Pin_5, (BitAction)BitValue); //根据BitValue,设置SCK引脚的电平
}/*** 函 数:SPI写MOSI引脚电平* 参 数:BitValue 协议层传入的当前需要写入MOSI的电平,范围0~0xFF* 返 回 值:无* 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置MOSI为低电平,当BitValue非0时,需要置MOSI为高电平*/
void MySPI_W_MOSI(uint8_t BitValue)
{GPIO_WriteBit(GPIOA, GPIO_Pin_7, (BitAction)BitValue); //根据BitValue,设置MOSI引脚的电平,BitValue要实现非0即1的特性
}/*** 函 数:I2C读MISO引脚电平* 参 数:无* 返 回 值:协议层需要得到的当前MISO的电平,范围0~1* 注意事项:此函数需要用户实现内容,当前MISO为低电平时,返回0,当前MISO为高电平时,返回1*/
uint8_t MySPI_R_MISO(void)
{return GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6); //读取MISO电平并返回
}/*** 函 数:SPI初始化* 参 数:无* 返 回 值:无* 注意事项:此函数需要用户实现内容,实现SS、SCK、MOSI和MISO引脚的初始化*/
void MySPI_Init(void)
{/*开启时钟*/RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟/*GPIO初始化*/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); //将PA4、PA5和PA7引脚初始化为推挽输出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); //将PA6引脚初始化为上拉输入/*设置默认电平*/MySPI_W_SS(1); //SS默认高电平MySPI_W_SCK(0); //SCK默认低电平
}/*协议层*//*** 函 数:SPI起始* 参 数:无* 返 回 值:无*/
void MySPI_Start(void)
{MySPI_W_SS(0); //拉低SS,开始时序
}/*** 函 数:SPI终止* 参 数:无* 返 回 值:无*/
void MySPI_Stop(void)
{MySPI_W_SS(1); //拉高SS,终止时序
}/*** 函 数:SPI交换传输一个字节,使用SPI模式0* 参 数:ByteSend 要发送的一个字节* 返 回 值:接收的一个字节*/
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{uint8_t i, ByteReceive = 0x00; //定义接收的数据,并赋初值0x00,此处必须赋初值0x00,后面会用到for (i = 0; i < 8; i ++) //循环8次,依次交换每一位数据{MySPI_W_MOSI(ByteSend & (0x80 >> i)); //使用掩码的方式取出ByteSend的指定一位数据并写入到MOSI线MySPI_W_SCK(1); //拉高SCK,上升沿移出数据if (MySPI_R_MISO() == 1){ByteReceive |= (0x80 >> i);} //读取MISO数据,并存储到Byte变量//当MISO为1时,置变量指定位为1,当MISO为0时,不做处理,指定位为默认的初值0MySPI_W_SCK(0); //拉低SCK,下降沿移入数据}return ByteReceive; //返回接收到的一个字节数据
}
MySPI.h
#ifndef __MYSPI_H
#define __MYSPI_Hvoid MySPI_Init(void);
void MySPI_Start(void);
void MySPI_Stop(void);
uint8_t MySPI_SwapByte(uint8_t ByteSend);#endif
W25Q64.c
#include "stm32f10x.h" // Device header
#include "MySPI.h"
#include "W25Q64_Ins.h"/*** 函 数:W25Q64初始化* 参 数:无* 返 回 值:无*/
void W25Q64_Init(void)
{MySPI_Init(); //先初始化底层的SPI
}/*** 函 数:MPU6050读取ID号* 参 数:MID 工厂ID,使用输出参数的形式返回* 参 数:DID 设备ID,使用输出参数的形式返回* 返 回 值:无*/
void W25Q64_ReadID(uint8_t *MID, uint16_t *DID)
{MySPI_Start(); //SPI起始MySPI_SwapByte(W25Q64_JEDEC_ID); //交换发送读取ID的指令*MID = MySPI_SwapByte(W25Q64_DUMMY_BYTE); //交换接收MID,通过输出参数返回*DID = MySPI_SwapByte(W25Q64_DUMMY_BYTE); //交换接收DID高8位*DID <<= 8; //高8位移到高位*DID |= MySPI_SwapByte(W25Q64_DUMMY_BYTE); //或上交换接收DID的低8位,通过输出参数返回MySPI_Stop(); //SPI终止
}/*** 函 数:W25Q64写使能* 参 数:无* 返 回 值:无*/
void W25Q64_WriteEnable(void)
{MySPI_Start(); //SPI起始MySPI_SwapByte(W25Q64_WRITE_ENABLE); //交换发送写使能的指令MySPI_Stop(); //SPI终止
}/*** 函 数:W25Q64等待忙* 参 数:无* 返 回 值:无*/
void W25Q64_WaitBusy(void)
{uint32_t Timeout;MySPI_Start(); //SPI起始MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1); //交换发送读状态寄存器1的指令Timeout = 100000; //给定超时计数时间while ((MySPI_SwapByte(W25Q64_DUMMY_BYTE) & 0x01) == 0x01) //循环等待忙标志位{Timeout --; //等待时,计数值自减if (Timeout == 0) //自减到0后,等待超时{/*超时的错误处理代码,可以添加到此处*/break; //跳出等待,不等了}}MySPI_Stop(); //SPI终止
}/*** 函 数:W25Q64页编程* 参 数:Address 页编程的起始地址,范围:0x000000~0x7FFFFF* 参 数:DataArray 用于写入数据的数组* 参 数:Count 要写入数据的数量,范围:0~256* 返 回 值:无* 注意事项:写入的地址范围不能跨页*/
void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count)
{uint16_t i;W25Q64_WriteEnable(); //写使能MySPI_Start(); //SPI起始MySPI_SwapByte(W25Q64_PAGE_PROGRAM); //交换发送页编程的指令MySPI_SwapByte(Address >> 16); //交换发送地址23~16位MySPI_SwapByte(Address >> 8); //交换发送地址15~8位MySPI_SwapByte(Address); //交换发送地址7~0位for (i = 0; i < Count; i ++) //循环Count次{MySPI_SwapByte(DataArray[i]); //依次在起始地址后写入数据}MySPI_Stop(); //SPI终止W25Q64_WaitBusy(); //等待忙
}/*** 函 数:W25Q64扇区擦除(4KB)* 参 数:Address 指定扇区的地址,范围:0x000000~0x7FFFFF* 返 回 值:无*/
void W25Q64_SectorErase(uint32_t Address)
{W25Q64_WriteEnable(); //写使能MySPI_Start(); //SPI起始MySPI_SwapByte(W25Q64_SECTOR_ERASE_4KB); //交换发送扇区擦除的指令MySPI_SwapByte(Address >> 16); //交换发送地址23~16位MySPI_SwapByte(Address >> 8); //交换发送地址15~8位MySPI_SwapByte(Address); //交换发送地址7~0位MySPI_Stop(); //SPI终止W25Q64_WaitBusy(); //等待忙
}/*** 函 数:W25Q64读取数据* 参 数:Address 读取数据的起始地址,范围:0x000000~0x7FFFFF* 参 数:DataArray 用于接收读取数据的数组,通过输出参数返回* 参 数:Count 要读取数据的数量,范围:0~0x800000* 返 回 值:无*/
void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count)
{uint32_t i;MySPI_Start(); //SPI起始MySPI_SwapByte(W25Q64_READ_DATA); //交换发送读取数据的指令MySPI_SwapByte(Address >> 16); //交换发送地址23~16位MySPI_SwapByte(Address >> 8); //交换发送地址15~8位MySPI_SwapByte(Address); //交换发送地址7~0位for (i = 0; i < Count; i ++) //循环Count次{DataArray[i] = MySPI_SwapByte(W25Q64_DUMMY_BYTE); //依次在起始地址后读取数据}MySPI_Stop(); //SPI终止
}
W25Q64.h
#ifndef __W25Q64_H
#define __W25Q64_Hvoid 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
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
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "W25Q64.h"uint8_t MID; //定义用于存放MID号的变量
uint16_t DID; //定义用于存放DID号的变量uint8_t ArrayWrite[] = {0x01, 0x02, 0x03, 0x04}; //定义要写入数据的测试数组
uint8_t ArrayRead[4]; //定义要读取数据的测试数组int main(void)
{/*模块初始化*/OLED_Init(); //OLED初始化W25Q64_Init(); //W25Q64初始化/*显示静态字符串*/OLED_ShowString(1, 1, "MID: DID:");OLED_ShowString(2, 1, "W:");OLED_ShowString(3, 1, "R:");/*显示ID号*/W25Q64_ReadID(&MID, &DID); //获取W25Q64的ID号OLED_ShowHexNum(1, 5, MID, 2); //显示MIDOLED_ShowHexNum(1, 12, DID, 4); //显示DID/*W25Q64功能函数测试*/W25Q64_SectorErase(0x000000); //扇区擦除W25Q64_PageProgram(0x000000, ArrayWrite, 4); //将写入数据的测试数组写入到W25Q64中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){}
}
五、硬件SPI读写W25Q64
硬件SPI读写W25Q64有固定引脚如下:
MySPI.c
#include "stm32f10x.h" // Device header/*** 函 数:SPI写SS引脚电平,SS仍由软件模拟* 参 数:BitValue 协议层传入的当前需要写入SS的电平,范围0~1* 返 回 值:无* 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SS为低电平,当BitValue为1时,需要置SS为高电平*/
void MySPI_W_SS(uint8_t BitValue)
{GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue); //根据BitValue,设置SS引脚的电平
}/*** 函 数:SPI初始化* 参 数:无* 返 回 值:无*/
void MySPI_Init(void)
{/*开启时钟*/RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE); //开启SPI1的时钟/*GPIO初始化*/GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA, &GPIO_InitStructure); //将PA4引脚初始化为推挽输出GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA, &GPIO_InitStructure); //将PA5和PA7引脚初始化为复用推挽输出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); //将PA6引脚初始化为上拉输入/*SPI初始化*/SPI_InitTypeDef SPI_InitStructure; //定义结构体变量SPI_InitStructure.SPI_Mode = SPI_Mode_Master; //模式,选择为SPI主模式SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; //方向,选择2线全双工SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; //数据宽度,选择为8位SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; //先行位,选择高位先行SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_128; //波特率分频,选择128分频SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; //SPI极性,选择低极性SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; //SPI相位,选择第一个时钟边沿采样,极性和相位决定选择SPI模式0SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; //NSS,选择由软件控制SPI_InitStructure.SPI_CRCPolynomial = 7; //CRC多项式,暂时用不到,给默认值7SPI_Init(SPI1, &SPI_InitStructure); //将结构体变量交给SPI_Init,配置SPI1/*SPI使能*/SPI_Cmd(SPI1, ENABLE); //使能SPI1,开始运行/*设置默认电平*/MySPI_W_SS(1); //SS默认高电平
}/*** 函 数:SPI起始* 参 数:无* 返 回 值:无*/
void MySPI_Start(void)
{MySPI_W_SS(0); //拉低SS,开始时序
}/*** 函 数:SPI终止* 参 数:无* 返 回 值:无*/
void MySPI_Stop(void)
{MySPI_W_SS(1); //拉高SS,终止时序
}/*** 函 数:SPI交换传输一个字节,使用SPI模式0* 参 数:ByteSend 要发送的一个字节* 返 回 值:接收的一个字节*/
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) != SET); //等待发送数据寄存器空SPI_I2S_SendData(SPI1, ByteSend); //写入数据到发送数据寄存器,开始产生时序while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) != SET); //等待接收数据寄存器非空return SPI_I2S_ReceiveData(SPI1); //读取接收到的数据并返回
}
MySPI.h
#ifndef __MYSPI_H
#define __MYSPI_Hvoid MySPI_Init(void);
void MySPI_Start(void);
void MySPI_Stop(void);
uint8_t MySPI_SwapByte(uint8_t ByteSend);#endif
W25Q64.c
#include "stm32f10x.h" // Device header
#include "MySPI.h"
#include "W25Q64_Ins.h"/*** 函 数:W25Q64初始化* 参 数:无* 返 回 值:无*/
void W25Q64_Init(void)
{MySPI_Init(); //先初始化底层的SPI
}/*** 函 数:MPU6050读取ID号* 参 数:MID 工厂ID,使用输出参数的形式返回* 参 数:DID 设备ID,使用输出参数的形式返回* 返 回 值:无*/
void W25Q64_ReadID(uint8_t *MID, uint16_t *DID)
{MySPI_Start(); //SPI起始MySPI_SwapByte(W25Q64_JEDEC_ID); //交换发送读取ID的指令*MID = MySPI_SwapByte(W25Q64_DUMMY_BYTE); //交换接收MID,通过输出参数返回*DID = MySPI_SwapByte(W25Q64_DUMMY_BYTE); //交换接收DID高8位*DID <<= 8; //高8位移到高位*DID |= MySPI_SwapByte(W25Q64_DUMMY_BYTE); //或上交换接收DID的低8位,通过输出参数返回MySPI_Stop(); //SPI终止
}/*** 函 数:W25Q64写使能* 参 数:无* 返 回 值:无*/
void W25Q64_WriteEnable(void)
{MySPI_Start(); //SPI起始MySPI_SwapByte(W25Q64_WRITE_ENABLE); //交换发送写使能的指令MySPI_Stop(); //SPI终止
}/*** 函 数:W25Q64等待忙* 参 数:无* 返 回 值:无*/
void W25Q64_WaitBusy(void)
{uint32_t Timeout;MySPI_Start(); //SPI起始MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1); //交换发送读状态寄存器1的指令Timeout = 100000; //给定超时计数时间while ((MySPI_SwapByte(W25Q64_DUMMY_BYTE) & 0x01) == 0x01) //循环等待忙标志位{Timeout --; //等待时,计数值自减if (Timeout == 0) //自减到0后,等待超时{/*超时的错误处理代码,可以添加到此处*/break; //跳出等待,不等了}}MySPI_Stop(); //SPI终止
}/*** 函 数:W25Q64页编程* 参 数:Address 页编程的起始地址,范围:0x000000~0x7FFFFF* 参 数:DataArray 用于写入数据的数组* 参 数:Count 要写入数据的数量,范围:0~256* 返 回 值:无* 注意事项:写入的地址范围不能跨页*/
void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count)
{uint16_t i;W25Q64_WriteEnable(); //写使能MySPI_Start(); //SPI起始MySPI_SwapByte(W25Q64_PAGE_PROGRAM); //交换发送页编程的指令MySPI_SwapByte(Address >> 16); //交换发送地址23~16位MySPI_SwapByte(Address >> 8); //交换发送地址15~8位MySPI_SwapByte(Address); //交换发送地址7~0位for (i = 0; i < Count; i ++) //循环Count次{MySPI_SwapByte(DataArray[i]); //依次在起始地址后写入数据}MySPI_Stop(); //SPI终止W25Q64_WaitBusy(); //等待忙
}/*** 函 数:W25Q64扇区擦除(4KB)* 参 数:Address 指定扇区的地址,范围:0x000000~0x7FFFFF* 返 回 值:无*/
void W25Q64_SectorErase(uint32_t Address)
{W25Q64_WriteEnable(); //写使能MySPI_Start(); //SPI起始MySPI_SwapByte(W25Q64_SECTOR_ERASE_4KB); //交换发送扇区擦除的指令MySPI_SwapByte(Address >> 16); //交换发送地址23~16位MySPI_SwapByte(Address >> 8); //交换发送地址15~8位MySPI_SwapByte(Address); //交换发送地址7~0位MySPI_Stop(); //SPI终止W25Q64_WaitBusy(); //等待忙
}/*** 函 数:W25Q64读取数据* 参 数:Address 读取数据的起始地址,范围:0x000000~0x7FFFFF* 参 数:DataArray 用于接收读取数据的数组,通过输出参数返回* 参 数:Count 要读取数据的数量,范围:0~0x800000* 返 回 值:无*/
void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count)
{uint32_t i;MySPI_Start(); //SPI起始MySPI_SwapByte(W25Q64_READ_DATA); //交换发送读取数据的指令MySPI_SwapByte(Address >> 16); //交换发送地址23~16位MySPI_SwapByte(Address >> 8); //交换发送地址15~8位MySPI_SwapByte(Address); //交换发送地址7~0位for (i = 0; i < Count; i ++) //循环Count次{DataArray[i] = MySPI_SwapByte(W25Q64_DUMMY_BYTE); //依次在起始地址后读取数据}MySPI_Stop(); //SPI终止
}
W25Q64.h
#ifndef __W25Q64_H
#define __W25Q64_Hvoid 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
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
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "W25Q64.h"uint8_t MID; //定义用于存放MID号的变量
uint16_t DID; //定义用于存放DID号的变量uint8_t ArrayWrite[] = {0x01, 0x02, 0x03, 0x04}; //定义要写入数据的测试数组
uint8_t ArrayRead[4]; //定义要读取数据的测试数组int main(void)
{/*模块初始化*/OLED_Init(); //OLED初始化W25Q64_Init(); //W25Q64初始化/*显示静态字符串*/OLED_ShowString(1, 1, "MID: DID:");OLED_ShowString(2, 1, "W:");OLED_ShowString(3, 1, "R:");/*显示ID号*/W25Q64_ReadID(&MID, &DID); //获取W25Q64的ID号OLED_ShowHexNum(1, 5, MID, 2); //显示MIDOLED_ShowHexNum(1, 12, DID, 4); //显示DID/*W25Q64功能函数测试*/W25Q64_SectorErase(0x000000); //扇区擦除W25Q64_PageProgram(0x000000, ArrayWrite, 4); //将写入数据的测试数组写入到W25Q64中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){}
}