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 存储器的通讯,或者需要同时处理多个任务的系统。
相关推荐
蓝斯4978 小时前
从.到实现一个Windows上的虚拟hid键盘设备
windows·单片机·计算机外设
熙芯XiChip9 小时前
硬实时与软实时RTOS核心区别
嵌入式硬件
伟大的前程9 小时前
unsigned int 定义的变量 在 电脑的 64 位系统中 和 32 位 cortex-m0 和 8 位的 51 单片机
单片机·嵌入式硬件
小渔村的拉线工10 小时前
1.HPM6E80 解析芯片整体工作原理和存储架构
单片机·嵌入式硬件·mcu·架构·hpm6e80
熙芯XiChip10 小时前
CAN与CANopen的本质区别
单片机
lilihewo11 小时前
Altium Designer 封装库的创建(分享pcb 3D封装库)
嵌入式硬件·51单片机
神罚天下ET12 小时前
典型arm位单片机启动流程(从上电到main.c)
c语言·arm开发·单片机
碧海银沙音频科技研究院13 小时前
蓝牙耳机使用WSDF5361锂保芯片进入船运模式实现方法
嵌入式硬件·算法
Zevalin爱灰灰13 小时前
模拟信号链电路——基于TL431的电压基准电路
单片机·嵌入式硬件
csdn杰哥14 小时前
瑞萨单片机开发教程[二十七] DTC 外部中断触发传输实验
单片机·嵌入式硬件·mongodb·瑞萨·ra6m5