SPI的HAL库全双工收发函数详解

1.剖析SPI全双工函数

HAL_StatusTypeDef HAL_SPI_TransmitReceive_DMA(SPI_HandleTypeDef *hspi, const uint8_t *pTxData, uint8_t *pRxData,uint16_t Size)
{
  uint32_t             tmp_mode;
  HAL_SPI_StateTypeDef tmp_state;

  /* Check rx & tx dma handles */  // 断言检查DMA的句柄配置
  assert_param(IS_SPI_DMA_HANDLE(hspi->hdmarx));
  assert_param(IS_SPI_DMA_HANDLE(hspi->hdmatx));

  /* Check Direction parameter */  // 断言检查当前的方向是否符合配置 全双工(DIRECTION_2LINES)
  assert_param(IS_SPI_DIRECTION_2LINES(hspi->Init.Direction));

  /* Init temporary variables */  // 初始化临时变量来储存当前的SPI模式和状态
  tmp_state           = hspi->State;
  tmp_mode            = hspi->Init.Mode;

  // 检查SPI状态,判断主机为主机 + 双线全双工 + 接收忙的情况下以及处于准备好的状态就不会触发error
  // 硬件上发送完成事件总是先于接收完成事件发生,所以判断是否处于接受忙状态,确保发送处于正常状态
  if (!((tmp_state == HAL_SPI_STATE_READY) ||
        ((tmp_mode == SPI_MODE_MASTER) && (hspi->Init.Direction == SPI_DIRECTION_2LINES) &&
         (tmp_state == HAL_SPI_STATE_BUSY_RX)))) 
  {
    return HAL_BUSY;
  }
  //检查数据缓冲区是否合法,里面必须开辟了地址空间
  if ((pTxData == NULL) || (pRxData == NULL) || (Size == 0U))
  {
    return HAL_ERROR;
  }
  // 对该SPI句柄进行上锁,防止并发访问,导致错误
  /* Process locked */
  __HAL_LOCK(hspi);

  // 如果处于HAL_SPI_STATE_BUSY_RX的状态下后续的回调函数就会不同
  /* Don't overwrite in case of HAL_SPI_STATE_BUSY_RX */
  if (hspi->State != HAL_SPI_STATE_BUSY_RX)
  {
    hspi->State = HAL_SPI_STATE_BUSY_TX_RX;
  }

  /* Set the transaction information */
  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;

  /* Init field not used in handle to zero */
  hspi->RxISR       = NULL;
  hspi->TxISR       = NULL;

// 判断是否开启CRC校验
#if (USE_SPI_CRC != 0U)
  /* Reset CRC Calculation */
  if (hspi->Init.CRCCalculation == SPI_CRCCALCULATION_ENABLE)
  {
    SPI_RESET_CRC(hspi);
  }
#endif /* USE_SPI_CRC */

  /* Check if we are in Rx only or in Rx/Tx Mode and configure the DMA transfer complete callback */
  if (hspi->State == HAL_SPI_STATE_BUSY_RX) // 如果只是处于接收忙状态,那么就会启用DMA接收的回调函数,而非全双工
  {
    /* Set the SPI Rx DMA Half transfer complete callback */
    hspi->hdmarx->XferHalfCpltCallback = SPI_DMAHalfReceiveCplt;
    hspi->hdmarx->XferCpltCallback     = SPI_DMAReceiveCplt;
  }
  else
  {
    /* Set the SPI Tx/Rx DMA Half transfer complete callback */
    hspi->hdmarx->XferHalfCpltCallback = SPI_DMAHalfTransmitReceiveCplt;
    hspi->hdmarx->XferCpltCallback     = SPI_DMATransmitReceiveCplt;
  }

  /* Set the DMA error callback */  // 设置DMA错误回调函数
  hspi->hdmarx->XferErrorCallback = SPI_DMAError;

  /* Set the DMA AbortCpltCallback */
  hspi->hdmarx->XferAbortCallback = NULL;

  /* Enable the Rx DMA Stream/Channel  */// 启用接收DMA的传输,并且从DR寄存器里面接收的数据通过DMA搬运到接收的数据缓冲区
  if (HAL_OK != HAL_DMA_Start_IT(hspi->hdmarx, (uint32_t)&hspi->Instance->DR, (uint32_t)hspi->pRxBuffPtr,
                                 hspi->RxXferCount))   
  {
    /* Update SPI error code */
    SET_BIT(hspi->ErrorCode, HAL_SPI_ERROR_DMA);  // 将错误码的寄存器的标志位置位
    /* Process Unlocked */
    __HAL_UNLOCK(hspi); // 如果 DMA 接收启动失败,则需要解锁句柄以释放资源,并返回错误状态。
    return HAL_ERROR;
  }

  /* Enable Rx DMA Request */ // 通过寄存器置位,在前面开启DMA的传输的基础上,开启DMA接收,没有上面的操作这里就无法执行
  SET_BIT(hspi->Instance->CR2, SPI_CR2_RXDMAEN);

  /* Set the SPI Tx DMA transfer complete callback as NULL because the communication closing
  is performed in DMA reception complete callback  */
  hspi->hdmatx->XferHalfCpltCallback = NULL;
  hspi->hdmatx->XferCpltCallback     = NULL;
  hspi->hdmatx->XferErrorCallback    = NULL;
  hspi->hdmatx->XferAbortCallback    = NULL;

  /* Enable the Tx DMA Stream/Channel  */ // 启动DMA传输,将发送的数据从缓冲区放入 DR寄存器
  if (HAL_OK != HAL_DMA_Start_IT(hspi->hdmatx, (uint32_t)hspi->pTxBuffPtr, (uint32_t)&hspi->Instance->DR,
                                 hspi->TxXferCount))
  {
    /* Update SPI error code */
    SET_BIT(hspi->ErrorCode, HAL_SPI_ERROR_DMA);   // 将错误码的寄存器的标志位置位
    /* Process Unlocked */
    __HAL_UNLOCK(hspi);  // 如果 DMA 接收启动失败,则需要解锁句柄以释放资源,并返回错误状态。
    return HAL_ERROR;
  }

  /* Check if the SPI is already enabled */  // 通过寄存器标志位判断是否使能SPI传输
  if ((hspi->Instance->CR1 & SPI_CR1_SPE) != SPI_CR1_SPE)
  {
    /* Enable SPI peripheral */
    __HAL_SPI_ENABLE(hspi);
  }

  /* Process Unlocked */
  __HAL_UNLOCK(hspi);

  /* Enable the SPI Error Interrupt Bit */
  __HAL_SPI_ENABLE_IT(hspi, (SPI_IT_ERR));  

  /* Enable Tx DMA Request */  // DMA控制器实际上会帮助SPI自动完成发送
  SET_BIT(hspi->Instance->CR2, SPI_CR2_TXDMAEN);  // 使能DMA发送传输的发送请求,在前面DMA发送通道解锁的基础上

  return HAL_OK;
}

