在前面,我们分析了统治板内通信系统“市场”的两个最普遍的通信标准之一:I²C 协议。现在是时候分析另一个参与者了: SPI 协议。
所有 STM32 微控制器都提供至少一个 SPI 接口,允许开发主和从应用程序。CubeHAL 实现了所有必要的东西,可以轻松地对此类外设进行编程。在简要介绍 SPI 规范之后,对 HAL_SPI 模块进行了简要概述。
1、SPI 规范简介
串行外设接口 (SPI) 是关于主控制器(通常通过 MCU 或具有可编程功能的东西实现)和多个从设备之间的串行、同步和全双工通信的规范。SPI 接口的性质允许在同一总线上进行全双工和半双工通信。SPI 规范是一个事实上的标准,它由摩托罗拉在 70 年代末定义,并且仍然被广泛采用作为许多数字 IC 的通信协议。与 I²C 协议不同,SPI 规范不强制给定的消息协议通过其总线,而是仅限于总线信号,使从设备可以完全自由地处理交换消息的结构。
摩托罗拉是半导体行业的先驱公司,多年来一直被拆分为多个子公司。摩托罗拉的半导体部门流入 ON Semiconductor,而微控制器部门则成为 Freescale Semiconductor,于 2015 年被 NXP 收购。
典型的 SPI 总线由四个信号组成,如下图所示,
可以仅用三个 I/O 驱动某些 SPI 设备(在本例中我们谈论 3 线 SPI):
• SCK:该信号 I/O 用于生成时钟,以同步 SPI 总线上的数据传输。它由主设备生成,这意味着在 SPI 总线中,每个传输总是由主设备启动。与 I²C 规格不同,SPI 本质上更快,SPI 时钟速度通常为几 MHz。如今,SPI 设备能够以高达 100MHz 的速率交换数据是很常见的。此外,SPI 协议允许具有不同通信速度的设备在同一总线上共存。
• MOSI:该信号 I/O 的名称代表 Master Output Slave Input,用于将数据从主设备发送到从设备。与 I²C 总线不同,I²C 总线仅使用一根线来双向交换数据,而 SPI 协议定义了两条不同的线来在主设备和从设备之间交换数据。
• MISO:它代表 Master Input Slave Output,它对应于用于将数据从从设备发送到主设备的 I/O 线。
• SSn:它代表 Slave Select,在典型的 SPI 总线中存在“n”条分隔线,用于寻址传输中涉及的特定 SPI 设备。与 I²C 协议不同,SPI 不使用从地址来选择器件,但它要求对置位为 LOW 的物理线路执行此操作以执行选择。在典型的 SPI 总线中,只有一个从器件可以通过置位其 SS 线路为低电平来同时处于活动状态。这就是具有不同通信速度的设备可以共存于同一总线的原因。(主要原因是从属 I/O 是通过三态 I/O 实现的,也就是说,当 SS 线未置为低电平时,它们被置于高阻抗状态(断开)。)
SPI 具有两条独立的数据通信线路 MOSI 和 MISO,本质上允许全双工通信,因为从设备可以向主设备发送数据,同时从主设备接收新数据。在一对一 SPI 总线(只有一个主和一个从)中,可以省略 SS 信号(相应的从机的 I/O 接地),并且 MISO/MOSI 线路融合在一条称为从机输入/从机输出 (SISO) 的线路中。在这种情况下,我们可以谈论 2 线 SPI,即使它本质上是 3 线总线。
总线上的每次传输都是通过根据最大从机频率启用 SCK 线来启动的。一旦时钟线开始产生信号, 主机置位 SS LOW 并且可以开始数据传输。传输通常涉及两个给定字长的寄存器,一个在主机中,一个在从机中。数据通常首先从最高有效位移出,同时将新的最低有效位移入同一个寄存器,这就是来自从机的数据被转移到最低有效位寄存器中。在寄存器的位被移出和移入后,主机和 从机已经交换了数据。如果需要交换更多数据,则重新加载移位寄存器并重复该过程。传输可以继续任意数量的时钟周期。完成后,主机停止切换时钟信号,并且通常会取消选择从机。
上图显示了全双工传输中数据的传输方式,而下图显示了半双工连接中通常的交换方式。
1.1、时钟极性和相位
除了设置总线时钟频率外,主设备和从设备还必须就通过 MOSI 和 MISO 线路交换的数据的时钟极性和相位达成一致。Motorola 的 SPI 规范http://bit.ly/2cc3T3S将这两个设置分别命名为 CPOL 和 CPHA,大多数芯片供应商都采用了该约定。极性和相位的组合通常被称为 SPI 总线模式,通常根据下表进行编号。
最常见的模式是模式 0 和模式 3,但大多数从器件至少支持几种总线模式。时序图如下图所示,
下面进一步描述:
• 在 CPOL=0 时,时钟的基值为零,即活动状态为 1,空闲状态为 0。
— 当 CPHA=0 时,数据在 SCK 上升沿捕获(LOW → HIGH 转换),数据在下降沿输出(HIGH → LOW 时钟转换)。
— 当 CPHA=1 时,数据在 SCK 下降沿捕获,数据在上升沿输出。
• 在 CPOL=1 时,clock 的基值为 1 (CPOL=0 的反转),即活动状态为 0,空闲状态为 1。
— 当 CPHA=0 时,数据在 SCK 下降沿捕获,数据在上升沿输出。
— 当 CPHA=1 时,数据在 SCK 上升沿捕获,数据在下降沿输出。
也就是说, CPHA=0 表示在第一个 clock edge 上采样,而 CPHA=1 表示在第二个 clock edge 上采样,无论该 clock edge 是上升还是下降。请注意,当 CPHA=0 时,数据必须在第一个 clock cycle之前稳定半个周期。
1.2、从机选择信号管理
如前所述,SPI 从设备在总线上没有标识它们的地址,但只要从机选择 (SS) 信号为低电平,它们就会开始与主机交换数据。STM32 微控制器提供两种不同的模式来处理 SS 信号,在 ST 文档中称为 NSS。
• NSS 软件模式:SS 信号由固件驱动,当 MCU 工作在主模式时,任何空闲的 GPIO 都可用于驱动 IC,或者如果 MCU 工作在从模式,则用于检测另一个主站何时开始传输。
• NSS 硬件模式:使用特定的 MCU I/O 来驱动 SS 信号,内部由 SPI 外设管理。根据 NSS 输出配置,有两种配置可供选择:
– 启用 NSS 输出:仅当设备在主模式下运行时,才使用此配置。当主控制器开始通信时,NSS 信号被驱动为低电平,并保持为低电平,直到 SPI 被禁用。需要注意的是,当总线上只有一个 SPI 从设备并且其 SS I/O 连接到 NSS 信号时,此模式适用。此配置不允许多主模式。
– NSS 输出已禁用:此配置允许在主模式下运行的设备具有多主控功能。对于设置为从器件的设备,NSS 引脚充当经典的 NSS 输入:当 NSS 为低电平时选择从器件,当 NSS 为高电平时取消选择。
1.3、SPI TI 模式
STM32 微控制器中的 SPI 外设,在主模式下工作以及 NSS 信号配置为在硬件模式时,支持 TI 模式。在 TI 模式下,无论设置的值如何,clock polarity 和 phase 都被迫符合 Texas Instruments 协议要求。NSS 管理也特定于 TI 协议,这使得 NSS 管理的配置对用户透明。实际上,在 TI 模式下,NSS 信号在每个传输字节的末尾发出脉冲信号(它从 LSB 位的开头从低电平变为高电平,然后在形成下一个传输字节的 MSB 位的开头从高电平变为低电平)。
1.4、STM32 MCU 中 SPI 外设的可用性
根据所使用的系列类型和封装,STM32 微控制器可以提供多达六个独立的 SPI 外设。例如,给定STM32F401RE MCU,我们可以看到 SPI1 外设映射到 PA7、PA6 和 PA5,但 PB5、PB5 和 PB3 也可以用作备用引脚。请注意,SPI1 外设在所有采用 LQFP-64 封装的 STM32 MCU 中使用相同的 I/O 引脚。这是 STM32 微控制器提供的引脚对引脚兼容性的另一个明显示例。现在,我们已准备好了解如何使用 CubeHAL API 对此外设进行编程。
2、HAL_SPI 模块
要对 SPI 外设进行编程,HAL 定义了 C 结构SPI_HandleTypeDef,定义方式如下:
typedef struct __SPI_HandleTypeDef {
SPI_TypeDef *Instance; /* SPI registers base address */
SPI_InitTypeDef Init; /* SPI communication parameters */
uint8_t *pTxBuffPtr; /* Pointer to SPI Tx transfer Buffer */
uint16_t TxXferSize; /* SPI Tx Transfer size */
__IO uint16_t TxXferCount; /* SPI Tx Transfer Counter */
uint8_t *pRxBuffPtr; /* Pointer to SPI Rx transfer Buffer */
uint16_t RxXferSize; /* SPI Rx Transfer size */
__IO uint16_t RxXferCount; /* SPI Rx Transfer Counter */
DMA_HandleTypeDef *hdmatx; /* SPI Tx DMA Handle parameters */
DMA_HandleTypeDef *hdmarx; /* SPI Rx DMA Handle parameters */
HAL_LockTypeDef Lock; /* Locking object */
__IO HAL_SPI_StateTypeDef State; /* SPI communication state */
__IO uint32_t ErrorCode; /* SPI Error code */
} SPI_HandleTypeDef;
• Instance:是指向我们将要使用的 SPI 描述符的指针。例如,SPI1 是第一个 SPI 外设的描述符。
• Init:是用于配置外设的 C 结构SPI_InitTypeDef实例。
• pTxBuffPtr, pRxBuffPtr:指向内部缓冲区的指针,用于临时存储传入和传出 SPI 外设的数据。当 SPI 在中断模式下工作时使用,不应从用户代码中修改。
• hdmatx, hdmarx:指向 SPI 外设在 DMA 模式下工作时使用的 DMA_HandleTypeDef 结构实例的指针。
SPI 外设的设置是使用 C 结构体SPI_InitTypeDef的实例来执行的,该实例定义如下:
typedef struct {
uint32_t Mode; /* Specifies the SPI operating mode. */
uint32_t Direction; /* Specifies the SPI bidirectional mode state. */
uint32_t DataSize; /* Specifies the SPI data size. */
uint32_t CLKPolarity; /* Specifies the serial clock steady state. */
uint32_t CLKPhase; /* Specifies the clock active edge for the bit capture. */
uint32_t NSS; /* Specifies whether the NSS signal is managed by hardware (NSS pin) or by software */
uint32_t BaudRatePrescaler; /* Specifies the Baud Rate prescaler value which will be used to configure the SCK clock. */
uint32_t FirstBit; /* Specifies whether data transfers start from MSB or LSB bit. */
uint32_t TIMode; /* Specifies if the TI mode is enabled or not. */
uint32_t CRCCalculation; /* Specifies if the CRC calculation is enabled or not. */
uint32_t CRCPolynomial; /* Specifies the polynomial used for the CRC calculation. */
} SPI_InitTypeDef;
• Mode:此参数将 SPI 设置为主模式或从模式。它可以采用值 SPI_MODE_MASTER 和 SPI_MODE_SLAVE。
• Direction:指定从机在 4 线(两条单独的输入/输出线)或 3 线(只有一条线用于 I/O)中工作。它可以假设 SPI_DIRECTION_2LINES 的值来配置全双工 4 线模式;该值SPI_DIRECTION_2LINES_RXONLY设置半双工 4 线模式;该值SPI_DIRECTION_1LINE配置半双工 3 线模式。
• DataSize:配置通过 SPI 总线传输的数据的大小,它可以采用值 SPI_DATASIZE_8BIT 和 SPI_DATASIZE_16BIT。
• CLKPolarity:配置 SCK CPOL 设置,并且可以采用值 SPI_POLARITY_LOW(对应于 CPOL=0)和 SPI_POLARITY_HIGH(对应于 CPOL=1)。
• CLKPhase 此相关字段设置 clock phase,它可以采用值 SPI_PHASE_1EDGE (对应于 CPHA=0) 和 SPI_PHASE_2EDGE (对应于 CPHA=1)。
• NSS:此字段处理 NSS I/O 的行为。它可以假定SPI_NSS_SOFT值以在软件模式下配置 NSS 信号;值 SPI_NSS_HARD_INPUT 和 SPI_NSS_HARD_OUTPUT 分别在输入和输出硬件模式下配置 NSS 信号。
• BaudRatePrescaler:它设置 APB 时钟的预分频器,并建立最大 SCK 时钟速度。它可以采用 SPI_BAUDRATEPRESCALER_2、 SPI_BAUDRATEPRESCALER_4、 ...、 SPI_BAUDRATEPRESCALER_256 的值(两者的幂从 2¹ 到 2⁸)。
• FirstBit:指定数据传输顺序,它可以采用 SPI_FIRSTBIT_MSB 和 SPI_FIRSTBIT_LSB 的值。
• TIMode:用于启用/禁用 TI 模式,可以采用 SPI_TIMODE_DISABLE 和 SPI_TIMODE_ENABLE 的值。
• CRCCalculation 和 CRCPolynomial:所有 STM32 微控制器中的 SPI 外设都支持在硬件中生成 CRC。在 Tx 模式下,可以将 CRC 值作为最后一个字节传输,或者可以对最后一个接收的字节执行自动 CRC 错误检查。CRC 值为使用每个位上的奇数可编程多项式计算。这个计算在 CPHA 和 CPOL 配置定义的 sampling clock edge 上处理。计算出的 CRC 值在数据块结束时自动检查,以及由 CPU 或 DMA 管理。当检测到根据接收数据内部计算的 CRC 与发射器发送的 CRC 不匹配时,将设置错误条件。当 SPI 以 DMA 循环模式驱动时,CRC 功能不可用。
要配置 SPI 外设,我们使用函数:
HAL_StatusTypeDef HAL_SPI_Init(SPI_HandleTypeDef *hspi);
它接受指向前面看到的 SPI_HandleTypeDef 结构实例的指针。
2.1 使用 SPI 外设交换消息
配置 SPI 外设后,我们就可以开始与从设备交换数据了。由于 SPI 规范不强制使用给定的通信协议,因此在从机或主模式下使用 SPI 外设时,CubeHAL 例程之间没有区别。唯一的区别在于外设配置,相应地设置 SPI_InitTypeDef 结构的 Mode 参数。
CubeHAL 提供了三种通过 SPI 总线进行通信的方式:轮询、中断和 DMA 模式。
要在轮询模式下向从设备发送一定数量的字节,我们使用函数:
HAL_StatusTypeDef HAL_SPI_Transmit(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout);
如果 SPI 外设配置为在 SPI_DIRECTION_1LINE 或 SPI_DIRECTION_2LINES 模式下工作,则可以使用此功能。
要在轮询模式下接收字节数,我们使用函数:
HAL_StatusTypeDef HAL_SPI_Receive(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout);
此功能可用于所有三种 Direction 模式。
如果 slave 设备支持全双工模式,那么我们可以使用这个功能:
HAL_StatusTypeDef HAL_SPI_TransmitReceive(SPI_HandleTypeDef *hspi, uint8_t *pTxData, uint8_t *pRxData, uint16_t Size, uint32_t Timeout);
它允许在同时接收相同数量的字节的同时传输给定数量的字节。显然,它仅在 SPI Direction 设置为 SPI_DIRECTION_2LINES 时起作用。
为了在中断模式下通过 SPI 交换数据,CubeHAL 提供了以下功能:
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);
在 DMA 模式下通过 SPI 交换数据的 CubeHAL 例程与之前的三个例程相同,只是它们以 _DMA 结尾。
一旦使用基于中断和 DMA 的例程,我们必须准备好在传输结束时收到通知,因为它是异步执行的。这意味着我们需要在 NVIC 级别启用相应的中断,并从 ISR 调用 HAL_SPI_IRQHandler() 函数。我们可以实现六种不同的回调,如下表中所述。
当 SPI 外设配置为 DMA 循环模式时,我们可以使用以下例程来暂停/恢复/中止 DMA 循环传输:
HAL_StatusTypeDef HAL_SPI_DMAPause(SPI_HandleTypeDef *hspi);
HAL_StatusTypeDef HAL_SPI_DMAResume(SPI_HandleTypeDef *hspi);
HAL_StatusTypeDef HAL_SPI_DMAStop(SPI_HandleTypeDef *hspi);
当 SPI 在 DMA 循环模式下工作时,以下限制适用:
• 当 SPI 在接收模式下独占访问时,不能使用 DMA 循环模式;
• 启用 DMA 循环模式时,不管理 CRC 功能
• 当使用 SPI DMA 暂停/停止功能时,我们必须仅在 SPI 回调下使用函数 HAL_SPI_DMAPause()/ HAL_SPI_DMAStop()。
以SPI1读取FLASH芯片W25Q64 ID 为例,
#include "main.h"
#include "spi.h"
#include "usart.h"
#include "gpio.h"
#include "string.h"
#include "stdio.h"
SPI_HandleTypeDef hspi1;
void SystemClock_Config(void);
void MX_SPI1_Init(void);
int main(void)
{
uint8_t W25X_JedecDeviceID = 0X9F; //W25X_JedecDeviceID read command
uint8_t tmp[3];
uint32_t temp;
char str[30];
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART1_UART_Init();
MX_SPI1_Init();
while (1)
{
/* NSS LOW FIRST */
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_0, GPIO_PIN_RESET);
/* READ JEDEC ID */
HAL_SPI_Transmit(&hspi1, &W25X_JedecDeviceID, 1, HAL_MAX_DELAY);
HAL_SPI_Receive(&hspi1, tmp, 3, HAL_MAX_DELAY);
/* NSS HIGH */
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_0, GPIO_PIN_SET);
temp = (tmp[0] << 16) | (tmp[1] << 8) | tmp[2]; //0XEF4017
sprintf(str, "W25X_JedecDeviceID: %x\n", temp);
HAL_UART_Transmit(&huart1, (uint8_t *)str, strlen(str), HAL_MAX_DELAY);
while(1);
}
}
/* SPI1 init function */
void MX_SPI1_Init(void)
{
hspi1.Instance = SPI1;
hspi1.Init.Mode = SPI_MODE_MASTER;
hspi1.Init.Direction = SPI_DIRECTION_2LINES;
hspi1.Init.DataSize = SPI_DATASIZE_8BIT;
hspi1.Init.CLKPolarity = SPI_POLARITY_HIGH;
hspi1.Init.CLKPhase = SPI_PHASE_2EDGE;
hspi1.Init.NSS = SPI_NSS_SOFT;
hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_4;
hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
hspi1.Init.TIMode = SPI_TIMODE_DISABLE;
hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
hspi1.Init.CRCPolynomial = 10;
HAL_SPI_Init(&hspi1);
}
void HAL_SPI_MspInit(SPI_HandleTypeDef* spiHandle)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
if(spiHandle->Instance==SPI1)
{
/* SPI1 clock enable */
__HAL_RCC_SPI1_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_GPIOC_CLK_ENABLE();
/**SPI1 GPIO Configuration
PC0 ------> SPI1_NSS //SOFT NSS
PA5 ------> SPI1_SCK
PA6 ------> SPI1_MISO
PA7 ------> SPI1_MOSI
*/
/*Configure GPIO pin : PC0 */
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);
GPIO_InitStruct.Pin = GPIO_PIN_5|GPIO_PIN_7;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
GPIO_InitStruct.Pin = GPIO_PIN_6;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}
}
在本章中,我们不会分析任何其它具体的例子。在后面我们将使用 SPI 外设对硬连线 TCP/IP 嵌入式以太网控制器进行编程,这允许我们使用板子构建基于 Internet 的应用程序。
2.2、使用 CubeHAL 可达到的最大传输频率
SCK 频率是使用可编程预分频器从 PCLK 频率得出的。此预分频器的范围为 2¹ 到 2⁸。然而,CubeHAL 在驱动外设时增加了不可避免的开销。这也适用于 SPI 的。事实上,使用 CubeHAL 不可能通过不同的 SPI 模式达到所有支持的 SPI 频率。ST已经在 CubeHAL 中清楚地记录了这一点。如果打开 stm32XXxx_hal_spi.c 文件,可以看到 (大约在第 120 行) 两个表,它们报告了给定方向模式(半双工或全双工)以及编程和使用外设的方式(轮询、中断和 DMA)可达到的最大传输频率。
例如,在 STM32F4 MCU 中,如果 SPI 外设在从模式下工作,并且我们在中断模式下使用 CubeHAL 对其进行编程,则可以达到等于 fPCLK/8 的 SCK 频率。
3、使用 CubeMX 配置 SPI 外设
要使用 CubeMX 启用所需的 SPI 外设,我们必须按以下顺序进行。首先,我们需要选择所需的通信,如下图所示。
接下来,我们需要在同一配置视图中指定 NSS 信号的行为。设置这两个参数后,我们可以在 CubeMX Configuration 窗格中配置其他 SPI 设置。