STM32 SPI DMA主从双机通讯问题总结

SPI主从双机通讯使用如下方案,实现的部分功能:

1)STM32H723将EtherCAT主站的电机指令通过SPI发送至STM32G473;

2)STM32G473将接收到的电机指令通过CAN发送至电机,同时接收电机反馈数据;

3)STM32G473同时通过SPI接收IMU的数据,与电机CAN反馈数据打包一起发送至STM32H723。

为提高效率,SPI 都使用DMA方式传输,调试过程中遇到了一些问题,花了两三天时间,这里记录一下几个主要问题,以方便后续避坑。

1、SPI配置

1.1 STM32H723的SPI配置

作为主机,片选信号单独使用1个GPIO。

 DMA配置,由于Data Size设为16 Bits,此处Data Width选择Half Word。

1.2  STM32G473的SPI配置

 作为从机,选择硬件NSS输入信号,注意Data size和Clock参数与主机保持一致。

DMA配置,发送和接收都一样,此处就放一张图了,注意Data Width和主机保持一致

 IMU的SPI作为主站配置,参数基本一样,就不在上图了。

2、问题总结 

问题1:SPI通讯一段时间就停止了

1)SPI通讯部分代码如下

主机:

主程序以2ms的周期调用MCU_SPI_DMA_CMD()函数进行 SPI通讯。

/**
\brief    This function will called from the synchronisation ISR 
            or from the mainloop if no synchronisation is supported
*
void APPL_Application(void)
{
  uint8_t TxCntStart[canNm] = {0};     //发送CAN数据后开始计数,用于CAN节点数据反馈超时判断
  static uint16_t TxCnt[canNm] = {0};  //计数值
  uint8_t canSts = 0;                  //CAN发送状态
  static uint8_t firstRun = 1;

  /**PDO to CAN**/    
  if(escRxUpdate == 1)
  {
    /**从站接收的数据通过SPI转发到MCU2的CAN*/
    uint16_t  *spiTxData = (uint16_t *)&CAN_OUT0x7000.OutCanMsg9_Typ_St;
    if(canChNm == 4){
       spiTxData = (uint16_t *)&CAN_OUT0x7000.OutCanMsg12_Typ_St;
    }

//    if(mcu2_spiRdy){
    if(firstRun || spiTrCpt){
       MCU_SPI_DMA_CMD(spiTxData,SPI_TR_LENGHT);
       firstRun = 0;
//       mcu2_spiRdy = 0;
    }

  ……

  }
}

 主机SPI DMA传输函数

/**
 * MCU SPI send and receive
 */
//uint8_t mcu2_spiRdy = 1;
uint8_t spiTrCpt = 0;  //SPI2收发完成标志
uint16_t spiRxData[SPI_TR_LENGHT] = {0};
void MCU_SPI_DMA_CMD(uint16_t *TxData,uint16_t length)
{
//  uint8_t txSts = 0;
  uint8_t *tmpTxData = (uint8_t *)TxData;

  spiTrCpt = 0;
  
  SELECT_SPI2;
  if(HAL_SPI_TransmitReceive_DMA(&hspi2,tmpTxData,(uint8_t *)spiRxData,length) != HAL_OK){
//     Error_Handler();
  }

//  return txSts;
}

 主机SPI数据传输完成回调函数 

/**
 * MCU SPI DMA输出传输完成中断处理函数
 */
void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi)
{
  if(hspi== &hspi2){
    spiTrCpt = 1;
    DESELECT_SPI2;
  }
}

 从机:

主任务中启动SPI DMA,准备接收主机数据。

/**
  * @brief          ESC PDI数据通讯处理任务
  * @param[in]      pvParameters: NULL
  * @retval         none
  */
void StartEscTask(void const * argument)
{

  ……

  MCU_SPI_DMA_CMD(spiTxData,SPI_TR_LENGHT);            //启动DMA,准备接收主机数据
  
  for(;;)
  {
    ……
  }
}

从机SPI DMA传输函数

/**
 * @brief       MCU SPI send and receive
 * @param[in]   TxData:要发送的数据
 * @param[in]   length:要发送的数据长度
 */
