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框图
- 首先左上角核心部分就这个移位寄存器,右边的数据低位,一位一位的从mosi移出去,然后miso的数据一位一位的移入到左边的数据高位,显然移位寄存器应该是一个右移的状态,所以目前图上表示的是低位先行的配置,对应右下角有一个LSBFIRST的控制位,这一位可以控制是低位先行还是高位先行,手册里寄存器描述可以查一下,这里LSBFIRST帧格式,给0先发送msb,msb就是高位的意思,给1先发送lsb ,lsb就是低位的意思,那ppt这里目前的状态LSBFIRST的应该是1,低位先行,如果LSBFIRST给零高位先行的话,这个图还要变动一下,就是移位寄存器变为左移,输出,从左边移出去,输入,从右边移进来,这样才符合逻辑,然后继续看左边这一块,这里画了个方框,里面把mosi和miso做了个交叉,这一块主要是用来进行主从模式引脚变换的,我们这个spi外设可以做主机,也可以做从机,做主机时这个交叉就不用,mosi为mo,主机输出,miso为mi,主机输入,这是主机的情况,如果我们stm32作为从机的话,mosi为si,从机输入,这时他就要走交叉的这一路,输入到移位寄存器,同理miso为so,从机输出,这时输出的数据也走交叉的这一路输出到miso。
- 接下来上下两个缓冲区,就还是我们熟悉的设计,这两个缓冲区实际上就是数据寄存器DR,下面发送缓冲区就是发送数据寄存器TDR,上面接收缓冲区就是接收数据寄存器RDR,和串口那里一样,TDR和RDR占用同一个地址,统一叫做DR,写入DR时数据从这里写入到TDR,读取DR时,数据从这里从RDR读出,数据寄存器和移位寄存器打配合,可以实现连续的数据流,具体流程就是比如我们需要连续发送一批数据,第一个数据写入到TDR,当移位寄存器没有数据移位时,TDR的数据会立刻转入移位寄存器,开始移位,这个转入时刻,会置状态寄存器的TXE为1,表示发送寄存器空,当我们检查TXE置1后,紧跟着下一个数据就可以提前写入到TDR里侯着了,一旦上个数据发完,下一个数据就可以立刻跟进,实现不间断的连续传输,然后移位寄存器,这里一旦有数据过来了,它就会自动产生时钟将数据移出去,在移出的过程中,miso的数据也会移入,一旦数据移出完成,数据移入是不是也完成了,这时移入的数据就会整体的从移位计算器转入到接收缓冲区RDR,这个时刻会置状态寄存器器的RXNE为1,表示接收计寄存器器非空,当我们检查RXNE置1后,就要尽快把数据从RDR读出来,在下一个数据到来之前,读出RDR就可以实现连续接收,否则如果下一个数据已经收到了,上个数据还没从RDR读出来,那RDR的数据就会被覆盖,就不能实现连续的数据流了。和之前串口、I2C的都差不多的,当然这三者也是有一些区别的,比如这里spi全双工发送和接收同步进行,所以它的数据寄存器发送和接收是分离的,而移位寄存器发送和接收可以共用;然后看一下前面I2C的框图,因为I2C是半双工,发送和接收不会同时进行,所以它的数据寄存器和移位寄存器,发送和接收都可以是共用的;串口是全双工,并且发送和接收可以异步进行,所以这就要求它的数据寄存器,发送和接收是分离的,移位寄存器发送和接收也得是分离的。
- 接着后面这些通信电路和各种寄存器,都是一些黑盒子电路,如果你要具体研究,可以看一下这些位的寄存器描述,我挑几个重点的讲一下,比如LSBFIRST的刚才说过,决定高位先行还是低位先行,spe是spi使能,就是SPI_Cmd函数配置的位,BR配置波特率,就是sck时钟频率,MSTR(Master),配置主从模式,1是主模式,0是从模式,我们一般用主模式,cpul和cpha,这个之前讲过,用来选择spi的四种模式,然后这里sr状态计算器,最后两个txe发送寄存器空,rxne接收寄存器非空,这两个比较重要,我们发送接收数据的时候需要关注这两位,最后CR2寄存器就是一些使能位了,比如中断使能dma使能等。
-
最后这里还有一个NSS引脚,ss就是从机选择,低电平有效,所以这里前面加了个n,这个nss和我们想象的从机选择可能不太一样,我们想象的应该是用来指定某个从机对吧,但是根据手册里的描述,我也研究了一下,这里的nss设计,可能更偏向于实现这里说的多主机模型,总的来说啊,这个NSS我们并不会用到,SS引脚我们直接使用一个gpio模拟就行,因为ss引脚很简单,就置一个高低电平就行了,而且从机的情况下,ss还会有多个,这里硬件的nss也完成不了我们想要的功能,那这个nss是如何实现多主机切换的功能呢,假如这里有三个stm32设备,我们需要把这三个设备的nss全都连接在一起,首先这个nss可以配置为输出或者输入,当配置为输出时,可以输出电平告诉别的设备,我现在要变为主机,你们其他设备都给我变从机,不要过来捣乱,当配置为输入时,可以接收别设备的信号,当有设备是主机拉低nss后,我就无论如何也变不成主机了,这就是它的作用,然后内部电路的设计,当这里这个ssoe等于1时,nss作为输出引脚,并在当前设备变为主设备时,给nss输出低电平,这个输出的低电平,就是告诉其他设备,我现在是主机了,当主机结束后,ssoe要清0,nss变为输入,这时输入信号就会跑到右边这里,这个数据选择器ssm位决定选择哪一路,当选择上面一路时是硬件nss模式,也就是说这时外部如果输入了低电平,那当前的设备就进入不了主模式了,因为nss低电平肯定是,外部已经有设备进入了主模式,他已经提前告诉我他是主模式了,我就不能再跟大家抢了,当数据选择器选择下面一路时,是软件管理nss输入,nss是1还是2,由这一位SSI来决定,这个就是nss实现多主机的思路,但这个设计是nss作为多从机选择的作用消失了,揪出所有人的小辫子之后,主机发送的数据就只能是广播发送给所有人的,如果想实现指定设备通信,可能还需要再加入寻址机制。
3.SPI基本结构
4.主模式传输
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,就不能实现连续数据流的接收了。
4.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频率非常高时隙拖后腿的现象就比较严重了。
5.软/硬件波形对比
6.常用API
6.1 SPI_I2S_SendData
/*** @brief Transmits a Data through the SPIx/I2Sx peripheral.* @param SPIx: where x can be* - 1, 2 or 3 in SPI mode * - 2 or 3 in I2S mode* @param Data : Data to be transmitted.* @retval None*/
void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data)
功能:通过外设 SPIx 发送一个数据
参数:SPIx:x 可以是 1 或者 2,来选择 SPI 外设Data: 待发送的数据
返回值:无
6.2 SPI_I2S_ReceiveData
/*** @brief Returns the most recent received data by the SPIx/I2Sx peripheral. * @param SPIx: where x can be* - 1, 2 or 3 in SPI mode * - 2 or 3 in I2S mode* @retval The value of the received data.*/
uint16_t SPI_I2S_ReceiveData(SPI_TypeDef* SPIx)
功能:返回通过 SPIx 最近接收的数据
参数:SPIx:x 可以是 1 或者 2,来选择 SPI 外设
返回值:接收到的字
6.3 SPI_I2S_GetFlagStatus
/*** @brief Checks whether the specified SPI/I2S flag is set or not.* @param SPIx: where x can be* - 1, 2 or 3 in SPI mode * - 2 or 3 in I2S mode* @param SPI_I2S_FLAG: specifies the SPI/I2S flag to check. * This parameter can be one of the following values:* @arg SPI_I2S_FLAG_TXE: Transmit buffer empty flag.* @arg SPI_I2S_FLAG_RXNE: Receive buffer not empty flag.* @arg SPI_I2S_FLAG_BSY: Busy flag.* @arg SPI_I2S_FLAG_OVR: Overrun flag.* @arg SPI_FLAG_MODF: Mode Fault flag.* @arg SPI_FLAG_CRCERR: CRC Error flag.* @arg I2S_FLAG_UDR: Underrun Error flag.* @arg I2S_FLAG_CHSIDE: Channel Side flag.* @retval The new state of SPI_I2S_FLAG (SET or RESET).*/
FlagStatus SPI_I2S_GetFlagStatus(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG)
功能:检查指定的 SPI 标志位设置与否
参数:SPIx:x 可以是 1 或者 2,来选择 SPI 外设SPI_I2S_FLAG:待检查的 SPI 标志位
返回值:SPI_FLAG 的新状态(SET 或者 RESET)
6.4 SPI_I2S_ClearFlag
/*** @brief Clears the SPIx CRC Error (CRCERR) flag.* @param SPIx: where x can be* - 1, 2 or 3 in SPI mode * @param SPI_I2S_FLAG: specifies the SPI flag to clear. * This function clears only CRCERR flag.* @note* - OVR (OverRun error) flag is cleared by software sequence: a read * operation to SPI_DR register (SPI_I2S_ReceiveData()) followed by a read * operation to SPI_SR register (SPI_I2S_GetFlagStatus()).* - UDR (UnderRun error) flag is cleared by a read operation to * SPI_SR register (SPI_I2S_GetFlagStatus()).* - MODF (Mode Fault) flag is cleared by software sequence: a read/write * operation to SPI_SR register (SPI_I2S_GetFlagStatus()) followed by a * write operation to SPI_CR1 register (SPI_Cmd() to enable the SPI).* @retval None*/
void SPI_I2S_ClearFlag(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG)
功能:清除 SPIx 的待处理标志位
参数:SPIx:x 可以是 1 或者 2,来选择 SPI 外设SPI_I2S_FLAG:待清除的 SPI 标志位
返回值:无
7.接线图
8.相关代码
8.1 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初始化* 参 数:无* 返 回 值:无* 注意事项:此函数需要用户实现内容,实现SS、SCK、MOSI和MISO引脚的初始化*/
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引脚初始化为推挽输出 SS是软件控制的输出信号 配置通用推挽输出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引脚初始化为复用推挽输出 MOSI和SCK是外设输出的控制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_InitTypeDef SPI_InitStructure;SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_128; //波特率分频,选择128分频SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; //SPI相位,选择第一个时钟边沿采样,极性和相位决定选择SPI模式0 1Edge代表CPHA=0 2Edge代表CPHA=1SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; //SPI极性,选择低极性 这里46和45配合使用模式0 都需要配置低电平SPI_InitStructure.SPI_CRCPolynomial = 7; //CRC多项式,暂时用不到,给默认值7SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; //数据宽度,选择为8位SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; //先行位,选择高位先行 49和48行常用8位高位先行 在SPI外设简介提到SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; //方向,选择2线全双工SPI_InitStructure.SPI_Mode = SPI_Mode_Master; //模式,选择为SPI主模式SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; //NSS,选择由软件控制SPI_Init(SPI1,&SPI_InitStructure);SPI_Cmd(SPI1,ENABLE);MySPI_W_SS(1);
}/*** 函 数: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); //读取接收到的数据并返回
}
8.2 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
8.3 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){}
}