1.1.初始临时变量来储存当前的SPI模式和状态

tmp_state           = hspi->State;
tmp_mode            = hspi->Init.Mode;
  • HAL库为 State 定义了一个枚举体方便状态机调用

  • typedef enum
    {
      HAL_SPI_STATE_RESET      = 0x00U,    /*!< Peripheral not Initialized                         */
      HAL_SPI_STATE_READY      = 0x01U,    /*!< Peripheral Initialized and ready for use           */
      HAL_SPI_STATE_BUSY       = 0x02U,    /*!< an internal process is ongoing                     */
      HAL_SPI_STATE_BUSY_TX    = 0x03U,    /*!< Data Transmission process is ongoing               */
      HAL_SPI_STATE_BUSY_RX    = 0x04U,    /*!< Data Reception process is ongoing                  */
      HAL_SPI_STATE_BUSY_TX_RX = 0x05U,    /*!< Data Transmission and Reception process is ongoing */
      HAL_SPI_STATE_ERROR      = 0x06U,    /*!< SPI error state                                    */
      HAL_SPI_STATE_ABORT      = 0x07U     /*!< SPI abort is ongoing                               */
    } HAL_SPI_StateTypeDef;
    
  • 这里第一次调用应该是处于 HAL_SPI_STATE_READY 状态

1.2.判断当前的SPI传输的状态,如果是空闲状态或者接收忙状态都不会触发错误

