前言

大家好,这里是 Hello_Embed。上一篇我们理解了 SPI 的硬件引脚和时序规则,今天进入实战核心 ——SPI HAL 库编程。和 UART、I2C 一致,SPI 也支持 查询、中断、DMA 三种方式,且核心逻辑相通:“启动传输 + 回调判断完成”。特别注意 SPI 的特性:发送数据时必然接收数据,接收数据前必须先发送数据(即使发送无效数据)。本篇将从函数梳理、CubeMX 配置,到中断 / DMA 底层实现,完整拆解 SPI 编程逻辑。

一、SPI 核心特性与 HAL 函数梳理

首先明确 SPI 的关键特性:全双工传输,“发必收、收先发”。基于此,HAL 库为三种方式提供了对应的收发函数,参数和逻辑高度统一。

编程方式 功能 HAL库函数 关键差异
查询方式 发送 + 接收 HAL_SPI_TransmitReceive(&hspi, pTxData, pRxData, Size, Timeout) Timeout,阻塞等待完成
查询方式 仅发送 HAL_SPI_Transmit(&hspi, pData, Size, Timeout) 同上,接收缓冲区无效
查询方式 仅接收 HAL_SPI_Receive(&hspi, pData, Size, Timeout) 需先发送无效数据(如 0x00)
中断方式 发送 + 接收 HAL_SPI_TransmitReceive_IT(&hspi, pTxData, pRxData, Size) Timeout,非阻塞,靠回调
中断方式 仅发送 HAL_SPI_Transmit_IT(&hspi, pData, Size) 同上,接收相关参数置空
中断方式 仅接收 HAL_SPI_Receive_IT(&hspi, pData, Size) 需先启动发送无效数据的中断
DMA方式 发送 + 接收 HAL_SPI_TransmitReceive_DMA(&hspi, pTxData, pRxData, Size) Timeout,DMA 自动传输
DMA方式 仅发送 HAL_SPI_Transmit_DMA(&hspi, pData, Size) 同上,DMA 仅配置发送通道
DMA方式 仅接收 HAL_SPI_Receive_DMA(&hspi, pData, Size) 需先配置 DMA 发送无效数据
2. 函数参数说明

所有函数的核心参数含义一致,以中断方式的 “发送 + 接收” 为例:

