一、SPI 通讯基本原理
SPI (Serial Peripheral Interface) 是一种高速、全双工、同步串行通信协议,由摩托罗拉公司开发。它采用主从架构,通常包含一个主设备和一个或多个从设备。
1.1 基本信号线
- SCK(Serial Clock):串行时钟,由主设备提供,控制数据传输速率
- MOSI(Master Output, Slave Input):主设备输出 / 从设备输入
- MISO(Master Input, Slave Output):主设备输入 / 从设备输出
- NSS(Slave Select):从设备选择,主设备通过拉低该线选中对应从设备
1.2 数据传输过程
SPI 数据传输基于移位寄存器,主从设备各有一个移位寄存器,通过 MOSI 和 MISO 线连接形成一个环形。当主设备发送一个字节时,实际上是将移位寄存器中的数据逐位移出,同时从 MISO 线逐位接收从设备的数据。
1.3 时钟极性 (CPOL) 和时钟相位 (CPHA)
SPI 有四种工作模式,由 CPOL 和 CPHA 组合决定:
- CPOL=0:空闲时 SCK 为低电平
- CPOL=1:空闲时 SCK 为高电平
- CPHA=0:数据在 SCK 的第一个边沿采样
- CPHA=1:数据在 SCK 的第二个边沿采样
二、阻塞模式 (轮询模式)
2.1 工作原理
阻塞模式是最简单的 SPI 通讯方式,CPU 不断查询 SPI 状态寄存器中的标志位,直到满足条件后才进行下一步操作。在数据传输过程中,CPU 完全被占用,无法执行其他任务。
2.2 涉及的寄存器
-
SPI_CR1 (SPI 控制寄存器 1)
- SPE 位:SPI 使能位
- MSTR 位:主 / 从模式选择位
- BR 2:0 位:波特率控制位
- CPOL 位:时钟极性位
- CPHA 位:时钟相位位
- DFF 位:数据帧格式位 (8 位 / 16 位)
- LSBFIRST 位:数据传输顺序位
-
SPI_CR2 (SPI 控制寄存器 2)
- SSOE 位:SS 输出使能位
-
SPI_SR (SPI 状态寄存器)
- TXE 位:发送缓冲区空标志
- RXNE 位:接收缓冲区非空标志
- BSY 位:忙标志
-
SPI_DR (SPI 数据寄存器)
- 用于写入要发送的数据和读取接收到的数据
2.3 关键状态位
- TXE=1:发送缓冲区为空,可以写入新数据
- RXNE=1:接收缓冲区有数据,可以读取
- BSY=1:SPI 正在进行通讯,忙状态
2.4 应用示例 (HAL 库)
cpp
#include "stm32h7xx_hal.h"
SPI_HandleTypeDef hspi1;
// SPI1初始化函数
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_LOW;
hspi1.Init.CLKPhase = SPI_PHASE_1EDGE;
hspi1.Init.NSS = SPI_NSS_SOFT;
hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_16;
hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
hspi1.Init.TIMode = SPI_TIMODE_DISABLE;
hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
if (HAL_SPI_Init(&hspi1) != HAL_OK)
{
Error_Handler();
}
}
// 阻塞模式发送一个字节
uint8_t SPI_TransmitByte(uint8_t data)
{
// 等待发送缓冲区为空
while (__HAL_SPI_GET_FLAG(&hspi1, SPI_FLAG_TXE) == RESET);
// 写入数据到数据寄存器
*(__IO uint8_t *)&hspi1.Instance->DR = data;
// 等待接收完成
while (__HAL_SPI_GET_FLAG(&hspi1, SPI_FLAG_RXNE) == RESET);
// 读取接收到的数据
return *(__IO uint8_t *)&hspi1.Instance->DR;
}
// 阻塞模式发送多个字节
void SPI_Transmit(uint8_t *pData, uint16_t Size)
{
uint16_t i;
for (i = 0; i < Size; i++)
{
SPI_TransmitByte(pData[i]);
}
}
// 阻塞模式接收多个字节
void SPI_Receive(uint8_t *pData, uint16_t Size)
{
uint16_t i;
for (i = 0; i < Size; i++)
{
// 发送哑数据以产生时钟
pData[i] = SPI_TransmitByte(0xFF);
}
}
// 主函数中使用示例
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_SPI1_Init();
uint8_t tx_data[] = {0x01, 0x02, 0x03, 0x04};
uint8_t rx_data[4];
while (1)
{
// 拉低NSS选中从设备
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET);
// 发送数据
SPI_Transmit(tx_data, 4);
// 接收数据
SPI_Receive(rx_data, 4);
// 拉高NSS释放从设备
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET);
HAL_Delay(1000);
}
}
三、中断模式
3.1 工作原理
中断模式下,当 SPI 发生特定事件 (如发送缓冲区空、接收缓冲区非空) 时,会触发中断,CPU 在中断服务函数中处理数据传输。这种方式可以让 CPU 在数据传输过程中执行其他任务,提高了 CPU 利用率。
3.2 涉及的寄存器
除了阻塞模式涉及的寄存器外,还涉及:
-
SPI_CR2 (SPI 控制寄存器 2)
- TXEIE 位:发送缓冲区空中断使能位
- RXNEIE 位:接收缓冲区非空中断使能位
- ERRIE 位:错误中断使能位
-
NVIC 相关寄存器
- 用于配置 SPI 中断的优先级和使能
3.3 关键状态位和中断标志
- TXE 中断:当发送缓冲区为空时触发
- RXNE 中断:当接收缓冲区有数据时触发
- OVR 中断:溢出错误中断
- MODF 中断:模式错误中断
3.4 应用示例 (HAL 库)
cpp
#include "stm32h7xx_hal.h"
SPI_HandleTypeDef hspi1;
uint8_t tx_buffer[10];
uint8_t rx_buffer[10];
uint8_t tx_index = 0;
uint8_t rx_index = 0;
uint8_t transfer_complete = 0;
// SPI1初始化函数
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_LOW;
hspi1.Init.CLKPhase = SPI_PHASE_1EDGE;
hspi1.Init.NSS = SPI_NSS_SOFT;
hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_16;
hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
hspi1.Init.TIMode = SPI_TIMODE_DISABLE;
hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
if (HAL_SPI_Init(&hspi1) != HAL_OK)
{
Error_Handler();
}
// 使能SPI接收和发送中断
__HAL_SPI_ENABLE_IT(&hspi1, SPI_IT_RXNE | SPI_IT_TXE);
}
// SPI中断服务函数
void SPI1_IRQHandler(void)
{
HAL_SPI_IRQHandler(&hspi1);
}
// SPI发送完成回调函数
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi)
{
if (hspi->Instance == SPI1)
{
// 发送完成处理
}
}
// SPI接收完成回调函数
void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi)
{
if (hspi->Instance == SPI1)
{
transfer_complete = 1;
}
}
// SPI收发完成回调函数
void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi)
{
if (hspi->Instance == SPI1)
{
transfer_complete = 1;
}
}
// 主函数中使用示例
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_SPI1_Init();
// 初始化发送缓冲区
for (int i = 0; i < 10; i++)
{
tx_buffer[i] = i;
}
while (1)
{
transfer_complete = 0;
// 拉低NSS选中从设备
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET);
// 启动中断模式收发
HAL_SPI_TransmitReceive_IT(&hspi1, tx_buffer, rx_buffer, 10);
// 等待传输完成
while (transfer_complete == 0)
{
// 这里可以执行其他任务
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0);
HAL_Delay(10);
}
// 拉高NSS释放从设备
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET);
HAL_Delay(1000);
}
}
// 在stm32h7xx_it.c中添加中断服务函数
void SPI1_IRQHandler(void)
{
HAL_SPI_IRQHandler(&hspi1);
}
四、DMA 模式
4.1 工作原理
DMA (Direct Memory Access) 模式下,SPI 的数据传输由 DMA 控制器负责,不需要 CPU 干预。CPU 只需配置好 DMA 通道和 SPI 外设,然后启动传输,DMA 控制器会自动完成数据在内存和 SPI 外设之间的传输,传输完成后触发中断通知 CPU。这种方式 CPU 利用率最高,适合高速、大量数据传输。
4.2 涉及的寄存器
除了前面提到的寄存器外,还涉及:
-
SPI_CR2 (SPI 控制寄存器 2)
- TXDMAEN 位:发送 DMA 使能位
- RXDMAEN 位:接收 DMA 使能位
-
DMA 相关寄存器
- DMA_SxCR:DMA 流 x 配置寄存器
- DMA_SxNDTR:DMA 流 x 数据数量寄存器
- DMA_SxPAR:DMA 流 x 外设地址寄存器
- DMA_SxM0AR:DMA 流 x 内存 0 地址寄存器
- DMA_SxM1AR:DMA 流 x 内存 1 地址寄存器
- DMA_LISR 和 DMA_HISR:DMA 中断状态寄存器
- DMA_LIFCR 和 DMA_HIFCR:DMA 中断标志清除寄存器
4.3 关键状态位和中断标志
- DMA_TCIFx:DMA 传输完成中断标志
- DMA_HTIFx:DMA 半传输完成中断标志
- DMA_TEIFx:DMA 传输错误中断标志
4.4 应用示例 (HAL 库)
cpp
#include "stm32h7xx_hal.h"
SPI_HandleTypeDef hspi1;
DMA_HandleTypeDef hdma_spi1_tx;
DMA_HandleTypeDef hdma_spi1_rx;
uint8_t tx_buffer[100];
uint8_t rx_buffer[100];
uint8_t transfer_complete = 0;
// SPI1初始化函数
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_LOW;
hspi1.Init.CLKPhase = SPI_PHASE_1EDGE;
hspi1.Init.NSS = SPI_NSS_SOFT;
hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_2; // 高速传输
hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
hspi1.Init.TIMode = SPI_TIMODE_DISABLE;
hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
if (HAL_SPI_Init(&hspi1) != HAL_OK)
{
Error_Handler();
}
}
// DMA初始化函数
void MX_DMA_Init(void)
{
__HAL_RCC_DMA1_CLK_ENABLE();
// SPI1 TX DMA配置
hdma_spi1_tx.Instance = DMA1_Stream3;
hdma_spi1_tx.Init.Request = DMA_REQUEST_SPI1_TX;
hdma_spi1_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
hdma_spi1_tx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_spi1_tx.Init.MemInc = DMA_MINC_ENABLE;
hdma_spi1_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_spi1_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_spi1_tx.Init.Mode = DMA_NORMAL;
hdma_spi1_tx.Init.Priority = DMA_PRIORITY_HIGH;
hdma_spi1_tx.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
if (HAL_DMA_Init(&hdma_spi1_tx) != HAL_OK)
{
Error_Handler();
}
__HAL_LINKDMA(&hspi1, hdmatx, hdma_spi1_tx);
// SPI1 RX DMA配置
hdma_spi1_rx.Instance = DMA1_Stream2;
hdma_spi1_rx.Init.Request = DMA_REQUEST_SPI1_RX;
hdma_spi1_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
hdma_spi1_rx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_spi1_rx.Init.MemInc = DMA_MINC_ENABLE;
hdma_spi1_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_spi1_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_spi1_rx.Init.Mode = DMA_NORMAL;
hdma_spi1_rx.Init.Priority = DMA_PRIORITY_HIGH;
hdma_spi1_rx.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
if (HAL_DMA_Init(&hdma_spi1_rx) != HAL_OK)
{
Error_Handler();
}
__HAL_LINKDMA(&hspi1, hdmarx, hdma_spi1_rx);
// DMA中断配置
HAL_NVIC_SetPriority(DMA1_Stream2_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(DMA1_Stream2_IRQn);
HAL_NVIC_SetPriority(DMA1_Stream3_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(DMA1_Stream3_IRQn);
}
// SPI DMA收发完成回调函数
void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi)
{
if (hspi->Instance == SPI1)
{
transfer_complete = 1;
}
}
// 主函数中使用示例
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_DMA_Init();
MX_SPI1_Init();
// 初始化发送缓冲区
for (int i = 0; i < 100; i++)
{
tx_buffer[i] = i;
}
while (1)
{
transfer_complete = 0;
// 拉低NSS选中从设备
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET);
// 启动DMA模式收发
HAL_SPI_TransmitReceive_DMA(&hspi1, tx_buffer, rx_buffer, 100);
// 等待传输完成
while (transfer_complete == 0)
{
// 这里可以执行其他任务
// CPU完全空闲,可以处理其他复杂任务
process_other_tasks();
}
// 拉高NSS释放从设备
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET);
// 处理接收到的数据
process_received_data(rx_buffer, 100);
HAL_Delay(100);
}
}
// 在stm32h7xx_it.c中添加DMA中断服务函数
void DMA1_Stream2_IRQHandler(void)
{
HAL_DMA_IRQHandler(&hdma_spi1_rx);
}
void DMA1_Stream3_IRQHandler(void)
{
HAL_DMA_IRQHandler(&hdma_spi1_tx);
}
五、三种模式对比与选择建议
| 特性 | 阻塞模式 | 中断模式 | DMA 模式 |
|---|---|---|---|
| CPU 利用率 | 低 (100% 占用) | 中 (仅中断时占用) | 高 (几乎不占用) |
| 实现复杂度 | 最简单 | 中等 | 最复杂 |
| 传输速度 | 低 | 中 | 最高 |
| 适合数据量 | 小量数据 | 中等数据量 | 大量数据 |
| 实时性 | 差 | 好 | 最好 |
| 资源消耗 | 最少 | 中等 | 最多 (占用 DMA 通道) |
选择建议:
- 阻塞模式:适合简单应用、数据量小、对实时性要求不高的场景,如读取传感器的单个字节数据。
- 中断模式:适合中等数据量、对实时性有一定要求的场景,如与显示屏的通讯。
- DMA 模式:适合高速、大量数据传输的场景,如与 SD 卡、Flash 存储器的通讯,或者需要同时处理多个任务的系统。