if (!((tmp_state == HAL_SPI_STATE_READY) ||
    ((tmp_mode == SPI_MODE_MASTER) && (hspi->Init.Direction == SPI_DIRECTION_2LINES) &&
     (tmp_state == HAL_SPI_STATE_BUSY_RX)))) 
{
return HAL_BUSY;
}
  • 这里的条件判断:!(A || B) // 既不是READY,也不是(主机+双线+BUSY_RX)

    • 只有在 A = falseB = false 时,!(A || B) 才为 true,从而触发 if 语句

    • 所以它可以处于 READY 状态或者 主机 + 双线 + BUSY_RX 状态就不会触发错误

    • 如果是空闲状态 READY 后续会自动设置为 HAL_SPI_STATE_BUSY_RX ,代表可以安全重启发送和接收的DMA

      • 设置状态位

        /* Don't overwrite in case of HAL_SPI_STATE_BUSY_RX */
        if (hspi->State != HAL_SPI_STATE_BUSY_RX)
        {
        	hspi->State = HAL_SPI_STATE_BUSY_TX_RX;
        }
        
    • 如果状态为 HAL_SPI_STATE_BUSY_RX 则保持该状态

  • HAL_SPI_STATE_READYHAL_SPI_STATE_BUSY_RX 两个状态位会在后续有不同的处理

1.3.锁住SPI句柄

// 对该SPI句柄进行上锁,防止并发访问,导致错误
/* Process locked */
__HAL_LOCK(hspi);
  • SPI不支持并发的对于多设备的通信,为了防止资源竞争,开始通讯前会将SPI句柄锁住,不会让其他函数抢占句柄

1.4.为SPI的句柄里面的结构体配置相关信息

/* Set the transaction information */
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;

/* Init field not used in handle to zero */
hspi->RxISR       = NULL;
hspi->TxISR       = NULL;
  • 将函数传递的参数配置进入结构体

1.5.通过宏定义配置CRC校验

// 判断是否开启CRC校验
#if (USE_SPI_CRC != 0U)
  /* Reset CRC Calculation */
  if (hspi->Init.CRCCalculation == SPI_CRCCALCULATION_ENABLE)
  {
    SPI_RESET_CRC(hspi);
  }
#endif /* USE_SPI_CRC */

1.6.HAL_SPI_STATE_READYHAL_SPI_STATE_BUSY_RX 的不同配置的回调函数

if (hspi->State == HAL_SPI_STATE_BUSY_RX) // 如果只是处于接收忙状态,那么就会启用DMA接收的回调函数,而非全双工
{
/* Set the SPI Rx DMA Half transfer complete callback */
hspi->hdmarx->XferHalfCpltCallback = SPI_DMAHalfReceiveCplt;
hspi->hdmarx->XferCpltCallback     = SPI_DMAReceiveCplt;
}
else
{
/* Set the SPI Tx/Rx DMA Half transfer complete callback */
hspi->hdmarx->XferHalfCpltCallback = SPI_DMAHalfTransmitReceiveCplt;
hspi->hdmarx->XferCpltCallback     = SPI_DMATransmitReceiveCplt;
}
  • HAL_SPI_STATE_BUSY_RX 状态下不会配置标准的回调函数,此回调函数为

    static void SPI_DMAReceiveCplt(DMA_HandleTypeDef *hdma)
    {
        // 这里只展示部分重要代码
        
          /* Check if we are in Master RX 2 line mode */
        if ((hspi->Init.Direction == SPI_DIRECTION_2LINES) && (hspi->Init.Mode == SPI_MODE_MASTER))
        {
          /* Disable Rx/Tx DMA Request (done by default to handle the case master rx direction 2 lines) */
          CLEAR_BIT(hspi->Instance->CR2, SPI_CR2_TXDMAEN | SPI_CR2_RXDMAEN);
        }
        else
        {
          /* Normal case */
          CLEAR_BIT(hspi->Instance->CR2, SPI_CR2_RXDMAEN);
        }
    
        
        if (SPI_EndRxTransaction(hspi, SPI_DEFAULT_TIMEOUT, tickstart) != HAL_OK)
        {
          hspi->ErrorCode = HAL_SPI_ERROR_FLAG;
        }
        
        hspi->RxXferCount = 0U; 			 // 计数器清零
        hspi->State = HAL_SPI_STATE_READY;   // 设置状态
        
        HAL_SPI_RxCpltCallback(hspi);  // 调用用户回调函数
    }
    
    • 首先判断是不是全双工通讯,且作为主设备,如果是则停止发送(TX)和接收(RX)的DMA请求,如果不是,则仅仅停止接收的DMA请求,SPI_CR2_TXDMAENSPI_CR2_RXDMAEN 代表CR2寄存器里面的两个标志位

    • 在此状态下调用的回调函数会检查是否结束,并且配置了超时的时间,超时后如果仍然没有接收完成会触发错误编码,将计数器清零(表示接收数据完成),并且将状态位从 HAL_SPI_STATE_BUSY_RX 重置为 HAL_SPI_STATE_READY ,确保下一次SPI传输数据的正常。 同时数据接收完成最后会调用用户回调函数,让用户能够第一时间获取数据

    • 通常配置DMA为正常模式,所以这里关闭后,会将DMA的资源进行清理,等待下一次的重启,如果配置循环模式会自动重载

  • HAL_SPI_STATE_READY 情况下调用的回调函数为:

    • static void SPI_DMATransmitReceiveCplt(DMA_HandleTypeDef *hdma)
      {
          // 这里只展示部分重要代码
          
          if (SPI_EndRxTxTransaction(hspi, SPI_DEFAULT_TIMEOUT, tickstart) != HAL_OK)
          {
            SET_BIT(hspi->ErrorCode, HAL_SPI_ERROR_FLAG);
          }
      
          /* Disable Rx/Tx DMA Request */  // 停止DMA的发送和接收请求
          CLEAR_BIT(hspi->Instance->CR2, SPI_CR2_TXDMAEN | SPI_CR2_RXDMAEN);
      
          hspi->TxXferCount = 0U; // 清零计数器
          hspi->RxXferCount = 0U; // 清零计数器
          hspi->State = HAL_SPI_STATE_READY;   // 设置状态标志位
      }
      
      
    • 判断DMA传输是否超时,如果超时会触发错误编码

    • 同时停止发送(TX)和接收(RX)的DMA请求,并且清零发送和接收的计数器,最后将状态位设置为空闲

    • 通常配置DMA为正常模式,所以这里关闭后,会将DMA的资源进行清理,等待下一次的重启,如果配置循环模式会自动重载

  • 回调函数本质是SPI经过DMA传输完毕后进行资源清理的必要步骤,最后也包含了用户回调函数,有助于用户第一时间处理数据

