最近使用通信比较多,包含UART,I2C,SPI,DMA等。这篇博客主要是对近期项目使用的SPI进行讲解,并结合普中STM32F407ZET6芯片以及W25Q128芯片和所对应模块电路进行阐述。
主要是对SPI通信的基本原理和流程,W325Q128的原理图以及芯片手册的阅读进行阐述。
SPI通信一般会用到四个接口,它们分别是:MOSI,MISO,CLK,NSS
MOSI
:主输出,从输入Master Output Slave Input ,主设备输出/从设备输入信号,从设备上该信号一般简写为SI。MOSI是主设备的串行数据输出,SI是从设备的串行数据输入,主设备和从设备的这两个信号连接。
MISO
:主输入,从输出。Master Input Slave Output,主设备输入/从设备输出信号,从设备上该信号一般简写为SO。MISO是主设备的串行数据输入,SO是从设备的串行数据输出,主设备和从设备的这两个信号链接。
CLK
:时钟。时钟在SPI通信中是通过主机给予的,从机是没有CLK的。
NSS
:片选。SPI通信中,在一主多从的情况下,一般来说只有片选引脚置低,对应的从机才有效。部分芯片是高电平有效。
一主多从情况的连线图大致如下:
SPI通信在CLK的驱动下进行串行传输,SPI的传输协议定义了SPI的起始信号,结束信号,数据有效性,时钟同步。
SPI每次传输的数据帧长度是8位或者16位。
SPI通信有四种时序模式。四种时序模式和CPOL,CPHA这两个参数有关。前者是时钟极性(Clock Polarity),后者是时钟相位(Clock Phase)。
CPOL:
时钟极性。时钟在空闲状态时的电平。如果CPOL=0,则空闲时,CLK为低电平;如果CPOL=1,则空闲时,CLK为高电平。
CPHA:
时钟相位。如果CPHA=0,则在SCK的第一个边沿对数据采样;如果CPHA-1,则在SCK的第2个边沿数据采样。
所有四种时序模式就可以如下表所示:
时序模式 | CPOL | CPHA | 说明 |
---|---|---|---|
0 | 0 | 0 | 时钟空闲为low,对SCK的第1个跳变边沿数据采样 |
1 | 0 | 1 | 时钟空闲为low,对SCK的第2个跳变边沿数据采样 |
2 | 1 | 0 | 时钟空闲为high,对SCK的第1个跳变边沿数据采样 |
3 | 1 | 1 | 时钟空闲为high,对SCK的第2个跳变边沿数据采样 |
下图为CPHA=0,CPOL=1和CPOL=0情况对应的时序图。看懂这个应该就知道SPI的原理了。
使用SPI通信要特别注意,主从机设备的SPI时序一定要一致。 其实这就类似于主从机数据解析方式,如果我主机按时序模式0发,从机按时序模式3收,那这个从机收到的数据就和主机发送的数据不一致了。这个要特别注意。
SPI传输数据的方式分为三种,阻塞式,中断式,DMA方式。
下面分别简单介绍一下,这篇博客就用最简单的阻塞式发送进行验证。
1.阻塞式
一般阻塞式发送用到的函数为:HAL_SPI_Transmit()
,这个函数表示阻塞式发送一个缓冲区的数据。
完整的定义如下:
HAL_StatusTypeDef HAL_SPI_Transmit(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout)
一般阻塞式接收用到的函数为:HAL_SPI_Receive()
,这个函数表示阻塞式接收指定长度的数据保存到缓冲区。
完整的定义如下:
HAL_StatusTypeDef HAL_SPI_Receive(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout)
发送与接受函数的四个形参含义:
hspi
是SPI的外设对象指针,表明用的是哪一个SPI的指针。
pData
表示发送/接收的数据缓冲区的指针
Size
表示发送/接收的缓冲区数据的长度
Timeout
表示超时等待的时间,单位是系统嘀嗒信号节拍数,默认情况下就是ms
除了上面的单独发送与接受的函数,还有一个集成收发的函数HAL_SPI_TransmitReceive()
,这个函数表示阻塞式同时发送和接受一定长度的数据。
完整的定义如下:
HAL_StatusTypeDef HAL_SPI_TransmitReceive(SPI_HandleTypeDef *hspi, uint8_t *pTxData, uint8_t *pRxData, uint16_t Size,uint32_t Timeout)
这个函数的几个参数含义和上面类似,不过是把收发数据的指针进行了分开,也就是把一个收发指针变成了发送指针与接收指针。
将pData分为pTxData与pRxData,其他的参数我这里就不再解释。
2.中断式
和上面阻塞式对应,中断式发送与接收数据有三个函数,它们分别是:HAL_SPI_Transmit_IT()
,HAL_SPI_Receive_IT()
,HAL_SPI_TransmitReceive_IT()
完整定义如下:
HAL_StatusTypeDef HAL_SPI_Transmit_IT(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size)
HAL_StatusTypeDef HAL_SPI_Receive_IT(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size)
HAL_StatusTypeDef HAL_SPI_TransmitReceive_IT(SPI_HandleTypeDef *hspi, uint8_t *pTxData, uint8_t *pRxData, uint16_t Size)
我们类比阻塞式发送,发现其实它们的参数都是差不多的,只不过少了个超时参数。所以共性的部分我就不解释了。不同的地方在于,函数名字后面多了一个IT,这表示Interrupt(中断)。
这三个函数都是表示以中断方式发送/接受数据,它们执行完成后都会产生中断事件。
中断发送方式会产生SPI_IT_TXE,会调用回调函数:HAL_SPI_TxCpltCallback()
中断接收方式会产生SPI_IT_RXNE,会调用回调函数:HAL_SPI_RxCpltCallback()
中断发送接收方式会产生SPI_IT_TXE、SPI_IT_RXNE,会调用回调函数:HAL_SPI_TxRxCpltCallback()
关于回调函数之类的,其实和我写外部中断篇博客类似,这里也不再阐述。
3.DMA式
DMA发送与接收数据有三个函数,
它们分别是:HAL_SPI_Transmit_DMA()
,HAL_SPI_Receive_DMA()
,HAL_SPI_TransmitReceive_DMA()
完整定义如下:
HAL_StatusTypeDef HAL_SPI_Transmit_DMA(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size)
HAL_StatusTypeDef HAL_SPI_Receive_DMA(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size)
HAL_StatusTypeDef HAL_SPI_TransmitReceive_DMA(SPI_HandleTypeDef *hspi, uint8_t *pTxData, uint8_t *pRxData, uint16_t Size)
DMA也会产生中断,只不过中断有两种,分为传输完成和传输半完成。
DMA方式功能函数 | 函数功能 | DMA流中断事件 | 对应回调函数 |
---|---|---|---|
HAL_SPI_Transmit_DMA | DMA方式发送数据 | DMA传输完成/DMA传输半完成 | HAL_SPI_TxCpltCallback()/ HAL_SPI_TxHalfCpltCallback() |
HAL_SPI_Receive_DMA | DMA方式发送数据 | DMA传输完成/DMA传输半完成 | HAL_SPI_TxCpltCallback()/ HAL_SPI_TxHalfCpltCallback() |
HAL_SPI_TransmitReceive_DMA | DMA方式发送/接收数据 | DMA传输完成/DMA传输半完成 | HAL_SPI_TxRxCpltCallback()/ HAL_SPI_TxRxHalfCpltCallback() |
看了好多STM32的开发板,都是用这个芯片作为SPI通信的示例芯片来做。SPI通信的基本配置以及原理并不难,刚开始学的时候觉得难大概率和使用的芯片进行通讯需要看芯片手册难,这个小节就写一下使用芯片进行SPI通信的部分,详细的芯片手册阅读我后面写一篇贴上来。
我们SPI有数据传输引脚,时钟,还有片选。
一般信号片选信号拉低为有效,这个可以根据芯片手册中的介绍以及时序图看出。
下图是普中F407ZET6开发板W25Q128部分的原理图,我们可以看到片选/CS是低电平有效。上面有一杠的标志,就表示是低电平有效。
关键的部分人话翻译一下:
/CS拉高,表示设备未选中,串行输出脚是高阻态。未选中的时候,芯片啥也不干的时候处于待机状态,除非进行内部擦除、编程或写入状态寄存器循环。
/CS拉低,设备选中。芯片激活状态,程序员可以写控制指令和数据进去。
说白了就是拉低有效,拉高无效。
这篇博客的目的是获取设备ID,所以我们在确定片选拉低之后要找怎么获取设备ID。
查阅数据手册,找到了下图部分信息。
这个表告诉我们我们使用Command指令90h也就是向芯片输入控制字0x90h,后面的dummy
可不是笨蛋的意思哈,在电子行业这个是虚设的意思,也就是无效单位,只是为了填充或满足某些要求或约束,我们一般给00h就行了。
下面这里给提示,我们给完获取设备ID指令后,它会一直发送,直到片选拉高。
写到这里,其实还是云里雾里有点。我为什么发这个指令,芯片就给我发ID数据呢?我怎么知道什么时候芯片发,什么时候我发数据?这就和时序图有关系了。现在来分析一下时序图。
上面这个图是连着的,只不过由于页面宽度的原因它分层了,为了便于查看,我截图稍微整理了一下,这样看起来更加完整。
我们这里是SPI通信,先看片选,再看时钟来确定模式,再看数据传输方向。
首先是片选拉低。我们可以看到在片选拉低的时候才会有数据传输部分,一旦拉高,后面的数据传输都存在了。这说明片选拉低有效(说了很多次了,有点啰嗦)
其次看时钟。这个时序图中CLK有Mode3与Mode0模式,对应的CPOL和CPHA都为0和都为1情况。具体对应关系刚写过了,忘了的话往上翻翻。这个模式主从机要一致,这里W25Q128只支持模式0和模式3,所以芯片手册的时序图里面将它的时钟效果列了出来。
我们可以看到前8个时钟周期是控制指令周期,后面从第九个周期(数字是8,从0开始的)到第32个周期是写地址周期。后面的时钟周期一直到/CS拉高,其实都和主机输入没啥关系了,时序图DI后面都是XXX了,无效数据。
再看数据传输。主机向芯片输入指令的时候,我们看DI线,是数据有效的,DO线高阻态。结合时钟就是前32个时钟周期主机数据过来有效,后面无效。33时钟周期开始,DI的数据都变成了XXXX,也就是无效,这时候DO线的数据有效,表示芯片向主机发送数据。且一直发送,直到片选拉高。
这里不间断传输数据其实芯片手册也有介绍,xxxx can be read continuously。
同时如果我们地址为部分给01,输出的先是设备ID再制造商ID,这里就不过多讨论。
结合原理图,我们进行配置。
开外部晶振,之前我写过,这个不一定要开,只要你想要的频率满足需要的要求,且能通过内部晶振分频获得就可以不开启外部晶振。我一般都是习惯性开启,也不会影响什么。
SPI1在APB2总线上,所以SPI通信的波特率其实是根据PCLK2的频率来算的。
这里个字段含义解释一下:
Mode:
工作模式。STM32作为主机时,一般选权全双工主机,做从机全双工从机。全双工是指使用MISO和MOSI线可以同时接收和发送。还有半双工模式,就是只用一根数据线,可发又可收,但是同一时间只能有一种功能。
Hardware NSS Signal:
硬件NSS信号。三种选项,Disable表示不使用NSS硬件信号;Hardware Nss Input Signal 表示硬件NSS输入信号,SPI 从机使用硬件NSS信号时选择此项。Hardware Nss output Signal示硬件NSS输出信号,SPI主机输出片选时选择这个,但是我们是使用单独的GPIO作为从机的片选信号,所以这里设置为Disable。
Frame Format:
帧格式。有Motorola和TI两个选项。但一般只能选Motorola,这个参数对应控制寄存器SPI_CR2的FRF位。
Data Size:
数据大小。数据帧的位数,可选8/16bits。
First Bit:
首选传输的位。可选MSB First或LSB First。
Prescaler(for Baud Rate):
用于产生波特率的预分频系数。就是拿那个SPI对应的APBx的总线频率除以这个分频系数,就是波特率了。这里是APB2所以是50MHz再除以8分频,所以波特率就是6.25MBits/s。
CPOL和CPHA我就不解释了,一开始讲过了。
CRC Calculation:
CRC(循环冗余校验)。这个主要是传输数据的最后加上一个字节的CRC计算结果,在发生CRC错误的时候可以产生中断。不使用就默认为Disabled
NSS Signal Type:
NSS信号类型。这个参数和上面的Hardware NSS Signal的选择结果决定。当Hardware NSS Signal 为disable时,这个参数的选项就只能是Software,表示用软件产生NSS输出信号,即本示例用PB14输出信号作为从机的片选信号。
工程配置因人而异,这里就给个参考
先贴代码,再详细解释。
uint16_t Flash_ReadIDbywzy(void)
{
uint16_t Temp = 0;
uint8_t byteData=0;
uint8_t commandData=0x90;
HAL_GPIO_WritePin(FLASH_CS_GPIO_Port, FLASH_CS_Pin, GPIO_PIN_RESET);//cs=0
HAL_SPI_Transmit(&hspi1, &commandData, 1, 200);
commandData=0x00;
HAL_SPI_Transmit(&hspi1, &commandData, 1, 200);
HAL_SPI_Transmit(&hspi1, &commandData, 1, 200);
HAL_SPI_Transmit(&hspi1, &commandData, 1, 200);
HAL_SPI_Receive(&hspi1, &byteData, 1, 200);
Temp=byteData;
Temp=Temp<<8;//Manufacturer ID
HAL_SPI_Receive(&hspi1, &byteData, 1, 200);
Temp|=byteData; //Device ID
HAL_GPIO_WritePin(FLASH_CS_GPIO_Port, FLASH_CS_Pin, GPIO_PIN_SET);
return Temp;
}
代码的流程:
首先定义一些接收和发送数据的变量,接着SPI通信流程,片选使能,发送指令,这里发送的是0x90000000,接着开始读数据,一次读一个字节数据,读两次,读完数据再拉高片选结束一次传输,并把接收到的数据返回。
代码验证的话,如果手头没有啥工具(比如USB转TTL,屏幕这种),那就打断点调试看数据吧。
这里断点到第三个的时候,看flashID的变量就可以看到了。其中前两位是Manufacturer ID,后两位是Device ID。
这里单步调试可能有点BUG,第一次执行完break出来,这个flashID的值为FFFF,然后按RST之后再运行到break的断点,它就会有正确的ID了。
这里把核心代码贴了,还有一些细节这里就不写了,比如定义函数,头文件添加,烧录配置什么的,重复性的东西之前都写过,这里就不再赘述了,同样的东西多写无意义。
完整代码以及项目工程我传到博客对应的资源里面。有需要自取。
本篇博客从SPI通信原理到W25Q128芯片手册阅读,时序图阅读,再到STM32CubeMX配置和代码编写,整个项目工程非常完善,形成闭环。写的也非常详细,通俗不通俗我不知道,我觉得还是挺好懂的。
这篇博客写起来确实非常非常费时间,好在收获颇丰。