//uint8_t spiDataUpdate = 0;
uint16_t spiRxData[SPI_TR_LENGHT] = {0};
uint16_t spiTxData[SPI_TR_LENGHT] = {0};
void MCU_SPI_DMA_CMD(uint16_t *TxData,uint16_t length)
{
  uint8_t *tmpTxData = (uint8_t *)TxData;

  if(HAL_SPI_TransmitReceive_DMA(&hspi_mcu,tmpTxData,(uint8_t *)spiRxData,length)!=HAL_OK){
//     Error_Handler(); 
  }

}

从机SPI数据传输完成回调函数

/**
 * SPI传输完成中断处理函数
 */
extern QueueHandle_t Queue_spiRxHandle;
void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi)    
{
  if(hspi== &hspi_mcu)
  {
    //HAL_GPIO_WritePin(SPI2_RDY_GPIO_Port, SPI2_RDY_Pin, GPIO_PIN_RESET); //spi通讯接收,下降沿通知主设备
    
    if( Queue_spiRxHandle != NULL ){
      BaseType_t xHigherPriorityTaskWoken = pdFALSE;
      xQueueOverwriteFromISR( Queue_spiRxHandle, spiRxData, &xHigherPriorityTaskWoken); //ESC SPI的数据写入队列
      portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
    }
    
//    spiDataUpdate = 1;
    MCU_SPI_DMA_CMD(spiTxData,SPI_TR_LENGHT);  //重新启动DMA传输,准备接收主机数据,同时返回从机数据
  }
}

2) 问题分析

通过调试发现,每次通讯停止,HAL_SPI_TransmitReceive_DMA()函数会返回错误状态,查看该函数,发现代码执行到DMA发送,调用HAL_DMA_Start_IT()时出错,goto error了。

/**
  * @brief  Transmit and Receive an amount of data in non-blocking mode with DMA.
  * @param  hspi pointer to a SPI_HandleTypeDef structure that contains
  *               the configuration information for SPI module.
  * @param  pTxData pointer to transmission data buffer
  * @param  pRxData pointer to reception data buffer
  * @note   When the CRC feature is enabled the pRxData Length must be Size + 1
  * @param  Size amount of data to be sent
  * @retval HAL status
  */
HAL_StatusTypeDef HAL_SPI_TransmitReceive_DMA(SPI_HandleTypeDef *hspi, uint8_t *pTxData, uint8_t *pRxData,
                                              uint16_t Size)
{
   ……

  /* Enable the Tx DMA Stream/Channel  */
  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);
    errorcode = HAL_ERROR;

    goto error;
  }

  ……

}

进一步查看HAL_DMA_Start_IT(), 该函数需要判断DMA状态才能继续执行,而此时DMA Tx状态为 HAL_DMA_STATE_BUSY,从而导致发送失败。 

/**
  * @brief  Start the DMA Transfer with interrupt enabled.
  * @param  hdma pointer to a DMA_HandleTypeDef structure that contains
  *               the configuration information for the specified DMA Channel.
  * @param  SrcAddress The source memory Buffer address
  * @param  DstAddress The destination memory Buffer address
  * @param  DataLength The length of data to be transferred from source to destination (up to 256Kbytes-1)
  * @retval HAL status
  */
HAL_StatusTypeDef HAL_DMA_Start_IT(DMA_HandleTypeDef *hdma, uint32_t SrcAddress, uint32_t DstAddress,
                                   uint32_t DataLength)
{
  HAL_StatusTypeDef status = HAL_OK;

  /* Check the parameters */
  assert_param(IS_DMA_BUFFER_SIZE(DataLength));

  /* Process locked */
  __HAL_LOCK(hdma);

  if (HAL_DMA_STATE_READY == hdma->State)
  {

    ……

  }
  else
  {
    /* Process Unlocked */
    __HAL_UNLOCK(hdma);

    /* Remain BUSY */
    status = HAL_BUSY;
  }
  return status;
}

 DMA数据传输完成后,理论上要进入中断处理函数中,在该函数中清除中断标志位,并改变状态,从下面DMA中断处理函数可以确认。

/**
  * @brief  Handle DMA interrupt request.
  * @param  hdma pointer to a DMA_HandleTypeDef structure that contains
  *               the configuration information for the specified DMA Channel.
  * @retval None
  */