参数 说明
SPI_HandleTypeDef *hspi 指定使用的 SPI 控制器(如 &hspi1,CubeMX 自动生成)
uint8_t *pTxData 发送数据的缓冲区指针(仅发送 / 收发函数有效,接收函数可设为 NULL
uint8_t *pRxData 接收数据的缓冲区指针(仅接收 / 收发函数有效,发送函数可设为 NULL
uint16_t Size 传输数据的长度(字节数,发送和接收长度必须一致,因全双工同步)
uint32_t Timeout 超时时间(仅查询方式有,单位 ms,超时未完成返回错误)
二、CubeMX 配置:SPI1 引脚、DMA 与中断

以 “SPI1 主模式 + 中断 + DMA” 为例,配置步骤如下(从设备为 SPI Flash):

1. 引脚配置

SPI1 核心引脚与片选引脚(CS)配置:

  • PA5(SPI1_SCK):时钟线(主设备输出);
  • PA6(SPI1_MISO):主设备输入线(从设备输出);
  • PA7(SPI1_MOSI):主设备输出线(从设备输入);
  • PB9(CS):片选引脚(通用 GPIO 输出,手动拉低选中从设备);
    引脚配置图:
    请添加图片描述
2. SPI 模式配置
  • 进入 Connectivity → SPI1,选择 Full-Duplex Master(全双工主模式);
  • 配置 SPI 时序(如 CPOL=0、CPHA=0,数据位 = 8 位,MSB 优先),需与从设备匹配;
  • 使能 “Hardware NSS Signal”(若用硬件 CS,也可手动控制 GPIO 作为 CS)。
3. DMA 配置

添加 SPI1 的 DMA 发送和接收通道(减少 CPU 干预):

  • 发送通道:DMA1 Channel 3(SPI1_TX 对应通道,方向 Memory To Peripheral);
  • 接收通道:DMA1 Channel 2(SPI1_RX 对应通道,方向 Peripheral To Memory);
  • 模式均设为 Normal(一次传输完成后停止,需手动重启);
    配置图:
    请添加图片描述
4. 中断使能
  • 进入 NVIC Settings,勾选 SPI1 global interrupt(SPI 中断)和 DMA1 Channel 2/3 interrupt(DMA 中断);
  • 设置中断优先级(确保不低于其他低优先级中断);
    配置图:
    请添加图片描述

配置完成后生成工程,CubeMX 会自动初始化 SPI1、DMA 及中断向量表。

三、中断方式的底层实现:从启动到回调

中断方式的核心是 “启动传输→中断触发→回调置位标志”,我们以最常用的 HAL_SPI_TransmitReceive_IT 为例,拆解底层逻辑。

1. 启动中断:HAL_SPI_TransmitReceive_IT 函数解析

该函数仅 “启动传输并配置中断”,不等待完成,关键代码如下:

/* 1. 配置传输参数:发送/接收缓冲区、长度、计数 */
hspi->ErrorCode   = HAL_SPI_ERROR_NONE;  // 初始化错误码
hspi->pTxBuffPtr  = (const uint8_t *)pTxData;  // 发送缓冲区指针
hspi->TxXferSize  = Size;  // 总发送长度
hspi->TxXferCount = Size;  // 剩余发送计数(初始=总长度)
hspi->pRxBuffPtr  = (uint8_t *)pRxData;  // 接收缓冲区指针
hspi->RxXferSize  = Size;  // 总接收长度
hspi->RxXferCount = Size;  // 剩余接收计数

/* 2. 配置中断服务函数(根据数据位选择 8 位/16 位) */
if (hspi->Init.DataSize > SPI_DATASIZE_8BIT)
{
  hspi->RxISR = SPI_2linesRxISR_16BIT;  // 16位接收中断函数
  hspi->TxISR = SPI_2linesTxISR_16BIT;  // 16位发送中断函数
}
else
{
  hspi->RxISR = SPI_2linesRxISR_8BIT;   // 8位接收中断函数(常用)
  hspi->TxISR = SPI_2linesTxISR_8BIT;   // 8位发送中断函数(常用)
}

/* 3. 使能 SPI 发送/接收中断(TXE 和 RXNE 中断) */
__HAL_SPI_ENABLE_IT(hspi, SPI_IT_TXE | SPI_IT_RXNE | SPI_IT_ERR);

return HAL_OK;  // 立即返回,不等待传输完成

关键说明

  • 2lines 含义:指 SPI 用 MOSI 和 MISO 两条数据线(全双工模式);
  • TxXferCount/RxXferCount:剩余传输计数,每传输 1 字节减 1,减至 0 表示传输完成;
  • 中断使能后,硬件会在 “发送缓冲区空(TXE)” 或 “接收缓冲区非空(RXNE)” 时触发中断。
2. 仅发送 / 仅接收的中断配置差异

以 “仅发送” 函数 HAL_SPI_Transmit_IT 为例,与 “收发” 函数的差异在于 “接收参数置空”:

/* 仅发送时,接收相关参数设为 NULL/0 */
hspi->pRxBuffPtr  = (uint8_t *)NULL;  // 接收缓冲区空
hspi->RxXferSize  = 0U;               // 接收长度 0
hspi->RxXferCount = 0U;
hspi->RxISR       = NULL;             // 接收中断函数空

/* 仅配置发送中断函数(无 2lines,因不涉及接收) */
hspi->TxISR = SPI_TxISR_8BIT;  // 仅发送的中断函数,无 MISO 相关逻辑
3. 中断服务函数:SPI1_IRQHandler 与回调触发

当 SPI 触发中断时,会先进入中断入口函数 SPI1_IRQHandler,再调用 HAL 库的中断处理函数:

/* SPI1 中断入口(CubeMX 自动生成) */
void SPI1_IRQHandler(void)
{
  HAL_SPI_IRQHandler(&hspi1);  // 调用 HAL 库中断处理函数
}

/* HAL 库中断处理核心函数 */
void HAL_SPI_IRQHandler(SPI_HandleTypeDef *hspi)
{
  uint32_t itflag = hspi->Instance->SR;  // 读取 SPI 状态寄存器(SR)

  /* 1. 接收中断(RXNE 标志置位) */
  if ((itflag & SPI_FLAG_RXNE) && (hspi->RxISR != NULL))
  {
    hspi->RxISR(hspi);  // 调用接收中断函数(如 SPI_2linesRxISR_8BIT)
    return;
  }

  /* 2. 发送中断(TXE 标志置位) */
  if ((itflag & SPI_FLAG_TXE) && (hspi->TxISR != NULL))
  {
    hspi->TxISR(hspi);  // 调用发送中断函数(如 SPI_2linesTxISR_8BIT)
    return;
  }
}
4. 数据传输的实际执行:SPI_2linesRxISR_8BIT 解析

以 8 位接收中断函数为例,看看数据如何存入接收缓冲区:

static void SPI_2linesRxISR_8BIT(SPI_HandleTypeDef *hspi)
{
  /* 1. 读取 SPI 数据寄存器(DR)的值,存入接收缓冲区 */
  *hspi->pRxBuffPtr = *((__IO uint8_t *)&hspi->Instance->DR);
  
  /* 2. 接收缓冲区指针后移,剩余接收计数减 1 */
  hspi->pRxBuffPtr++;
  hspi->RxXferCount--;

  /* 3. 判断接收是否完成(剩余计数为 0) */
  if (hspi->RxXferCount == 0U)
  {
    /* 禁用接收和错误中断 */
    __HAL_SPI_DISABLE_IT(hspi, SPI_IT_RXNE | SPI_IT_ERR);

    /* 若发送也完成(收发同步),调用关闭函数并触发回调 */
    if (hspi->TxXferCount == 0U)
    {
      SPI_CloseRxTx_ISR(hspi);  // 关闭收发中断
      HAL_SPI_TxRxCpltCallback(hspi);  // 触发收发完成回调
    }
  }
}

发送中断函数逻辑对称(以 SPI_2linesTxISR_8BIT 为例):

  • 从发送缓冲区读取数据,写入 SPI 数据寄存器(DR);
  • 发送缓冲区指针后移,剩余发送计数减 1;
  • 发送完成后禁用发送中断,若接收也完成,触发 HAL_SPI_TxRxCpltCallback
四、DMA 方式的中断逻辑:批量传输更高效

DMA 方式适用于 “大量数据传输”,核心优势是 “CPU 不参与字节级传输”,仅在 “传输一半” 或 “传输完成” 时触发一次中断(而非每字节一次)。

1. DMA 启动函数:HAL_SPI_TransmitReceive_DMA

该函数会配置 DMA 通道的 “传输完成回调”,关键代码如下:

/* 1. 配置 DMA 发送/接收通道的回调函数 */
hspi->hdmatx->XferCpltCallback = SPI_DMATransmitCplt;    // 发送完成回调
hspi->hdmarx->XferCpltCallback = SPI_DMAReceiveCplt;    // 接收完成回调
hspi->hdmarx->XferHalfCpltCallback = SPI_DMAHalfReceiveCplt;  // 接收一半回调

/* 2. 启动 DMA 接收和发送通道 */
HAL_DMA_Start_IT(hspi->hdmarx, (uint32_t)&hspi->Instance->DR, (uint32_t)pRxData, Size);
HAL_DMA_Start_IT(hspi->hdmatx, (uint32_t)pTxData, (uint32_t)&hspi->Instance->DR, Size);

/* 3. 使能 SPI 的 DMA 模式(TX DMA 和 RX DMA 使能) */
__HAL_SPI_ENABLE_IT(hspi, SPI_IT_TXDMAEN | SPI_IT_RXDMAEN);

return HAL_OK;
2. DMA 中断处理:DMA1_Channel2_IRQHandler

DMA 传输完成后,会触发 DMA 通道的中断,进入入口函数后调用 HAL 库 DMA 处理函数:

/* DMA1 接收通道(Channel 2)中断入口 */
void DMA1_Channel2_IRQHandler(void)
{
  HAL_DMA_IRQHandler(&hdma_spi1_rx);  // 调用 DMA 中断处理函数
}

/* HAL 库 DMA 中断处理核心 */
void HAL_DMA_IRQHandler(DMA_HandleTypeDef *hdma)
{
  /* 1. 传输一半中断(可选,需配置) */
  if (hdma->XferHalfCpltCallback != NULL && (hdma->Instance->CR & DMA_CR_HTIE) && (hdma->Instance->ISR & DMA_ISR_HTIF))
  {
    hdma->XferHalfCpltCallback(hdma);  // 触发传输一半回调
  }

  /* 2. 传输完成中断 */
  if (hdma->XferCpltCallback != NULL && (hdma->Instance->CR & DMA_CR_TCIE) && (hdma->Instance->ISR & DMA_ISR_TCIF))
  {
    hdma->XferCpltCallback(hdma);  // 触发传输完成回调(如 SPI_DMATransmitReceiveCplt)
  }
}
3. DMA 与中断的回调统一

无论 DMA 还是中断方式,最终都会调用相同的 “传输完成回调函数”:

  • 中断方式:HAL_SPI_TxRxCpltCallback(hspi)
  • DMA 方式:SPI_DMATransmitReceiveCplt 内部也会调用 HAL_SPI_TxRxCpltCallback(hspi)
    因此,我们只需在 HAL_SPI_TxRxCpltCallback 中设置 “传输完成标志”,即可统一判断三种方式的完成状态。
五、总结:三种方式的共性与差异
维度 查询方式 中断方式 DMA方式
CPU 占用 高(阻塞等待) 中(仅中断时占用) 低(DMA 自动传输)
适用场景 少量数据、单任务 中量数据、多任务 大量数据(如 Flash 读写)
完成判断 函数返回值 回调函数置位标志 回调函数置位标志
核心优势 逻辑简单 非阻塞,兼顾多任务 高效,解放 CPU
结尾

本篇我们梳理了 SPI HAL 库的三种编程方式,深入解析了中断和 DMA 的底层逻辑 —— 核心都是 “启动传输 + 回调判断完成”,与 UART、I2C 保持一致。
下一篇,我们将深入Flash W25Q64芯片,理解其工作模式,我们会基于寄存器层面结合时序图看看Flash的芯片是如何读数据、擦除、烧写数据的。
Hello_Embed 继续带你从函数解析到实战应用,逐步掌握 SPI 全场景开发,敬请期待~

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