1.7.启动接收DMA传输

/* Enable the Rx DMA Stream/Channel  */// 启用接收DMA的传输,并且从DR寄存器里面接收的数据通过DMA搬运到接收的数据缓冲区
if (HAL_OK != HAL_DMA_Start_IT(hspi->hdmarx, (uint32_t)&hspi->Instance->DR, (uint32_t)hspi->pRxBuffPtr,
                             hspi->RxXferCount))   
{
/* Update SPI error code */
SET_BIT(hspi->ErrorCode, HAL_SPI_ERROR_DMA);  // 将错误码的寄存器的标志位置位
/* Process Unlocked */
__HAL_UNLOCK(hspi); // 如果 DMA 接收启动失败,则需要解锁句柄以释放资源,并返回错误状态。
return HAL_ERROR;
}
  • 启用接收DMA的传输,并且从DR寄存器里面接收的数据通过DMA搬运到接收的数据缓冲区
  • 如果产生错误则将响应的寄存器标志位置位
  • 如果 DMA 接收启动失败,则需要解锁句柄以释放资源,并返回错误状态

1.8. 开启SPI的DMA接收

/* Enable Rx DMA Request */ // 通过寄存器置位,在前面开启DMA的传输的基础上,开启DMA接收,没有上面的操作这里就无法执行
SET_BIT(hspi->Instance->CR2, SPI_CR2_RXDMAEN);

1.9.关闭多余的回调函数

/* Set the SPI Tx DMA transfer complete callback as NULL because the communication closing
is performed in DMA reception complete callback  */
hspi->hdmatx->XferHalfCpltCallback = NULL;
hspi->hdmatx->XferCpltCallback     = NULL;
hspi->hdmatx->XferErrorCallback    = NULL;
hspi->hdmatx->XferAbortCallback    = NULL;
  • HAL库希望在全双工的函数里面调用的回调函数由一个统一的回调函数来处理,所以关闭多余的回调函数

1.10.开启DMA的发送通道,并且使能SPI的DMA传输

/* Enable the Tx DMA Stream/Channel  */ // 启动DMA传输,将发送的数据从缓冲区放入 DR寄存器
if (HAL_OK != HAL_DMA_Start_IT(hspi->hdmatx, (uint32_t)hspi->pTxBuffPtr, (uint32_t)&hspi->Instance->DR,
                             hspi->TxXferCount))
{
/* Update SPI error code */
SET_BIT(hspi->ErrorCode, HAL_SPI_ERROR_DMA);   // 将错误码的寄存器的标志位置位
/* Process Unlocked */
__HAL_UNLOCK(hspi);  // 如果 DMA 接收启动失败,则需要解锁句柄以释放资源,并返回错误状态。
return HAL_ERROR;
}

