笔记之STM32F0芯片SPI_DMA的使用(HAL库)

关于SPI+DMA的使用,进行了两次测试了,之前测试过一次,结果一直没找到问题,现在明白该如何使用了,特此记录。

一、自身SPI时间

关于SPI的通常的应用很简单,特别是通过cubemx自动生成的代码,这里就不过多介绍,可自行百度搜索关于SPI的教程及相关资料
在这里插入图片描述

这张图片是通过cubemx生成的SPI代码的一个效果(NSS软件控制+SPI传输),上述是仿照
HAL_SPI_TransmitReceive()
此函数又重新修改创建了一个
BSP_SPI_TransmitReceive()
函数,通过调用BSP_SPI_TransmitReceive()函数,然后测出的波形(关于函数修改的,见下面的工程路径链接)。

至于为啥不用HAL自身提供的HAL_SPI_TransmitReceive()的函数,是因为我发现调用这个函数的时候会造成,每两个字节后的间距会比较远,例:字节0和字节1相隔比较近,但是字节1和字节2隔得挺远的。

说远了,回到主题,通过上面图片,可知字节与字节间距离大概为1.0us左右,并且NSS拉低拉高与数据传输的时间也大概1us,发送5个字节的数据,SPI至少需要9.46us。

二、对比SPI时间

这个数据说实话我觉的也还挺不错的,直到我突然发现样机SPI的速度,两字节间距控制在ns级别,如图
在这里插入图片描述上图同样是发送5字节的数据,字节间距控制在832ns,下拉NSS到数据输出248ns,数据输出完到上拉560ns,整体发送5字节需要5.7us。

对比我之前的参数,一个是字节间距一个是纳秒一个是毫秒,还有总的发送时间,样子发送时间大概就我的1/2。
因为我这个传输对时间要求较高,所以想着也将时间尽量减少。

三、尝试减少字节间距时间

1.初步测试思路

通过对比样机SPI的速度,发现最主要的是字节间距较短,那么缩短这个时间,我首先想到了的是SPI+DMA进行传输,然后就通过cubemx直接生成了一个SPI+DMA的配置,然后通过软件控制,具体测试代码如下:

 while (1)
  {
    /* USER CODE END WHILE */
	HAL_GPIO_WritePin(GPIOA,GPIO_PIN_15,GPIO_PIN_RESET);
    HAL_SPI_TransmitReceive_DMA(&hspi1,txbuff,rxbuff,8);
    HAL_GPIO_WritePin(GPIOA,GPIO_PIN_15,GPIO_PIN_SET);
   
    HAL_GPIO_TogglePin(LED_Red_GPIO_Port,LED_Red_Pin);
    HAL_Delay(50);
    /* USER CODE BEGIN 3 */
  }

在while中周期进行读写SPI数据,但是发现数据不对,测试现象如下
在这里插入图片描述在上图的测试现象中,NSS已经拉低了,但是要等一段时间才会进行数据传输,而且数据还未传输完成,NSS就拉高了,这个现象就有问题了。
针对NSS引脚和数据传输错开的现象,增加了一个等待传输完再拉高的代码,

 while (1)
  {
    /* USER CODE END WHILE */
	HAL_GPIO_WritePin(GPIOA,GPIO_PIN_15,GPIO_PIN_RESET);
    HAL_SPI_TransmitReceive_DMA(&hspi1,txbuff,rxbuff,8);
    while(__HAL_DMA_GET_COUNTER(&hdma_spi1_rx)!=0);
    HAL_GPIO_WritePin(GPIOA,GPIO_PIN_15,GPIO_PIN_SET);
    
    HAL_GPIO_TogglePin(LED_Red_GPIO_Port,LED_Red_Pin);
    HAL_Delay(50);
    /* USER CODE BEGIN 3 */
  }

结果如下:
在这里插入图片描述现在数据传输是在NSS拉低范围内了,但是NSS拉低到数据传输有9.96us,数据传输完到NSS拉高也有11.312us,字节间距倒是减少了不少,至少证明SPI+DMA这条路应该是没问题的,可以减少数据传输的时间。

2.再次测试

针对上一次测试,发现NSS的拉高拉低到数据传输需要较长的时间,我就想会不会是软件控制NSS太慢了,另外由于SPI是DMA进行传输的,就有可能导致SPI传输和控制NSS的操作没达到同步,所以就将SPI+DMA的软件控制NSS 改成了 硬件控制NSS 。

