SPI 通信
# 硬件基础
# 概览
SPI(Serial Peripheral Interface),是一种“一主多从”的同步通信方式。它的方式是“全速高双工”(允许通信双方同时收发信息)速度非常快,SPI 接口的读写操作,都是由主设备发起。当存在多个从设备时,通过各自的片选信号进行管理
它的接线除了时钟线与两根数据线之外,还需要每个设备的自己的 SS 线
# 信号线
- SCLK:串行时钟信号,由主设备产生
- MISO:主设备输入 / 从设备输出引脚。该引脚在从模式下发送数据,在主模式下接收数据
- MOSI:主设备输出 / 从设备输入引脚。该引脚在主模式下发送数据,在从模式下接收数据
- CS/SS:从设备片选信号,由主设备控制。它的功能是用来作为“片选引脚”,也就是选择指定的从设备,让主设备可以单独地与特定从设备通讯,避免数据线上的冲突
# 协议基础
# SPI 设备选择
在 SPI 协议中有主机与从机的区别,当主机想要与某个从机进行通信时,它会将从机的 SS 线电平拉低,表示我们两个要谈谈了。随后将会在时钟线 SCK 上输出时钟信号,为整个传输提供时钟支持
随后,主设备、从设备将通过两根数据线进行通讯
# SPI 数据发送
传输细节
SPI 主机和从机都有一个串行移位寄存器,主机通过向它的 SPI 串行寄存器写入一个字节来发起一次传输:
- 首先拉低对应 SS 信号线,表示与该设备进行通信
- 主机通过发送 SCLK 时钟信号,来告诉从机写数据或者读数据。这里要注意,SCLK 时钟信号可能是低电平有效,也可能是高电平有效,因为 SPI 有四种模式,这个在下面会介绍
- 主机 (Master) 将要发送的数据写到发送数据缓存区 (Memory),缓存区经过移位寄存器 (0~7),串行移位寄存器通过 MOSI 信号线将字节一位一位的移出去传送给从机,同时 MISO 接口接收到的数据经过移位寄存器一位一位的移到接收缓存区
- 从机 (Slave) 也将自己的串行移位寄存器 (0~7) 中的内容通过 MISO 信号线返回给主机。同时通过 MOSI 信号线接收主机发送的数据,这样,两个移位寄存器中的内容就被交换。
也就是讲,在 SPI 通讯中,没有特定的去读与写的操作,而是主机与从机之间交换数据的过程,但是主机占的主导地位。要想读到从机的数据,主机就必须发送一些数据来进行交换。也就是说,你发一个数据必然会收到一个数据;你要收一个数据必须也要先发一个数据
# SPI 四种模式
其实四种模式完全由两个参数决定:
# 时钟极性(CPOL)
- 当 CPOL = 1,此时表示时钟信号为高电平时为有效状态,时钟信号低电平时处于空闲态
- 当 CPOL = 0,此时表示时钟信号为低电平时为有效信号,时钟信号高电平时处于空闲态
# 时钟相位(CPHA)
- 当 CPHA = 0,此时表示将会在时钟信号的第奇数次跳变沿进行数据采集,相应的在第偶数次进行数据发送
- 当 CPHA = 1,此时表示将会在时钟信号的第偶数次跳变沿进行数据采集,相应的在第奇数次进行数据发送
这两个参数的不同组合直接组成了四种不同的 SPI 模式
注意
主设备与从设备必须使用相同的工作模式才可以进行数据交换
如果有多个从设备,并且它们使用了不同的工作模式,那么主设备必须在读写不同从设备时需要重新修改对应从设备的模式,或者使用不同的 SPI 接口
# 优缺点
优点:
他没有规定最大传输速率,没有地址方案,也没规定通信应答机制,没有规定流控制规则
缺点
没有指定的流控制,没有应答机制确认是否接收到数据
# 配置 CubeMX
# SPI 使能页面
点击 SPI 页面,首先出现的两个框就是设置 SPI 工作模式与是否使用硬件自带的 NSS 接线的选项:
模式设置 :
- 有主机模式全双工 / 半双工
- 从机模式全双工 / 半双工
- 只接收主机模式 / 只接收从机模式
- 只发送主机模式
关于片选NSS引脚
我们知道主机拉低从机的 SS 线电平代表开始通信。而 STM32 天生的提供了两种方式:硬件 NSS 片选信号与自己接 SS 线。
当然,如果你选择自己接 SS 线的话,当然要在写代码的时候在收发程序之前拉低、抬高这条线的电平(GPIO 正常推挽输出就行)
# 详细设置
详细设置页的几种配置如下:
# 程序
# 库函数们
/* I/O operation functions */
HAL_StatusTypeDef HAL_SPI_Transmit(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout);
HAL_StatusTypeDef HAL_SPI_Receive(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout);
HAL_StatusTypeDef HAL_SPI_TransmitReceive(SPI_HandleTypeDef *hspi, uint8_t *pTxData, uint8_t *pRxData, uint16_t Size, uint32_t Timeout);
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);
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);
HAL_StatusTypeDef HAL_SPI_DMAPause(SPI_HandleTypeDef *hspi);
HAL_StatusTypeDef HAL_SPI_DMAResume(SPI_HandleTypeDef *hspi);
HAL_StatusTypeDef HAL_SPI_DMAStop(SPI_HandleTypeDef *hspi);
/* Transfer Abort functions */
HAL_StatusTypeDef HAL_SPI_Abort(SPI_HandleTypeDef *hspi);
HAL_StatusTypeDef HAL_SPI_Abort_IT(SPI_HandleTypeDef *hspi);
/* Call back function */
void HAL_SPI_IRQHandler(SPI_HandleTypeDef *hspi);
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi);
void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi);
void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi);
void HAL_SPI_TxHalfCpltCallback(SPI_HandleTypeDef *hspi);
void HAL_SPI_RxHalfCpltCallback(SPI_HandleTypeDef *hspi);
void HAL_SPI_TxRxHalfCpltCallback(SPI_HandleTypeDef *hspi);
void HAL_SPI_ErrorCallback(SPI_HandleTypeDef *hspi);
void HAL_SPI_AbortCpltCallback(SPI_HandleTypeDef *hspi);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
关于函数的使用、参数的意义请参考库函数手册。
关于情况下对于函数类型的使用,请参考串口的使用方法
但是有一个函数 HAL_SPI_TransmitReceive()
是在一边发送一边接收数据,所以你要准备好你的两个数据缓存区
# 程序流程
流程大致如上。
技巧性的代码大概就是将拉低与抬高 SS 线电平的函数使用宏代替以提高可读性:
// 例如 GPIOB13 为某个设备的 SPI_CS/SS
#define DEVICE_SPI_ENABLE() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_13, GPIO_PIN_RESET)
#define DEVICE_SPI_DISABLE() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_13, GPIO_PIN_SET)
2
3
# 参考代码
以下代码来自于 ST 给出的示例代码
别管这个怎么写的,你的 SS 线改拉低还是要拉低
/********* 定义数据缓存区 **********/
/* Buffer used for transmission */
uint8_t aTxBuffer[] = "****SPI - Two Boards communication based on Polling **** SPI Message ******** SPI Message ******** SPI Message ****";
/* Buffer used for reception */
uint8_t aRxBuffer[BUFFERSIZE];
2
3
4
5
6
Polling模式发送接收
/*##-2- Start the Full Duplex Communication process ########################*/
/* While the SPI in TransmitReceive process, user can transmit data through
"aTxBuffer" buffer & receive data through "aRxBuffer" */
/* Timeout is set to 5s */
switch(HAL_SPI_TransmitReceive(&SpiHandle, (uint8_t*)aTxBuffer, (uint8_t*)aRxBuffer, BUFFERSIZE, 5000))
{
case HAL_OK:
/* Communication is completed_____________________________________________*/
/* Compare the sent and received buffers */
if(Buffercmp((uint8_t*)aTxBuffer, (uint8_t*)aRxBuffer, BUFFERSIZE))
{
/* Transfer error in transmission process */
Error_Handler();
}
/* Turn LED4 on: Transfer in transmission process is correct */
BSP_LED_On(LED4);
/* Turn LED6 on: Transfer in reception process is correct */
BSP_LED_On(LED6);
break;
case HAL_TIMEOUT:
/* A Timeout occurred______________________________________________________*/
/* Call Timeout Handler */
Timeout_Error_Handler();
break;
/* An Error occurred_______________________________________________________*/
case HAL_ERROR:
/* Call Timeout Handler */
Error_Handler();
break;
default:
break;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
这段代码真的很好!不管是正常发送、超时还是错误都是有指示灯在提醒,且有相应的函数去处理错误的发生
而Buffercmp()
是在比较发送与接收到的两个数据是否相同,因为这个示例是在一个板子自己通讯
中断模式收发数据
// 传输数据开始
if(HAL_SPI_TransmitReceive_IT(&SpiHandle, (uint8_t*)aTxBuffer, (uint8_t *)aRxBuffer, BUFFERSIZE) != HAL_OK)
{
/* Transfer error in transmission process */
Error_Handler();
}
// 等待传输完成
/* Before starting a new communication transfer, you need to check the current
state of the peripheral; if it抯 busy you need to wait for the end of current
transfer before starting a new one.
For simplicity reasons, this example is just waiting till the end of the
transfer, but application may perform other tasks while transfer operation
is ongoing. */
while (HAL_SPI_GetState(&SpiHandle) != HAL_SPI_STATE_READY)
{
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
DMA模式收发数据
// 传输数据开始
if(HAL_SPI_TransmitReceive_DMA(&SpiHandle, (uint8_t*)aTxBuffer, (uint8_t *)aRxBuffer, BUFFERSIZE) != HAL_OK)
{
/* Transfer error in transmission process */
Error_Handler();
}
// 等待传输完成
/* Before starting a new communication transfer, you need to check the current
state of the peripheral; if it抯 busy you need to wait for the end of current
transfer before starting a new one.
For simplicity reasons, this example is just waiting till the end of the
transfer, but application may perform other tasks while transfer operation
is ongoing. */
while (HAL_SPI_GetState(&SpiHandle) != HAL_SPI_STATE_READY)
{
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
你真的应该多看看源码,因为他们写的代码真的很好
# 参考
[1] SPI 原理超详细讲解:https://blog.csdn.net/as480133937/article/details/105764119 (opens new window)
[2] Robomaster 开发板 C 型嵌入式教程文档