SPI:串行外围设备接口,主要应用在EEPROM、FLASH、实时时钟、AD转换器、数字信号处理器和数字信号解码器之间。高速全双工,同步通信,只占四根引脚:MISO 主设备数据输入,从设备数据输出;MOSI 主设备数据输出,从设备数据输入;SCLK 时钟信号,由主设备产生;CS 从设备片选信号,由主设备控制。
一、功能
MISO:主设备输入/从设备输出引脚。该引脚在从模式下发送数据,在主模式下接收数据 MOSI:主设备输出/从设备输入引脚。该引脚在主模式下发送数据,在从模式下接收数据 SCK:串口时钟,作为主设备的输出,从设备的输入,主设备的SPI时钟输入到从设备中以保证时钟同步 NSS:从设备选择。这是一个可选的引脚,用来选择主/从设备。它的功能是用来作为“片选引脚”,让主设备可以单独地与特定从设备通讯,避免数据线上的冲突 其实四根引脚作用相对好理解:MISO字面意思就是master input servent out即主机输入,从机输出线,也就说如果咱把MCU配置成主机模式,那么这根引脚就是用来接收其他从机外设的返回信号的;如果把MCU配置成从机模式,那么这根引脚就是MCU给其他主机外设返回数据用的;即找准自己的定位。 当一个SPI设备需要发送广播数据,它必须拉低NSS信号,以通知所有其它的设备它是主设备;如果它不能拉低NSS,这意味着总线上有另外一个主设备在通信,这时将产生一个硬件失败错误(Hard Fault)。
移出的同时移入,写出的同时接收:SPI通信时发生的事情,看一下SPI数据寄存器
SPI的数据寄存器是16位的,我们可以选择8位或16位收发,上图显然是选择了8位进行收发,即每次传递8位数据。咱这MCU的SPI模块里有个DR寄存器,另一边外设的SPI模块里也有个DR寄存器,如果我们MCU当主机,外设当从机,然后主设备向从设备写入数据即发送数据:那么主设备里的DR寄存器从高位开始一位一位的取出(这里以先发高位为例子,向左移,左边为高位,每取出来一个高位,低位就会空出来一位),取出来后送给从设备,从设备则把收到的位放到自己DR寄存器的低位,难道这不会把从设备DR寄存器里面的数据给覆盖掉吗?不会的,因为在接收来自主设备的位数据同时,从设备的DR寄存器也在向左移动(即把自己的高位取出来送到主设备空出来的低位),把自己的低位给空出来,这空出来的低位就是用来接收主设备发过来的数据的。总而言之,主从设备共用一个时钟,在时钟控制下俩设备的DR寄存器都往左移把高位取出来,低位空出来,这就导致主设备向从设备发送(写入)数据时,还会收到从设备发送过来的数据,如果主机想读取从机的DR寄存器,主机需要向从机随便发送一些数据从而使从机把自己的DR数据发给主机。这就导致发送(写入)数据和接收(读取)数据是绑定在一起了。
SPI中断:上面的原理框图中的CR2寄存器控制
从CR2可以看出SPI有:TXEIE(发送寄存器为空,触发中断,即发送完后对应的标志位TXE会置位,从而触发中断),RXNEIE(接收缓冲区非空即在接收数据或者收满了,同样是对应的RXEN置位,触发中断),ERRIR(收发时出现了错误,触发中断好报警),SSOE(多主机模式下用的),TXDMAEN(召唤DMA把其他地方的数据搬运到SPI的发送寄存器),RXDMAEN(召唤DMA及时把SPI的接收寄存器的内容搬运到其他地方去)。 理解一下这个DMA和SPI的配合用法: 为了达到最大通信速度,需要及时往SPI发送缓冲器填数据,同样接收缓冲器中的数据也必须及时读走以防止溢出。为了方便高速率的数据传输,SPI实现了一种采用简单的请求/应答的DMA机制。当SPI_CR2寄存器上的对应使能位被设置时,SPI模块可以发出DMA传输请求。发送缓冲器和接收缓冲器亦有各自的DMA请求。 发送时,在每次TXE被设置为’1’时发出DMA请求(TXE是发送寄存器为空的标志位,说明此时DMA可以搬运数据到发送寄存器了,而TXDMAEN表示允许DMA往SPI搬运),DMA控制器则写数据至SPI_DR寄存器,TXE标志因此而被清除。接收时,在每次RXNE被设置为’1’时发出DMA请求(RXEN是SPI的接受寄存器满了的标志位,说明此时DMA可以搬运SPI的接受寄存器内容到其他地方了),DMA控制器则从SPI_DR寄存器读出数据,RXNE标志因此而被清除。(注:SPI只有一个DR寄存器即接受寄存器和发送寄存器都是指DR,叙述中只是为了方便才这样区分说法) 在发送模式下,当DMA已经传输了所有要发送的数据(DMA_ISR寄存器的TCIF标志变为’1’)后,可以通过监视BSY标志以确认SPI通信结束,这样可以避免在关闭SPI或进入停止模式时,破坏最后一个数据的传输。因此软件需要先等待TXE=1,然后等待BSY=0。
发送缓冲器空闲标志(TXE) :此标志为’1’时表明发送缓冲器为空,可以写下一个待发送的数据进入缓冲器中。当写入SPI_DR时,TXE标志被清除。接收缓冲器非空(RXNE) :此标志为’1’时表明在接收缓冲器中包含有效的接收数据。读SPI数据寄存器可以清除此标志。忙(Busy)标志:BSY标志由硬件设置与清除(写入此位无效果),此标志表明SPI通信层的状态。当它被设置为’1’时,表明SPI正忙于通信。
SPI参数配置:上面的原理框图中的CR1寄存器
这个寄存器功能还挺多的,只简单看看重要的几个吧,以后需要的话再看:DFF(SPI一次发8位还是16位),RXONLY(是否是全双工模式),SSM(是否软件控制NSS位),LSBFIRST(发送是先发高位数据还是低位数据),SPE(SPI使能),BR[2:0](波特率控制,波特率本质由SPI时钟决定,因此这个就是SPI时钟的分频系数),MSTR(配置当前设备为主机还是从机),CPOL(时钟极性,决定着空闲时SCK的高平状态),CPHA(时钟相位,即从第几个时钟边沿开始正式采样(获取)数据)。所以说CR1寄存器决定着:是否打开SPI,是否采用全双工模式,这是主机还是从机,每次发几位数据,先发高位还是低位,传输波特率是多少,时钟空闲时是低电平还是高电平,从第一个还是第二个时钟边沿获取数据等等。
SPI状态反映:原理框图中的SR寄存器
SPI的状态有:BSY(是否处于忙碌状态),OVR(溢出错误),TXE(发送缓冲是否为空),RXEN(接收缓冲是否为空)等等。
时钟信号的相位和极性
SPI_CR寄存器的CPOL和CPHA位,能够组合成四种可能的时序关系。CPOL(时钟极性)位控制在没有数据传输时时钟的空闲状态电平,此位对主模式和从模式下的设备都有效。如果CPOL被 清’0’,SCK引脚在空闲状态保持低电平;如果CPOL被置’1’,SCK引脚在空闲状态保持高电平。 如果CPHA(时钟相位)位被置’1’,SCK时钟的第二个边沿(CPOL位为0时就是下降沿,CPOL位 为’1’时就是上升沿)进行数据位的采样,数据在第二个时钟边沿被锁存。如果CPHA位被清’0’,SCK时钟的第一边沿(CPOL位为’0’时就是下降沿,CPOL位为’1’时就是上升沿)进行数据位采样,数据在第一个时钟边沿被锁存。 例如,对于CPHA=1,CPOL=0 的图,CPOL=0决定着当SPI空闲时SCK线会处于高电平,开始工作时会从高电平跳变到低电平,工作完后又保持高电平;CPHA=1决定着从SCK的第2个边沿信号开始获取数据线上的数据;另外还需注意:SPI是在SCK跳变沿处采集数据线上的电平数据的,即SCK跳变时对应到的数据线上的电平高低就是采集到的数据内容。
二、工作过程
配置成主模式 比如说我们把MCU配置成主机模式:首先要选时钟吧(咱MCU要是主机的话,这个时钟就是MCU自己产生,然后通过这个SCK引脚连到其他从机的SCK引脚,保证时钟同步),选完时钟后要分频吧(即波特率BR[:]位),然后时钟极性要考虑吧,先传高位还是低位,每次传几位要考虑吧,很重要的一点是咱这个设备是配置成主机还是从机呢,配置成主机后怎么选中其他从机呢。 配置成主机了,主机向从机发送数据:一开始待发送的数据是存在数据缓冲区(即数据寄存器中)中,当数据进入移位寄存器开始发送时(即开始移位了,一位一位的移到从机寄存器里去),TXE标志位置位表明发送缓冲区清空了,可以准备下次向缓冲区装载数据了,要是设置了发送中断还是触发中断。什么时候才会发送呢?当你对SPI的DR寄存器写入值,就会自动发送出去了,这一点和USART是一样的。主机接收从机返回的数据或者说主机读取从机中的数据:主机需要先向从机随便发送一些数据,然后检查自己的RXNE位是否置位了,置位就说明从机已经返回数据了,主机此时可以去读取数据寄存器中数据了。
配置成从模式 配置成从机了,那么时钟就不是自己产生了,而是通过SCK引脚接收主机SCK引脚传来的时钟信号,那么从机的波特率配置位(就是时钟分频系数)也没用了,因为波特率已经在主机那边配置过了。那么从机这边的SPI配置主要是配合主机那边的SPI配置,比如从机时钟极性和相位要和主机的一致,数据每次传几位、从高位传还是低位传也和主机一致等等。从机如何发送数据呢?由主机发起,只有当从机的MOSI引脚接收到数据时(即主机向从机发送了数据),从机才会通过自己的MISO引脚向主机发送数据,同样的如果从机把数据都从缓冲区(即数据寄存器)送到了移位寄存器,从机的TXE标志位会置位。从机如何接收数据呢?也是由主机发起,主机会主动向从机传输数据。
缓冲区(即数据寄存器)到移位寄存器是并行的,SPI传输数据是串行的!缓冲区指的是数据寄存器,传输用的寄存器是移位寄存器,存在个缓冲区到移位寄存器的过程。
当写入数据至发送缓冲器时,发送过程就开始了。在接收时,接收到的数据被存放在一个内部的接收缓冲器中;在发送时,在被发送之前,数据将首先被存放在一个内部的发送缓冲器中。对SPI_DR寄存器的读操作,将返回接收缓冲器的内容;写入SPI_DR寄存器的数据将被写入发送缓冲器中:也就是说,如果调用SPI的读取数据函数(这个函数本质不过是把DR寄存器内容给取出来,函数内部其实就是 SPIx->DR; 返回了SPI的DR数据内容),这样居然就能触发使得RXEN复原!!!!
if(主机的接收标志RXEN被置位了)
{
调用读取函数SPI2_ReadWriteByte()来读取DR数据内容;
}
u8 SPI2_ReadWriteByte(u8 TxData)
{
...
...
return SPI_I2S_ReceiveData(SPI2);
}
uint16_t SPI_I2S_ReceiveData(SPI_TypeDef* SPIx)
{
assert_param(IS_SPI_ALL_PERIPH(SPIx));
return SPIx->DR;
}
同样的,如果向DR中写入数据(本质也不过是对DR赋值),就能触发TXE的置位。(这个还能理解,毕竟是修改了DR的值,能检测到,所以能触发TXE置位,但是为啥上面只是对DR读一下都能触发有关寄存器置位呢)
if(TXE == 1)
{
SPI_I2S_SendData(SPI2, TxData);
}
void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data)
{
assert_param(IS_SPI_ALL_PERIPH(SPIx));
SPIx->DR = Data;
}
向SPI的DR中写入数据,就会直接开始SPI传输了!!!一旦传输开始,如果下一个将发送的数据被放进了发送缓冲器,就可以维持一个连续的传输流(软件必须保证在SPI主设备开始数据传输之前在发送寄存器中写入要发送的数据)。在试图写发送缓冲器之前,需确认TXE标志应该为’1’。
主从机区分 NSS:从设备选择。这是一个可选的引脚,用来选择主/从设备。它的功能是用来作为“片选引脚”,让主设备可以单独地与特定从设备通讯,避免数据线上的冲突。从设备的NSS引脚可以由主设备的一个标准I/O引脚来驱动。
硬件NSS模式:NSS输出被使能,当STM32F10xxx工作为主SPI,并且NSS输出已经通过SPI_CR2寄存器的SSOE位使能,这时NSS引脚被拉低,所有NSS引脚与这个主SPI的NSS引脚相连并配置为NSS的SPI设备,将自动变成从SPI设备。当一个SPI设备需要发送广播数据,它必须拉低NSS信号,以通知所有其它的设备它是主设备,此时,所有的SPI设备,如果它们的NSS引脚连接到主设备的NSS引脚,则会检测到低电平,如果它们被设置为NSS硬件模式,就会自动进入从设备状态;如果它不能拉低NSS,这意味着总线上有另外一个主设备在通信,这时将产生一个硬件失败错误(Hard Fault)。当配置为主设备、NSS配置为输入引脚(MSTR=1,SSOE=0)时,如果NSS被拉低,则这个SPI设备进入主模式失败状态:即MSTR位被自动清除,此设备进入从模式。
软件NSS模式:可以通过设置SPI_CR1寄存器的SSM位来使能这种模式(见图211)。在这种模式下NSS引脚可以用作它用,而内部NSS信号电平可以通过写SPI_CR1的SSI位来驱动。也就是软件模式下和硬件模式下功能是一样的,只不过一个通过硬件引脚控制,一个通过软件赋值进行控制:硬件模式下使能CR2中的SSOE位能够使NSS引脚拉低,软件模式下施恩那个CR1中的SSI位能够使NSS引脚拉低。
SPI_NSS 设置 NSS 信号由硬件(NSS 管脚)还是软件控制,这里我们通过软件控制 NSS 引脚电平,而不是硬件自动控制,所以选择 SPI_NSS_Soft。MCU的PB12、13、14、15引脚连接到了W25Qxx设备的四个SPI引脚,不就相当于PB12、13、14、15这四个引脚是MCU的SPI引脚了,把程序烧入到MCU,意味着我们现在只是对MCU进行了SPI配置?对W25Qxx外设配置SPI的话需要再单独烧入程序到W25Qxx中?这也是为什么后面程序中把PB12、13、14、15引脚配置成了主机模式,然后能作为主机去选中W25Qxx设备,主动去写入和读取W25Qxx的数据。这说明MCU是主机,W25Qxx已经是从机了,但在配置程序中我们没有看到将W25Qxx配置成从机的语句,只看到有个配置成主机的语句。也就是说,编写的程序是把MCU作为主机了,并采取了软件NSS模式来选择从机(由于没有对W25Qxx烧录程序即没有配置,默认是配合主机MCU的),接着再配置主机的CR1的SSI位使NSS拉低(只有主机才能把NSS拉低),就把与NSS相连的设备都当作从机了!那么怎么区分主机要找哪个从机呢?毕竟所有从机的CS引脚都连到了主机的NSS引脚,应该是如果从机的CS引脚此时也拉低了,就表示被选中了?“”多从机的SPI正常的应用会有多根SS线(类似片选线),不同的片选连接不同的从机的SS,想要选择哪个从机只要拉低相应的SS线就可以了“ —— 这句话中的拉低是谁拉低?主机拉低自己的NSS,从机也要拉低自己的CS才行,但从机又咋知道啥时候要拉低呢?是不是还需要个设备号呢? NSS引脚告诉你其他外设都是从机(排座次),MUC的PB12连到从机的CS引脚,才决定着要选中哪个外设(常规模式下)。
常规模式:主机里有多个CS引脚,另外三根线可以复用,但CS引脚会变得比较多,不能连太多从机 看到这里,我突然意识到,这里MCU和W25Qxx采用的是常规模式啊!!只不过因为只有一个外设,导致只看到了MCU上的一个CS引脚,如果还有其他外设,只需再从MCU上选出一个引脚比如PB10吧,连到这第二个外设的CS引脚就行了!然后如果MCU主机把PB10拉低就意味着选中了这第二个外设。是自己搞糊涂了啊!
CRC校验 DMA和SPI配合使用的时序图,锻炼看图能力吧,视需要看
W25Q128 是华邦公司推出的大容 量 SPI FLASH 产品,W25Q128 的容量为 128Mb,该系列还有 W25Q80/16/32/64 等。ALIENTEK 所选择的 W25Q128 容量为 128Mb,也就是 16M 字节。W25Q128 将 16M 的容量分为 256 个块(Block),每个块大小为 64K 字节,每个块又分为16 个扇区(Sector),每个扇区 4K 个字节。W25Q128 的最小擦除单位为一个扇区,也就是每次必须擦除 4K 个字节。这样我们需要给 W25Q128 开辟一个至少 4K 的缓存区,这样对 SRAM 要求比较高,要求芯片必须有 4K 以上 SRAM 才能很好的操作。W25Q128 的擦写周期多达 10W 次,具有 20 年的数据保存期限,支持电压为 2.7~3.6V,W25Q128 支持标准的 SPI,还支持双输出/四输出的 SPI,最大 SPI 时钟可以到 80Mhz(双输出时相当于 160Mhz,四输出时相当于 320M),更多的 W25Q128 的介绍,请参考 W25Q128 的DATASHEET。
程序
void SPI2_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
SPI_InitTypeDef SPI_InitStructure;
RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOB, ENABLE );
RCC_APB1PeriphClockCmd( RCC_APB1Periph_SPI2, ENABLE );
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO_SetBits(GPIOB,GPIO_Pin_13|GPIO_Pin_14|GPIO_Pin_15);
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
SPI_InitStructure.SPI_Mode = SPI_Mode_Master;
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;
SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;
SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_256;
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;
SPI_InitStructure.SPI_CRCPolynomial = 7;
SPI_Init(SPI2, &SPI_InitStructure);
SPI_Cmd(SPI2, ENABLE);
SPI2_ReadWriteByte(0xff);
}
void SPI2_SetSpeed(u8 SPI_BaudRatePrescaler)
{
assert_param(IS_SPI_BAUDRATE_PRESCALER(SPI_BaudRatePrescaler));
SPI2->CR1&=0XFFC7;
SPI2->CR1|=SPI_BaudRatePrescaler;
SPI_Cmd(SPI2,ENABLE);
}
void W25QXX_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOB, ENABLE );
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO_SetBits(GPIOB,GPIO_Pin_12);
W25QXX_CS=1;
SPI2_Init();
SPI2_SetSpeed(SPI_BaudRatePrescaler_2);
W25QXX_TYPE=W25QXX_ReadID();
}
|