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主机发送的数据长度改为和从机一致,从机接收的多余数据不用理会就是了。
作者:tianya_xuan