/* Check if the SPI is already enabled */  // 通过寄存器标志位判断是否使能SPI传输
if ((hspi->Instance->CR1 & SPI_CR1_SPE) != SPI_CR1_SPE)
{
/* Enable SPI peripheral */
__HAL_SPI_ENABLE(hspi);
}
/* Process Unlocked */
__HAL_UNLOCK(hspi);

/* Enable the SPI Error Interrupt Bit */
__HAL_SPI_ENABLE_IT(hspi, (SPI_IT_ERR));  

/* Enable Tx DMA Request */  // DMA控制器实际上会帮助SPI自动完成发送
SET_BIT(hspi->Instance->CR2, SPI_CR2_TXDMAEN);  // 使能DMA发送传输的发送请求,在前面DMA发送通道解锁的基础上
  • 首先开启发送DMA传输,将发送的数据缓冲区地址和需要放入的发送数据寄存器地址配置进去

  • 解锁句柄,避免死锁

  • 然后不是立刻开启SPI的DMA发送,而是进行检查SPI外设是否正常,然后解锁句柄,启用错误中断,最后才是开启SPI的DMA发送

    • 与接收不同,SPI 外设需要主动向 DMA 控制器发出请求,通知其将数据从用户缓冲区写入到 SPI 数据寄存器(DR),如果SPI外设未启用就会导致发送失败
  • SPI外设寄存器

2.函数设计特点

2.1.接收忙状态处理

  • **在SPI全双工通信中:**发送和接收是同步进行的,每个时钟周期同时发送1位和接收1位

  • 接收缓冲区依赖发送时钟:从设备只能在主设备提供的时钟边沿返回数据

  • 发送完成先于接收完成

    • 主设备发送完最后一个字节后,仍需额外时钟周期才能接收完整的响应数据
    • 发送寄存器空标志(TXE)置位 ≠ 接收寄存器非空标志(RXNE)置位
    • 硬件上发送完成事件总是先于接收完成事件发生
  • 发送完成先于接收可能会导致下一次启用的时候出现接收忙的状态,此时HAL库会对两种状态进行处理

    • 第一种:空闲状态(HAL_SPI_STATE_READY)

    • 第二种:接收忙状态(HAL_SPI_STATE_BUSY_RX),此时代表DMA接收还未完成

    • 如果处于第二种,则不会覆盖,保持该状态,如果第一种代表正常状态,会正常进行DMA的发送和接收更新

      READY
      BUSY_RX
      状态检查
      状态?
      设为BUSY_TX_RX
      保持BUSY_RX
      配置标准回调
      配置链式回调-普通接收回调
      启动DMA传输
  • 标准回调函数(SPI_DMATransmitReceiveCplt)中会进行同时停止发送(TX)和接收(RX)的DMA请求,并且清零发送和接收的计数器,最后将状态位设置为空闲,此时SPI的时钟信号断开,不可以接收和发送数据,需要手动开启,重新调用 HAL_SPI_TransmitReceive_DMA 即可

  • 链式回调函数( SPI_DMAReceiveCplt )中,也同样停止发送(TX)和接收(RX)的DMA请求,但是只清零接收的计数器,保留发送的计数器,此时你调用 HAL_SPI_TransmitReceive_DMA 可以重新维持时钟信号保持旧的任务从之前中断的地方继续运行,新的任务和旧的任务是独立的,不会互相影响

    • 所以在 SPI_DMAReceiveCplt 里面的用户回调函数可以放心处理完整的旧数据
    • 但是如果新的DMA接收任务和旧任务,它们的数据缓冲区一致会出现硬件冲突,但是实际上DMA启动函数内置了安全处理,在DMA传输的时候会返回BUSY状态,所以不会造成数据冲突,偶发性的碰上状态忙事件,仍然可以调用全双工函数恢复,并且恢复事件更快

2.2.接收DMA和发送DMA的开启顺序

  • SPI时钟由主机产生,接收需要时钟边沿采样数据,必须等待发送的时钟,所以优先配置接收的DMA,此时只需要等待时钟信号即可捕获数据,所以优先配置好接收DMA能够确保在第一个数据发送时接收DMA已准备好捕获返回数据

  • 如果先启动发送DMA:

    • 数据开始发送,接收端会立即返回数据,但接收DMA未准备,直接导致数据丢失。

剖析HAL库的设计风格可以帮助理解一些底层库对于寄存器的操作

Logo

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

更多推荐