/* SPI1 init function */
void MX_SPI1_Init(void)
{

        hspi1.Instance = SPI1;
        hspi1.Init.Mode = SPI_MODE_MASTER;                        //设置SPI工作模式,设置为主模式
        hspi1.Init.Direction = SPI_DIRECTION_2LINES;              //设置SPI单向或者双向的数据模式:SPI设置为双线模式
        hspi1.Init.DataSize = SPI_DATASIZE_8BIT;                  //设置SPI的数据大小:SPI发送接收8位帧结构
        hspi1.Init.CLKPolarity = SPI_POLARITY_LOW;                //串行同步时钟的空闲状态为低电平
        hspi1.Init.CLKPhase = SPI_PHASE_1EDGE;                    //串行同步时钟的第一个跳变沿(上升或下降)数据被采样
         hspi1.Init.NSS = SPI_NSS_HARD_INPUT;                            //NSS信号由软件控制    SPI_NSS_HARD_OUTPUT   SPI_NSS_SOFT
        hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_4;   //定义波特率预分频的值:波特率预分频值为4
        hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;                   //指定数据传输从MSB位还是LSB位开始:数据传输从MSB位开始
       hspi1.Init.TIMode = SPI_TIMODE_DISABLE;                   //关闭TI模式
       hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;   //关闭硬件CRC校验
       hspi1.Init.CRCPolynomial = 7;                             //CRC值计算的多项式
       hspi1.Init.CRCLength = SPI_CRC_LENGTH_DATASIZE;
       hspi1.Init.NSSPMode = SPI_NSS_PULSE_ENABLE;//SPI_NSS_PULSE_DISABLE;             //
   
        if (HAL_SPI_Init(&hspi1) != HAL_OK)
       {
                 _Error_Handler(__FILE__, __LINE__);
       }
}


void HAL_SPI_MspInit(SPI_HandleTypeDef* spiHandle)
{

  GPIO_InitTypeDef GPIO_InitStruct;
  if(spiHandle->Instance==SPI1)
  {
  /* USER CODE BEGIN SPI1_MspInit 0 */

  /* USER CODE END SPI1_MspInit 0 */
    /* SPI1 clock enable */
    __HAL_RCC_SPI1_CLK_ENABLE();
  
    /**SPI1 GPIO Configuration   
    PB3     ------> SPI1_SCK
    PB4     ------> SPI1_MISO
    PB5     ------> SPI1_MOSI
    */
GPIO_InitStruct.Pin = GPIO_PIN_15;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF0_SPI1;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
      
    GPIO_InitStruct.Pin = A7106_SCK_Pin|A7106_MISO_Pin|A7106_MOSI_Pin;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    GPIO_InitStruct.Alternate = GPIO_AF0_SPI1;
    HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);

    /* SPI1 DMA Init */
    /* SPI1_TX Init */
    hdma_spi1_tx.Instance = DMA1_Channel3;                          //通道选择
    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;    //外设数据长度:8位
    hdma_spi1_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;       //存储器数据长度:8位
    hdma_spi1_tx.Init.Mode = DMA_NORMAL;                            //DMA普通模式
    hdma_spi1_tx.Init.Priority = DMA_PRIORITY_HIGH;
    if (HAL_DMA_Init(&hdma_spi1_tx) != HAL_OK)
    {
      _Error_Handler(__FILE__, __LINE__);
    }

    __HAL_LINKDMA(spiHandle,hdmatx,hdma_spi1_tx);

    /* SPI1_RX Init */
    hdma_spi1_rx.Instance = DMA1_Channel2;
    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;
    if (HAL_DMA_Init(&hdma_spi1_rx) != HAL_OK)
    {
      _Error_Handler(__FILE__, __LINE__);
    }

    __HAL_LINKDMA(spiHandle,hdmarx,hdma_spi1_rx);

  /* USER CODE BEGIN SPI1_MspInit 1 */

  /* USER CODE END SPI1_MspInit 1 */
  }
}

硬件控制NSS,想着让其自动拉高拉低
但是测试发现,每发一个字节NSS引脚就自动拉高了,这就导致了读取的数据是有问题的,具体现象如下:
在这里插入图片描述
上图的现象,每个字节都进行拉高了,那对于一包数据大于1字节的话,那么就会有问题的,
SPI的数据 = 外设地址 + 该地址的命令(我的理解)
所以针对上图,我想着就如何让NSS拉高的操作和发送的数据相关联,即不让他每个字节都自动拉高
为此,研究了好久,同时也发过好几个帖子,都没有找到相关的资料

-------------------------------卡主了---------------------------------------------------------------------------------------------
在这个阶段卡了挺久的,后面也就没管SPI+DMA的应用了,直到后面空闲了在接着重新拿起这个SPI+DMA的操作。

3.间隔两个月后的再次尝试