void HAL_DMA_IRQHandler(DMA_HandleTypeDef *hdma)
{
  ……

  /* Transfer Complete Interrupt management ***********************************/
  else if ((0U != (flag_it & ((uint32_t)DMA_FLAG_TC1 << (hdma->ChannelIndex & 0x1FU))))
           && (0U != (source_it & DMA_IT_TC)))
  {
    if ((hdma->Instance->CCR & DMA_CCR_CIRC) == 0U)
    {
      /* Disable the transfer complete and error interrupt */
      __HAL_DMA_DISABLE_IT(hdma, DMA_IT_TE | DMA_IT_TC);

      /* Change the DMA state */
      hdma->State = HAL_DMA_STATE_READY;
    }
    /* Clear the transfer complete flag */
    hdma->DmaBaseAddress->IFCR = ((uint32_t)DMA_ISR_TCIF1 << (hdma->ChannelIndex & 0x1FU));

    /* Process Unlocked */
    __HAL_UNLOCK(hdma);

    if (hdma->XferCpltCallback != NULL)
    {
      /* Transfer complete callback */
      hdma->XferCpltCallback(hdma);
    }
  }

 ……

}

由此可以初步确认是因为在调用HAL_SPI_TransmitReceive_DMA()函数传输数据前,并没有及时的进入DMA中断函数中导致的。

可是为什么DMA发送中断函数没有进去呢? 

找bug的过程是痛苦且令人崩溃的,通过查看DMA寄存器,发现DMA使能、发送完成标志位以及中断使能都是置位的,可中断程序就是在收发正常一段时间后就进不去了。 

 

开始怀疑是收发频率太快,数据量太大(一次收发210个字节),尝试过多种方法,如降低频率、减少数据量、从机接收完成通过GPIO产生一个外部中断通知主机后再发送等,都没有效果。

后来以为是SPI从设备配置上有什么问题,然后在STM32G473上增加一个SPI作为主设备以DMA凡是来接收IMU数据,出现同样问题,而且都是DMA发送完成中断进不去,而接收中断正常。

由于时间问题,暂时使用了一个临时措施来解决这个问题,即在下次收发数据前,先判断一下DMA发送完成标志位以及状态,如果发送完成但状态未改变,则直接调用中断函数来处理。

/**
 * @brief       MCU SPI send and receive
 * @param[in]   TxData:要发送的数据
 * @param[in]   length:要发送的数据长度
 */
//uint8_t spiDataUpdate = 0;
uint16_t spiRxData[SPI_TR_LENGHT] = {0};
uint16_t spiTxData[SPI_TR_LENGHT] = {0};
void MCU_SPI_DMA_CMD(uint16_t *TxData,uint16_t length)
{
  uint8_t *tmpTxData = (uint8_t *)TxData;

//  HAL_GPIO_WritePin(SPI2_RDY_GPIO_Port, SPI2_RDY_Pin, GPIO_PIN_SET); //spi准备通讯
  /**问题1:DMA发送完成后会经常出现不能进入中断的问题,原因还未知,此处判断发送完成但未改变发送状态则调用中断函数**/  
  if(HAL_DMA_GetState(hspi_mcu.hdmatx) != HAL_DMA_STATE_READY  && __HAL_DMA_GET_TC_FLAG_INDEX(hspi_mcu.hdmatx)){
     HAL_DMA_IRQHandler(hspi_mcu.hdmatx);
  }
  
  if(HAL_SPI_TransmitReceive_DMA(&hspi_mcu,tmpTxData,(uint8_t *)spiRxData,length)!=HAL_OK){
//     Error_Handler(); 
  }

}

/**
 * @brief       IMU SPI send and receive
 */
