一、SPI简介1SPISerial Peripheral Interface是由Motorola公司开发的一种通用数据总线2四根通信线SCKSerial Clock、MOSIMaster Output Slave Input主机输出从机输入主机向从机发送数据的线路、MISOMaster Input Slave Output主机从从机接收数据的线路、SSSlave Select3同步全双工数据发送和数据接收单独各占一条线路二者互不影响4支持总线挂载多设备一主多从-----I2C一主多从的方式是在起始条件之后主机必须发送一个字节进行寻址用来指定我要跟哪个从机进行通信所以I2C涉及到分配地址和寻址的问题SPI表示直接再开辟一条通信线路专门指定我要跟哪个从机进行通信---SSSS可能不止一条SPI的主机表示我有几个从机我就开几个SS主机需要找从机的时候主机就控制某一条SS线SPI没有应答机制的涉及发送就是发送接收就是接收至于对面存不存在---SPI不管。二、硬件电路1所有SPI设备的SCK、MOSI、MISO分别连在一起。2主机另外引出多条SS控制线分别接到各从机的SS引脚主机需要找谁通信就把谁的SS置低电平同一时间主机只能置一个SS为低电平即只能选择一个从机否则主机同时选中多个从机会导致数据冲突。3输出引脚配置为推挽输出推挽--高低电平具有很强的推挽能力将使得SPI引脚信号的下降沿和上升沿迅速----信号变化的快可以有更快的数据传输速度输入引脚配置为浮空或上拉输入。图中引脚主机只有一个MISO引脚是输入但是3个从机全都是输出如果三个从机始终都是推挽输出势必会导致冲突SPI协议规定当从机的SS引脚为高电平的时候从机没有被选中它的MISO引脚必须切换成高阻态高阻态就相当于引脚断开不输出任何电平这样可以防止一条线有多个输出而导致的电平冲突的问题当SS为低电平的时候MISO才允许变为推挽输出前面所说的切换过程都是从机里的我们一般写的是主机的程序所以主机程序不用关心这个问题。SPI所有通信线都是单端信号高低电平都是相对于GND的电压差单端信号所有设备都需要供地图中没有标出GND和VCC。三、移位示意图SPI的基本收发电路就是使用移位的模型SPI主/从机都有一个8位的移位寄存器 移位寄存器有一个时钟输入端SPI一般是高位先行每来一个时钟移位寄存器就会向左移位移位寄存器的时钟源是由主机提供的---这里叫做波特率发送器它产生时钟驱动主机的移位寄存器进行移位同时这个时钟也通过SCK引脚进行输出接到从机的寄存器里面。主机移位寄存器左边移动出去的数据通过MOSI引脚输入到从机移位寄存器的右边从机移位寄存器左边移动出去的数据通过MISO引脚输入到主机移位寄存器的右边。波特率发生器时钟的上升沿所有移位寄存器向左移动一位移出去的位放到引脚上波特率发生器时钟的下降沿引脚上的位采样输入到移位寄存器的最低位。假设主机数据10101010要发送到从机同时从机有数据01010101要发送到主机那我们就可以驱动时钟产生一个上升沿所有的位就会向左移动一次数据放到通信线上实际上是放到了输出数据寄存器此时MOSI数据是1所以MOSI是高电平MISO同理时钟下降沿时主从机都会进行数据的采样输入也就是MOSI的1会采样输入到从机最低位如图所示这就是第一个时钟结束后的现象。依次往后最后的现象是主机中的数据和从机的数据互换了这就实现了主从机一个字节的数据的交换。SPI的数据收发都是基于字节交换这个基本单元来进行的当主机需要发送一个字节并且同时需要接收一个字节的时候就可以执行一下字节交换的时序这样主机要发送的数据跑到从机主机从从机接收的数据跑到主机这就完成了发送同时接收的功能。当只想发送或者只想接收的时候同样采用字节交换只不过把一方的数据换成0XFF或0完成置换数据。小总结SPI通信的基础就是交换一个字节在此基础上就可以实现发送一个字节、接收一个字节和发送同时接收一个字节。四、SPI的时序基本单元4.1 起始和终止条件1起始条件SS从高电平切换到低电平2终止条件SS从低电平切换到高电平结束了从机的选中状态SS低电平有效---表示选中从机选中的过程中SS始终保持为低电平低电平期间表示正在通信。3下降沿是通讯的开始上升沿是通讯的结束4.2交换一个字节模式0CPOL时钟极性0空闲状态时SCK为低电平CPHA时钟相位0SCK第一个边沿移入数据第二个边沿移出图中是下降沿数据----------CPHA时钟相位决定第一个时钟采样移入还是第二个时钟采样移入SCK第一个边沿之前就要提前开始移除数据了或者把它称之为第0个边沿移出第1个边沿移入如图所示趁着SCK没有变化SS下降沿的时候就要立刻触发移位输出所以这里MOSI和MISO的输出是对齐SS的下降沿的这样SCK上升沿就可以采样输入数据了这样B7就采样完毕SCK下降沿移除B6SCK上升沿移入B6........第八个上升沿的时候B0位移入完成一个字节此时交换完成之后SCK还有一个下降沿如果主机只需要交换一个字节就结束那么在这个下降沿MOSI可以置回默认电平或者不管。模式0把数据变化的时机给提前了模式0实际中用的多。4.3交换一个字节模式1CPOL0空闲状态时SCK为低电平CPHA1SCK第一个边沿移出数据主机和从机同时移除数据主机通过MOSI移出最高位此时MOSI的电平表示主机要发送的数据B7从机通过MISO移除最高位MISO表示从机要发送的数据B7然后时钟运行产生下降沿此时主从机同时移入数据也就是进行数据采样第二个边沿移入数据主机移出的B7进入从机移位寄存器的最低位从机移出的B7进入主机移位寄存器的最低位这样一个时钟脉冲产生完毕一个数据位传输完毕。接下里同样上升沿主从机同时输出当前移位寄存器的最高位第二次最高位就是原始数据的B6.......最后一个下降沿数据B0传输完成至此主从机就完成了一个字节的数据交换如果主机只想要交换一个字节那这时候就可以置SS为高电平结束通信在SS的上升沿MOSI还可以在变化一次将MOSI置到一个默认的高电平或者低电平SPI也没有硬性规定MOSI的默认电平然后MOSI从机必须置回高阻态了。上述就是一个字节交换的流程要想交换多个字节不必把SS置回高电平重复前面的字节操作即可。多个从机输出连在一起同时开启会造成冲突所以在SS未被选中时从机的MISO引脚必须关断输出即配置输出为高阻状态时序图中MISO用一条中间线表示高阻态)。通信开始前SS为高电平通信过程中SS保持低电平MISO因为多个从机输出连在一起如果同时开启输出会造成冲突所以解决办法是在SS未被选中的时候从机的MISO引脚必须关闭输出即配置输出为高阻状态如图所示MISO用一条中间的横线表示高阻状态。SS下降沿后从机的MISO允许开启输出SS上升沿之后从机的MISO必须置回高阻态。4.4 SPI时序以芯片W25Q64的时序波形进行讲解的1、1以下波形是发送指令。2向SS指定的设备发送指令0x06。从机一对比定义好的指令集发现0X06是写使能的指令那么从机就会控制硬件进行写使能。对比I2C字节流I2C有效数据流第一个字节是寄存器地址之后依次是读写的数据使用的是读写寄存器的模型SPI采用的是指令码加读写数据的模型SPI起始后第一个交换发送给从机的数据一般叫做指令码从机中对应的会定义一个指令集SPI的从机芯片手册中会定义好什么指令对应什么功能什么指令后面跟上什么数据。在空闲状态的时候SS为高电平SCK为低电平MOSI和MISO的默认电平没有严格规定下图使用的是SPI模式0主机用0X06换来了从机的0XFF实际上从机并没有输出0XFF是默认的高电平。2、1指定地址写2向SS指定的设备发送写指令0x02随后在指定地址Address[23:0]下写入指定数据Data图中是下降沿变化数据上升沿采样数据通过3个字节的交换24位的地址就发送完毕了从机收到的24位地址是0X1234563位地址结束后就要发送写入指定地址的内容了表示在0X123456地址下写入0X55的数据。这就是SPI写入的时序由于SPI没有应答的机制所以交换一个字节后就立刻交换下一个字节就行了整个流程并没有接收的功能所以MOSI这条接收的线路始终处于挂机的状态没有用到W25Q64存储器容量8M所以需要连续指定3个字节的地址。3、1指定地址读2向SS指定的设备发送读指令0x03随后在指定地址Address[23:0]下读取从机数据Data主机发送指令0X03代表我要读取数据了之后还是一样主机依次交换3个字节分别是0X120X340X56组合一起就是0X123456代表24位地址指定地址之后要开始接收数据需要把从机的数据搞过来随便给从机个数据一般是0XFF然后从机就会把0X123456地址下的数据通过MISO发给主机这样主机就实现了指定地址读一个字节的目的。五、W25Q64W25Qxx系列是一种低成本、小型化、使用简单的非易失性存储器存储数据上电不丢失常应用于数据存储、字库存储、固件程序存储等场景存储介质Nor Flash闪存时钟频率80MHz / 160MHz (Dual SPI) / 320MHz(Quad SPI)存储容量24位地址W25Q404Mbit / 512KByteW25Q808Mbit / 1MByteW25Q1616Mbit / 2MByteW25Q3232Mbit / 4MByteW25Q6464Mbit / 8MByteW25Q128128Mbit / 16MByteW25Q256256Mbit / 32MByte该芯片使用SPI串行通信根据时钟一位一为位发送并行是一个时钟,8位同时发送5.1 硬件电路5.2 W25Q64框图8MB的存储空间先划分为若干的块BLOCK其中每一块再划分为若干的扇区Sector对于每个扇区内部又可以分成很多页Page。W25Q64的地址宽度是24位---3个字节图右侧地址是 00 00 00 h之后的空间地址依次自增直到最后一个字节地址是7F FF FF h因为24位的地址最大寻址范围是16MB我们这个芯片只有8MB所以地址空间只用了一半。然后整个空间里面我们以64KB作为一个基本单元把它划分位若干的块BLOCK从前往后依次是块0,1等。8*1024/64128所以可以分为128块再对每一块进行细致划分分为扇区Sector每个块起始地址为XX 00 00---XX FF FF一块里面我们再以4KB为一个单元64KB/4KB16所以每一块里面可以分为扇区0---15。写入数据的时候还会有个更细致的划分就是页PAGE页是对整个存储空间划分的也可以看作是扇区里面在进行划分页的大小是256字节一个扇区是4KB以256字节划分4KB*10244096字节4096/25616页一个扇区里面就可以分为16页所以一个扇区里面可以分为16页。SPI控制逻辑他就是整个芯片的管理员执行指令、读写数据都靠它还有一个256字节的页缓存器他其实是一个256字节的RAM存储器数据读写是通过RAM缓存区来进行的写入数据会先放到缓存区里然后在时序结束后芯片再将缓存区的数据复制到对应的FLASH里面进行永久保存因为SPI写入的频率高而FLASH的写入较慢所以该芯片写入的数据先放到页缓存器里面存着因为缓存区是RAM速度比较快可以跟上SPI总线的速度但是缓存区只有256字节写入的一个时序连续写入的数据量不能超过256字节等你写完了芯片在慢慢把数据从缓存区转移到FLASH转移这个过程需要时间所以写入时序结束后芯片会进入一段忙的状态他有一条线通往状态寄存器给状态寄存器的BUSY位置1 表示芯片正在搬砖---忙忙的时候芯片不会再响应新的读写时序了这就是写入的流程。5.3写入操作时1写入操作前必须先进行写使能------保护措施防止操作简称先解锁在操作。2每个数据位只能由1改写为0不能由0改写为1-----FLASH并没有像RAM那样具备直接完全覆盖改写能力例如在某一个字节存储单元里面存储了0XAA这个数据对应的二进制为1010 1010 如果再次在这个存储单元写一个新的数据例如写入0X55RAM可重新覆盖该单元0XAA变成0X55但是FLASH有规定0X55(0101 0101)b由于成本或其它原因此时写入的数据按照规定变成了0X00所以这样就会出问题故引入规定3。3写入数据前必须先擦除擦除后所有数据位变为14擦除必须按最小擦除单元进行FLASH的擦除有最小擦除单元的限制不能指定某一个字节去擦除要擦除就要一大片一起擦除在这里芯片中一大片指的是可选择整个芯片擦除按块/扇区擦除所以最小的一个擦除单元就是一个扇区一个扇区是4KB----4096字节所以擦除最少按照4096个字节一起擦除5连续写入多字节时最多写入一页的数据超过页尾位置的数据会回到页首覆盖写入--------一个写入时序最多只能写入一页的数据(256字节)因为有一个页缓存区他只有256字节因为FLASH的写入太慢了根本上SPI的频率所以写入的数据会先放到RAM里暂存等时序结束后芯片再慢慢把数据写入到FLASH中6写入操作结束后芯片进入忙状态不响应新的读写操作---------因为写入操作都是对缓存区进行的等时序结束后芯片还要搬砖一段时间所以每次写入操作后都有一段时间的忙状态。读取操作时1直接调用读取时序无需使能无需额外操作没有页的限制.2读取操作结束后不会进入忙状态但不能在忙状态时读取--------意思就是读取之后芯片不会忙可立刻开始下一条指令。六、STM32的SPI外设STM32内部集成了硬件SPI收发电路可以由硬件自动执行时钟生成、数据收发等功能减轻CPU的负担---------硬件电路会自动翻转电平不需要我们手动翻转了。1可配置8位/16位数据帧、高位先行/低位先行2时钟频率fPCLK/ (2, 4, 8, 16, 32, 64, 128, 256)一个SCK时钟交换一个bit所以时钟频率一般体现出传输速度单位是hz或bit/sSPI的时钟是由fpclk分频得来的pclk外设时钟APB2的PCLK为72MhzAPB1的PCLK为36Mhz3支持多主机模型、主或从操作4可精简为半双工send;receive在一根线上进行/单工通信只有一根线上,要么进行发;要么收5支持DMA6兼容I2S协议(传输数字音频)7STM32F103C8T6 硬件SPI资源SPI1、SPI2其中SPI1是APB2的外设SPI2是APB1的外设6.1 SPI框图移位寄存器配合数据寄存器实现连续数据流过程比如我们需要连续发送一批数据第一个数据写入TDR当移位寄存器没有数据移位的时候TDR的数据会立刻转入移位寄存器开始移位这个转入时刻会置状态寄存器的TXE为1表示发送寄存器空当我们检查TXE置1后紧跟着下一个数据就可以提前写入到TDR里等候着了一旦上一个数据发完下一个数据就可以立刻跟进实现不间断的连续传输然后移位寄存器这里一旦有数据过来就会自动产生时钟将数据移出去框图中是从MOSI移除移出过程中MISO的数据也会移入数据移除完成数据移入也完成了这时候移入的数据就会从移位寄存器转入到接收缓存区RDR这个时刻会置状态寄存器RXNE1 表示接收寄存器非空但我们检查RXNE置1后就要尽快把数据从RDR读出来在下一个数据到来之前读出RDR就可以实现连续接收否则下一个数据收到了上一个数据还没从RDR出来那么RDR的数据就会被覆盖。简而言之就是发送数据先写入TDR再转到移位寄存器发送发送的同时MOSI接收数据MISO接收到的数据转到RDR我们再从RDR读取数据。框图中的NSS是偏向于用来实现多主机模式SPI框图简化6.2 主模式全双工连续传输连续传输传输更快但是操作复杂非连续传输使用简单会丢失一些性能。框图的SPI模式为3SCK默认高电平第一个下降沿MOSI和MISO移出数据然后上升沿移入数据我们想要发送数据的时候检测到TXE1TDR为空就软件写入0XF1到SPI_DR此时TDR变为0XF1TXE变为0目前移位寄存器为空所以这个0XF1会转入到移位寄存器开始发送波形产生并且置TXE1表示可以把下一个数据放在TDR里面等候了但是这里是非连续传输模式TXE1我们不着急把下一个数据写进去而是等待第一个字节的时序结束---图中第一个时序b7的右侧线表示时序结束表示接受第一个字节完成这时候接收的RXNE会置1等待RXNE置1后先把第一个接收到的数据读出来之后再写入下一个字节数据----软件等待TXE1但是较晚写入0XF2到SPI_DR。步骤为1-----等待TXE为12-----写入发送的数据至TDR3-----等待RXNE为14-----读取RDR接收的数据之后依次重复这几个步骤。非连续传输的缺点就是没有及时把下一个数据放到TDR候着等到第一个字节时序完成后第二个字节还没有送过来数据传输就会等着所以如框图所示时钟和数据的时序在字节与字节之间会产生间隙拖慢了整体传输的速度这个间隙在SCK频率低的时候影响不大SCK频率高的时候间隙拖后腿严重。七、 非连续传输代码流程7.1 硬件SPI1------开启SPI和GPIO的时钟2------初始化GPIO口其中SCK和MOSI是由硬件外设控制的输出信号所以配置为复用推挽输出MISO是硬件外设输入信号我们可以配置为上拉输入SS引脚是软件控制的输出引脚所以配置为通用推挽输出。3------配置SPI外设使用结构体配置参数4------SPI_cmd开关控制#include stm32f10x.h // Device header void MySPI_W_SS(uint8_t BitValue) { GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue); } void MySPI_Init(void) { RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE); 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); 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); 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); 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; SPI_InitStructure.SPI_CPOL SPI_CPOL_Low; SPI_InitStructure.SPI_CPHA SPI_CPHA_1Edge; SPI_InitStructure.SPI_NSS SPI_NSS_Soft; //选择软件SS SPI_InitStructure.SPI_CRCPolynomial 7; //CRC校验多项式这里任意填我们用不到 SPI_Init(SPI1, SPI_InitStructure); SPI_Cmd(SPI1, ENABLE); //默认不选中从机给SS高电平 MySPI_W_SS(1); } void MySPI_Start(void) { MySPI_W_SS(0); } void MySPI_Stop(void) { MySPI_W_SS(1); } /*SPI外设操作时序完成交换一个字节的流程 调用交换字节的函数硬件的外设就要自动控制SCK、MOSIMISO三个引脚产生时序 注意 这里的硬件SPI必须是发送同时接收要想接收必须先发送因为只有给TDR写东西才会触发时序的生成 如果不发送只调用接收函数时序不会动。 写入DR的时候会顺便执行清除TXE操作 读取DR的时候会顺便执行清除RXNE操作 */ uint8_t MySPI_SwapByte(uint8_t ByteSend) { while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) ! SET);//等待TXE为1 SPI_I2S_SendData(SPI1, ByteSend); /*软件写入到TDRByteSend写入到TDR之后ByteSend自动转入移位寄存器 一旦移位寄存器有数据了时序波形就会自动产生 */ while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) ! SET); //等待RXNE为1表示收到一个字节同时也表示发送时序产生完成了 return SPI_I2S_ReceiveData(SPI1); //读取RDR接收的数据就是置换接收的一个字节 }7.2 软件SPI