在我发的关于SPI+DMA的求助帖子上,我看到推荐说用软件NSS,同时我也仔细研究了下样机的SPI波形,发现他有两路SPI数据,
在这里插入图片描述如图,可以得知他采用的是软件控制NSS,因为有两个NSS引脚,并且公用同一路数据引脚,
那么样机采用的是软件控制,达到的SPI数据的速度提升,那么照理来说我们也能通过软件控制NSS,实现SPI+DMA的操作。

但是在初步测试时,软件控制NSS,出现错位问题,所以对软件控制还是有些怀疑的,直到我在帖子上看到 正常来讲,那个NSS每个字节抬高一个时钟时间是正常的操作,每个字节NSS抬高是正常操作,那么我想用硬件控制NSS,让其跟随传输的数据进行自动拉高拉低就行不通了,思路有问题了。

现在开始专研软件控制NSS,达到SPI+DMA的操作了,在测试1中,暴露的问题是NSS拉高的太快了,那么换个思路,能不能让他传输完成就自动拉高呢,这是可以的,DMA中断中正好就有一个传输完成的标志,那我就利用上这个标志,在中断中让其拉高NSS,

void DMA1_Channel2_3_IRQHandler(void)
{
  /* USER CODE BEGIN DMA1_Channel2_3_IRQn 0 */

  /* USER CODE END DMA1_Channel2_3_IRQn 0 */
  HAL_DMA_IRQHandler(&hdma_spi1_rx);
  HAL_DMA_IRQHandler(&hdma_spi1_tx);
  
  /* USER CODE BEGIN DMA1_Channel2_3_IRQn 1 */
 if(__HAL_DMA_GET_IT_SOURCE(&hdma_spi1_rx, DMA_IT_TC)){
  HAL_GPIO_WritePin(GPIOA, GPIO_PIN_15, GPIO_PIN_SET);
  }
  /* USER CODE END DMA1_Channel2_3_IRQn 1 */
}

在调用HAL_SPI_TransmitReceive_DMA()函数前,拉低NSS,在DMA的传输完成中断进行拉高NSS,然后测试,效果是可以的,数据传输完后就立刻拉高了,就是NSS拉低到数据传输的时间较长,那么都可以在中断里拉高NSS了,那么在开启DMA传输前,拉低NSS不也是可以的吗。。。。

所以我跳到HAL_SPI_TransmitReceive_DMA()函数里,在开启DMA前,增加了拉低NSS的操作

HAL_StatusTypeDef HAL_SPI_TransmitReceive_DMA(SPI_HandleTypeDef *hspi, uint8_t *pTxData, uint8_t *pRxData,uint16_t Size)
{
******************省略***********
  /* Enable the SPI Error Interrupt Bit */
  __HAL_SPI_ENABLE_IT(hspi, (SPI_IT_ERR));
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_15, GPIO_PIN_RESET);
  /* Enable Tx DMA Request */
  SET_BIT(hspi->Instance->CR2, SPI_CR2_TXDMAEN);

error :
  /* Process Unlocked */
  __HAL_UNLOCK(hspi);
  return errorcode;
}

现在NSS拉低的操作,放到HAL提供的HAL_SPI_TransmitReceive_DMA()函数里;
NSS拉高的操作,在DMA传输完成中断中;
测试效果:
在这里插入图片描述这个效果是不错的,字节间距是208ns,拉低到数据传输656ns,数据传输完到拉高1.328us,发送5个字节需要5.952us的时间,这个时间相比于之前已经提升挺多的了,就是数据传输完到拉高时间稍微有点长。

四、实现SPI+DMA传输(减少传输时间)

在最后一次测试中,时间已经不错了,就是稍微有点瑕疵,于是在进行修改,在
HAL_DMA_IRQHandler(&hdma_spi1_tx);
里面对NSS进行拉高

void HAL_DMA_IRQHandler(DMA_HandleTypeDef *hdma)
{

***************省略**************
/* Transfer Complete Interrupt management ***********************************/
  else if ((RESET != (flag_it & (DMA_FLAG_TC1 << hdma->ChannelIndex))) && (RESET != (source_it & DMA_IT_TC)))
  {
  	if((hdma->Instance->CCR & DMA_CCR_CIRC) == 0U)
  	{
  		/* Disable the transfer complete  & transfer error interrupts */
  		/* if the DMA mode is not CIRCULAR */
  		hdma->Instance->CCR &= ~(DMA_IT_TC | DMA_IT_TE);
  		
  		/* Change the DMA state */
  		hdma->State = HAL_DMA_STATE_READY;
  	}
  	
  	/* Clear the transfer complete flag */
  	hdma->DmaBaseAddress->IFCR = DMA_FLAG_TC1 << hdma->ChannelIndex;
    
    /*nss up*/
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_15, GPIO_PIN_SET);
    
  	/* Process Unlocked */
  	__HAL_UNLOCK(hdma);
  	
  	if(hdma->XferCpltCallback != NULL)
  	{
  		/* Transfer complete callback */
  		hdma->XferCpltCallback(hdma);
  	}
  }
  ***************省略**************
  }