void IMU_SPI_DMA_CMD(uint8_t imuAcc,uint8_t *TxData,uint8_t *RxData,uint8_t length)
{
  if (imuAcc){
    SELECT_SPI_ACCEL;
  }
  else{
    SELECT_SPI_GYRO;
  }
  
  /**DMA发送完成后会经常出现不能进入中断的问题,原因还未知,此处判断发送完成但未改变发送状态则调用中断函数**/  
  if(HAL_DMA_GetState(hspi_imu.hdmatx) != HAL_DMA_STATE_READY  && __HAL_DMA_GET_TC_FLAG_INDEX(hspi_imu.hdmatx)){
     HAL_DMA_IRQHandler(hspi_imu.hdmatx);
  }
  
  if(HAL_SPI_TransmitReceive_DMA(&hspi_imu,TxData,RxData,length)!=HAL_OK){
//        Error_Handler(); 
  }       
}

这种临时解决方式经过一晚上的验证,SPI通讯未再异常停止过。

后面也怀疑过和FreeRTOS是不是有关系,但改动量比较大,限于时间关系也没有去验证,先这样,后续有时间再来找找根本原因。

问题2:SPI从机接收数据正常,主机接收数据不正常

这个问题也挺莫名其妙的,过程也花了不少时间,但原因归结于对HAL函数的使用不熟悉。

/**
  * @brief  Transmit and Receive an amount of data in non-blocking mode with DMA.
  * @param  hspi pointer to a SPI_HandleTypeDef structure that contains
  *               the configuration information for SPI module.
  * @param  pTxData pointer to transmission data buffer
  * @param  pRxData pointer to reception data buffer
  * @note   When the CRC feature is enabled the pRxData Length must be Size + 1
  * @param  Size amount of data to be sent
  * @retval HAL status
  */
HAL_StatusTypeDef HAL_SPI_TransmitReceive_DMA(SPI_HandleTypeDef *hspi, uint8_t *pTxData, uint8_t *pRxData,
                                              uint16_t Size)
{
   ……
}

HAL_SPI_TransmitReceive_DMA ()函数的收发数据类型都是uint8_t,想当然的认为数据长度size是以字节计算的,而我SPI和DMA的数据都是以16bits传输的,从而导致数据长度比实际长度多一倍,而从机中发送数据缓存在内存分配上和接收数据缓存是连续的,这样一来,从机接收的数据由于多了一倍,正好覆盖了发送数据缓存,就导致了发送的数据是错误的。

问题3:SPI主机接收不到IMU的数据

这个问题同样是因为对HAL函数的使用不熟悉,根据前面的框架图,SPI主机发送电机指令,从机除了反馈电机信号,还包括IMU数据,即从机发送的数据比主机数据长。

我们查看HAL_SPI_TransmitReceive_DMA ()函数的参数size说明: 

@param  Size amount of data to be sent

size是要发送的数据量,而实际上该函数中默认接收的数据和发送的数量是一样的,如下所示:

/**
  * @brief  Transmit and Receive an amount of data in non-blocking mode with DMA.
  * @param  hspi pointer to a SPI_HandleTypeDef structure that contains
  *               the configuration information for SPI module.
  * @param  pTxData pointer to transmission data buffer
  * @param  pRxData pointer to reception data buffer
  * @note   When the CRC feature is enabled the pRxData Length must be Size + 1
  * @param  Size amount of data to be sent
  * @retval HAL status
  */
HAL_StatusTypeDef HAL_SPI_TransmitReceive_DMA(SPI_HandleTypeDef *hspi, uint8_t *pTxData, uint8_t *pRxData,
                                              uint16_t Size)
{

  ……

   /* Set the transaction information */
  hspi->ErrorCode   = HAL_SPI_ERROR_NONE;
  hspi->pTxBuffPtr  = (uint8_t *)pTxData;
  hspi->TxXferSize  = Size;
  hspi->TxXferCount = Size;
  hspi->pRxBuffPtr  = (uint8_t *)pRxData;
  hspi->RxXferSize  = Size;
  hspi->RxXferCount = Size;

  ……

}

可以看出,参数size不仅赋值给了Tx,也赋值给了Rx,说明使用该函数,发送和接收的数据长度要一致。

SPI从机数据收发是受主机时钟信号控制的,主机发送完就停止了,如果从机还有数据也无法发送了,所以IMU数据其实是没机会发送出去的。

解决这个问题就很简单,将SPI主机发送的数据长度改为和从机一致,从机接收的多余数据不用理会就是了。 

 

 

 

 

  • 12
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值