STM32 单片机 SPI 通讯原理详解

一、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 涉及的寄存器

  1. SPI_CR1 (SPI 控制寄存器 1)

    • SPE 位:SPI 使能位
    • MSTR 位:主 / 从模式选择位
    • BR 2:0 位:波特率控制位
    • CPOL 位:时钟极性位
    • CPHA 位:时钟相位位
    • DFF 位:数据帧格式位 (8 位 / 16 位)
    • LSBFIRST 位:数据传输顺序位
  2. SPI_CR2 (SPI 控制寄存器 2)

    • SSOE 位:SS 输出使能位
  3. SPI_SR (SPI 状态寄存器)

    • TXE 位:发送缓冲区空标志
    • RXNE 位:接收缓冲区非空标志
    • BSY 位:忙标志
  4. 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 涉及的寄存器

除了阻塞模式涉及的寄存器外,还涉及:

  1. SPI_CR2 (SPI 控制寄存器 2)

    • TXEIE 位:发送缓冲区空中断使能位
    • RXNEIE 位:接收缓冲区非空中断使能位
    • ERRIE 位:错误中断使能位
  2. 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 涉及的寄存器

除了前面提到的寄存器外,还涉及:

  1. SPI_CR2 (SPI 控制寄存器 2)

    • TXDMAEN 位:发送 DMA 使能位
    • RXDMAEN 位:接收 DMA 使能位
  2. 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 通道)

选择建议:

  1. 阻塞模式:适合简单应用、数据量小、对实时性要求不高的场景,如读取传感器的单个字节数据。
  2. 中断模式:适合中等数据量、对实时性有一定要求的场景,如与显示屏的通讯。
  3. DMA 模式:适合高速、大量数据传输的场景,如与 SD 卡、Flash 存储器的通讯,或者需要同时处理多个任务的系统。
相关推荐
资深流水灯工程师1 小时前
STM32 USART 通讯原理与三种模式详解
stm32·单片机·嵌入式硬件
EMTime2 小时前
玲珑GUI-工程设置
单片机·mcu·ui·用户界面
不做无法实现的梦~2 小时前
MAVLink 协议教程
linux·stm32·嵌入式硬件·算法
QiLinkOS3 小时前
【用呼吸重构创造价值关系——QiLink生态】
c语言·数据结构·c++·人工智能·单片机·嵌入式硬件·算法
sxstj3 小时前
STM32F103 串口数量 + 对应 GPIO
单片机·嵌入式硬件
嵌入式ZYXC3 小时前
第4章:MCU最小系统设计——从一颗光杆芯片到它能跑起来
stm32·单片机·嵌入式硬件·物联网
czhaii3 小时前
ABB变频器 ACS510 传动故障诊断的故障队列
嵌入式硬件·硬件工程
嵌入式小站3 小时前
STM32 零基础可移植教程 14:ADC 单通道采样,不接电位器也能读电压
chrome·stm32·嵌入式硬件
记帖5 小时前
STM32C542开发(1)----点亮LED
嵌入式硬件·stm32cubemx·stm32cubeide·stm32cubemx2·stm32c542cct6