企业网站建设感想做百度手机网站点击

当前位置: 首页 > news >正文

企业网站建设感想,做百度手机网站点击,成都营销型网站公司电话,施工企业跨专业接工作是否有效第10章 SPI通信 10.1 SPI通信协议 10.1.1 SPI通信 SPI#xff08;Serial Peripheral Interface#xff09;是由Motorola公司开发的一种通用数据总线#xff1b; 串行外设接口#xff1b; I2C无论是软件还是软件电路#xff0c;设计的都还是比较复杂的#xff0c;硬件…第10章 SPI通信 10.1 SPI通信协议 10.1.1 SPI通信 SPISerial Peripheral Interface是由Motorola公司开发的一种通用数据总线 串行外设接口 I2C无论是软件还是软件电路设计的都还是比较复杂的硬件上我们要配置为开漏外加上拉的模式软件上有很多功能和要求比如一根通信线兼顾数据收发、应答位的收发、寻址机制的设计等等。通过这么多的设计使得I2C的通信性价比非常高I2C可以在消耗最低硬件资源的情况下实现最多的功能。在硬件上无论挂载多少个设备都只需要两根通信线在软件上数据双向通信、应答位都可以实现如果把通信协议比做人的话那I2C就属于精打细算、思维灵活的人既要实现硬件上最少的通信线又要实现软件上最多的功能。I2C经过精心的设计也确实实现了这么多功能。缺点就是由于I2C采用开漏外加上拉电阻的电路结构使得通信线高电平的驱动能力比较弱这就会导致通信线由低电平变到高电平的时候上升沿比较长这会限制I2C的最大通信速度所以I2C的标准模式只有100KHz的时钟频率I2C的快速模式也只有400KHz虽然I2C协议最后又通过改进电路的方式设计出了高速模式可以达到3.4MHz但是高速模式目前普及模式不是很高所以一般情况下我们认为I2C的时钟速度最多就是400KHz这个速度相比较I2C而言还是慢了很多的。 SPI的优缺点 1SPI传输更快SPI协议并没有严格规定最大传输速度这个最大传输速度取决于芯片厂商的设计需求比如下图第一个图所示W25Q64存储器芯片手册里写的SPI时钟频率最大可达80Hz ,这比STM32F1的主频还要高 2其次SPI的设计比较简单粗暴实现的功能没有I2C那么多所以学习起来SPI比I2C简单很多 3SPI硬件开销比较大通信线的个数比较多并且通信过程中经常会有资源浪费的现象如果继续把通信协议比作一个人的话SPI就属于富家子弟、有钱任性这类型的人。SPI不在乎花了多少钱只在乎任务有没有最简单、最快速的完成。 四根通信线SCKSerial Clock、MOSIMaster Output Slave Input、MISOMaster Input Slave Output、SSSlave Select SCK串行时钟线 MOSI主机输出、从机输入 MISO主机输入、从机输出 SS从机选择 以上是SPI通信典型的引脚名称当然在实际情况下这些名称可能会有别的表述方式比如SCK有的地方可能叫做SCLK、CLK、CKMOSI和MISO有的地方可能直接叫做DOData Output和DIData InputSS有的地方也可能叫做NSSNot Slave Select、CSChip 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和MISODI到底是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有两个可以配置的位分别叫做CPOLClock Polarity、时钟极性和CPHAClock Phase,每一位都可以配置为1或0总共组合起来就有模式0、模式1、模式2、模式3这4中模式。模式虽然多但功能都是一样的。 实际应用中模式0的应用是最多的。模式0和模式1的区别就在于模式0把数据变化的时机给提前了。 交换一个字节模式0 CPOL0空闲状态时SCK为低电平 CPHA0SCK第一个边沿移入数据第二个边沿移出数据 MISO起始和终止位高阻态。 交换一个字节模式1 CPOL0空闲状态时SCK为低电平 CPHA1SCK第一个边沿移出数据第二个边沿移入数据或叫做进行采样 交换一个字节模式2 CPOL1空闲状态时SCK为高电平 CPHA0SCK第一个边沿移入数据第二个边沿移出数据 交换一个字节模式3 CPOL1空闲状态时SCK为高电平 CPHA1SCK第一个边沿移出数据第二个边沿移入数据 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芯片里的数据在断电重启后数据仍然保持原样。 字库存储可以用这个数据来存储汉字字库的点阵数据在显示某个数据之前先读取芯片查询字库再在显示屏上显示对应的点阵数据这样就能让显示屏任意显示中文了。 固件程序存储就相当于直接把程序文件下载到外挂芯片里需要执行程序的时候直接读取外挂芯片的程序文件来执行。这就是XIPeXecute 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和四重SPIMOSI用于发送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/102416MB的寻址空间。 W25Q256分为3字节地址模式和4字节地址模式在3字节地址模式下只能读取前16MB的数据后面16MB,3个字节的地址够不着要想读写到所有鵆单元可以进入4字节地址的模式。 10.2.2 硬件电路 引脚 功能 VCC、GND 电源2.7~3.6V CSSS SPI片选 CLKSCK SPI时钟 DIMOSI SPI主机输出从机输入 DOMISO SPI主机输入从机输出 WP 写保护 HOLD 数据保持
WPWrite 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位是否为1BUSY位为0时芯片就不忙了我们再进行操作。 另外这个写入操作包括上面的擦除在发出擦除指令后芯片也会进入忙状态我们也得等忙状态结束后才能进行后续操作。 读取操作时 直接调用读取时序无需使能无需额外操作没有页的限制读取操作结束后不会进入忙状态但不能在忙状态时读取。 Flash作为一种掉电不丢失的存储器为了保证掉电不丢失这个特性同时还要保证存储容量足够大、成本足够低所以Flash存储器会在其它地方比如操作的便捷性等做一些妥协和让步。Flash的写入和读取并不像RAM那样简单直接RAM是指哪打哪想在哪写就在哪写想写多少就写多少并且RAM是可以覆盖写入的。比如原来RAM里有个数据0xAA之后我直接再写入一个新的数据0x55那RAM的数据就变成0x55了。 10.2.5 器件手册 1芯片引脚定义及描述 2芯片系统框图 3SPI操作 4写保护逻辑 5状态寄存器  状态寄存器示意图  6 写保护配置表 7指令集 指令翻译Write Enable写使能Write Disable写失能Read Status Register-1读状态寄存器1Page Progam页编程Block Erase64KB按64KB的块擦除Block Erase32KB按32KB的块擦除Sector Erase4KB扇区擦除Chip Erase整片擦除JEDEC ID读ID号 指令翻译Read Data读取数据 10.3 软件SPI读写W25Q64 10.3.1 硬件电路 10.3.2 软件部分 1复制《OLED显示屏》工程并改名为《软件SPI读写W25Q64》 2添加驱动文件 3MySPI.c #include stm32f10x.h // Device header/从机选择函数/ void MySPI_W_SS(uint8_t BitValue) {GPIO_WriteBit(GPIOA,GPIO_Pin_4,(BitAction)BitValue); //SS端接在PA4引脚上 }/SCK控制函数/ void MySPI_W_SCK(uint8_t BitValue) {GPIO_WriteBit(GPIOA,GPIO_Pin_5,(BitAction)BitValue); //SCK端接在PA5引脚上}/MOSI控制函数/ void MySPI_W_MOSI(uint8_t BitValue) {GPIO_WriteBit(GPIOA,GPIO_Pin_7,(BitAction)BitValue); //MOSI端接在PA7引脚上 }/MISO控制函数/ uint8_t MySPI_R_MISO(void) {return GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_6); //MISO端接在PA6引脚上,STM32读取W25Q64数据 }/软件SPI的初始化函数/ void MySPI_Init(void) { RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitTypeDef GPIO_InitStruct;GPIO_InitStruct.GPIO_Mode GPIO_Mode_Out_PP; //输出引脚配置为推挽输出GPIO_InitStruct.GPIO_Pin GPIO_Pin_4|GPIO_Pin_5|GPIO_Pin_7;GPIO_InitStruct.GPIO_Speed GPIO_Speed_50MHz;GPIO_Init(GPIOA,GPIO_InitStruct);GPIO_InitStruct.GPIO_Mode GPIO_Mode_IPU; //输入引脚配置为上拉输入GPIO_InitStruct.GPIO_Pin GPIO_Pin_6;GPIO_InitStruct.GPIO_Speed GPIO_Speed_50MHz;GPIO_Init(GPIOA,GPIO_InitStruct);MySPI_W_SS(1); //初始化时给ss置高电平默认不选中从机MySPI_W_SCK(0); //使用模式0默认是低电平。 } /起始条件函数/ void MySPI_Start(void) {MySPI_W_SS(0); } /终止条件函数/ void MySPI_Stop(void) {MySPI_W_SS(1); } /交换字节函数这种方法使用掩码依次提出每一位不会改变传入参数本身/ uint8_t MySPI_SwapByte(uint8_t ByteSend) {uint8_t i,ByteReceive 0x00; //用来接收字节for(i0;i8;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(i0;i8;i) // { // MySPI_W_MOSI(ByteSend 0x80); //发送最高位 // ByteSend 1; //次高位向左移位变成最高位准备下一次发送
// MySPI_W_SCK(1); //产生上升沿程序把MOSI总线上的数据ByteSend 0x80读走 // if (MySPI_R_MISO()1){ByteSend | 0x01;} // MySPI_WSCK(0); //产生下降沿主机发送下一位 // } // return ByteReceive; //} 4MySPI.h #ifndef __MYSPI #define __MYSPI_void MySPI_Init(void); void MySPI_Start(void); void MySPI_Stop(void); uint8_t MySPI_SwapByte(uint8_t ByteSend);#endif 5W25Q64_lns.h #ifndef __W25Q64_INS_H #define __W25Q64_INS_H#define W25Q64_WRITE_ENABLE 0x06 #define W25Q64_WRITE_DISABLE 0x04 #define W25Q64_READ_STATUS_REGISTER_1 0x05 #define W25Q64_READ_STATUS_REGISTER_2 0x35 #define W25Q64_WRITE_STATUS_REGISTER 0x01 #define W25Q64_PAGE_PROGRAM 0x02 #define W25Q64_QUAD_PAGE_PROGRAM 0x32 #define W25Q64_BLOCK_ERASE_64KB 0xD8 #define W25Q64_BLOCK_ERASE_32KB 0x52 #define W25Q64_SECTOR_ERASE_4KB 0x20 #define W25Q64_CHIP_ERASE 0xC7 #define W25Q64_ERASE_SUSPEND 0x75 #define W25Q64_ERASE_RESUME 0x7A #define W25Q64_POWER_DOWN 0xB9 #define W25Q64_HIGH_PERFORMANCE_MODE 0xA3 #define W25Q64_CONTINUOUS_READ_MODE_RESET 0xFF #define W25Q64_RELEASE_POWER_DOWN_HPM_DEVICE_ID 0xAB #define W25Q64_MANUFACTURER_DEVICE_ID 0x90 #define W25Q64_READ_UNIQUE_ID 0x4B #define W25Q64_JEDEC_ID 0x9F #define W25Q64_READ_DATA 0x03 #define W25Q64_FAST_READ 0x0B #define W25Q64_FAST_READ_DUAL_OUTPUT 0x3B #define W25Q64_FAST_READ_DUAL_IO 0xBB #define W25Q64_FAST_READ_QUAD_OUTPUT 0x6B #define W25Q64_FAST_READ_QUAD_IO 0xEB #define W25Q64_OCTAL_WORD_READ_QUAD_IO 0xE3#define W25Q64_DUMMY_BYTE 0xFF#endif6W25Q64.c #include stm32f10x.h // Device header #include MySPI.h #include W25Q64_lns.h/W25Q64初始化函数/ void W25Q64_Init(void) {MySPI_Init(); }/读取ID号函数/ void W25Q64_ReadID(uint8_t *MID,uint16_t *DID) {MySPI_Start();MySPI_SwapByte(W25Q64_JEDEC_ID); //发送读ID号的指令*MID MySPI_SwapByte(W25Q64_DUMMY_BYTE); //随便给从机发一个东西没有意义目的就是把从机的数据置换过来获取到厂商ID*DID MySPI_SwapByte(W25Q64_DUMMY_BYTE); //获取设备ID的高8位*DID 8;*DID | MySPI_SwapByte(W25Q64_DUMMY_BYTE); //获取设备ID的低8位MySPI_Stop(); }/写使能/ void W25Q64_WriteEnable(void) {MySPI_Start();MySPI_SwapByte(W25Q64_WRITE_ENABLE); MySPI_Stop(); }/状态获取函数/ void W25Q64_WaitBusy(void) {uint32_t Timeout;MySPI_Start();MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1); //获取寄存器状态指令Timeout 100000;while((MySPI_SwapByte(W25Q64_DUMMY_BYTE) 0x01) 0x01) //等待Busy状态结束{Timeout–;if(Timeout 0){break; //超时退出}}MySPI_Stop(); }/页编程函数/ void W25Q64_PageProgram(uint32_t Address,uint8_t *DataArray,uint16_t Count) {uint16_t i;W25Q64_WriteEnable(); //写入操作前都必须进行写使能MySPI_Start();MySPI_SwapByte(W25Q64_PAGE_PROGRAM);MySPI_SwapByte(Address 16);MySPI_SwapByte(Address 8);MySPI_SwapByte(Address);for(i0;iCount;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(i0;iCount;i){DataArray[i] MySPI_SwapByte(W25Q64_DUMMY_BYTE);} MySPIStop(); } 7W25Q64.h #ifndef __W25Q64 #define __W25Q64 void W25Q64_Init(void); void W25Q64_ReadID(uint8_t *MID,uint16_t *DID); void W25Q64_PageProgram(uint32_t Address,uint8_t *DataArray,uint16_t Count); void W25Q64_SectorErase(uint32_t Address); void W25Q64_ReadData(uint32_t Address,uint8_t *DataArray,uint32_t Count); #endif 8main.c #include stm32f10x.h // Device header #include Delay.h // 调用延时头文件 #include OLED.h #include W25Q64.huint8_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的时钟就是由分频得来的 PCLKPeripheral Clock就是外设时钟APB2的PCLK就是72MHzAPB1的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”其它文件不变。 #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配置成模式0SPI_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 }