发现将NSS放在这里,时间是更短的
在这里插入图片描述数据传输完到拉高NSS,已经降低到560ns了,为了适配其他的DMA中断,本想在HAL_DMA_IRQHandler()里面增加 if 判断,再拉高NSS,但是发现增加了判断,数据传输完到拉高NSS这里的时间又变成了us,于是索性复制HAL_DMA_IRQHandler()这个函数,在里面进行拉高NSS的操作,并对函数重命名,只能SPI_DMA调用。

实现步骤一

创建属于自己的HAL_DMA_SPI读写函数

/*
* 复制HAL_SPI_TransmitReceive_DMA()函数,在开启DMA前,拉低NSS引脚
*/
HAL_StatusTypeDef HAL_SPI_MY_TransmitReceive_DMA(SPI_HandleTypeDef *hspi, uint8_t *pTxData, uint8_t *pRxData,uint16_t Size)
{
***************************
******************省略**************
  /* 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  */
  HAL_DMA_Start_IT(hspi->hdmatx, (uint32_t)hspi->pTxBuffPtr, (uint32_t)&hspi->Instance->DR, hspi->TxXferCount);
  
/* Check if the SPI is already enabled */
  if ((hspi->Instance->CR1 & SPI_CR1_SPE) != SPI_CR1_SPE)
  {
    /* Enable SPI peripheral */
    __HAL_SPI_ENABLE(hspi);
  }
  /* Enable the SPI Error Interrupt Bit */
  __HAL_SPI_ENABLE_IT(hspi, (SPI_IT_ERR));

HAL_GPIO_WritePin(GPIOA, GPIO_PIN_15, GPIO_PIN_RESET);

  /* Enable Tx DMA Request */
  SET_BIT(hspi->Instance->CR2, SPI_CR2_TXDMAEN);

error :
  /* Process Unlocked */
  __HAL_UNLOCK(hspi);
  return errorcode;
}

这个函数只是单纯复制HAL_SPI_TransmitReceive_DMA()函数,然后在开启DMA前,拉低NSS引脚,这样就能尽量减少对库的修改,同时在NSS更换引脚后,也可以更方便修改NSS引脚

实现步骤二

创建属于自己的HAL_DMA_SPI中断函数

/*
* 在it.c中,要先调用
  HAL_DMA_IRQHandler(&hdma_spi1_rx);
  HAL_SPI1_TX_DMA_IRQHandler(&hdma_spi1_tx);//在此函数中会对NSS引脚拉高
*/
void HAL_SPI1_TX_DMA_IRQHandler(DMA_HandleTypeDef *hdma)
{
*****************省略*************
  /* Transfer Complete Interrupt management ***********************************/
  else if ((RESET != (flag_it & (DMA_FLAG_TC1 << hdma->ChannelIndex))) && (RESET != (source_it & DMA_IT_TC)))
  {
          if((hdma->Instance->CCR & DMA_CCR_CIRC) == 0U)
          {
                  /* Disable the transfer complete  & transfer error interrupts */
                  /* if the DMA mode is not CIRCULAR */
                  hdma->Instance->CCR &= ~(DMA_IT_TC | DMA_IT_TE);
      
                  /* Change the DMA state */
                  hdma->State = HAL_DMA_STATE_READY;
          }
        
          /* Clear the transfer complete flag */
          hdma->DmaBaseAddress->IFCR = DMA_FLAG_TC1 << hdma->ChannelIndex;

    /*nss up*/
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_15, GPIO_PIN_SET);

          /* Process Unlocked */
          __HAL_UNLOCK(hdma);

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

}  

这个函数是复制 HAL_DMA_IRQHandler()函数,然后增加NSS的操作。
【注意】

  • 在void DMA1_Channel2_3_IRQHandler(void)中,要先调
    HAL_DMA_IRQHandler(&hdma_spi1_rx); 再调用HAL_SPI1_TX_DMA_IRQHandler(&hdma_spi1_tx);函数
    (调换位置时会出现数据还没传输完,NSS就拉高的操作)
  • 对于SPI_DMA的通道优先级尽量设置高点,这样可以更好的保证SPI传输时间短

实现效果

在这里插入图片描述发送5个字节大概需要5.16us,NSS上下拉时间在600ns左右:

关于测试工程,github链接:https://github.com/Gu-Yue-Hu/SPI_DMA_HAL.git

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>