STM32 HAL库 UART在循环DMA模式下接收大量不定字长数据并进行乒乓缓存

源码(Github)
详细Debug过程可见我的Blog
参考文献

一个严谨的STM32串口DMA发送&接收(1.5Mbps波特率)机制

STM32 HAL 库实现乒乓缓存加空闲中断的串口 DMA 收发机制,轻松跑上 2M 波特率

DMA在循环模式下工作时,如果在大规模传输数据时仍旧空闲中断(或传输完成中断)会有风险,因为当DMA传输数据完成,CPU介入开始拷贝DMA通道缓冲区数据时,如果此时UART继续有数据进来,DMA继续搬运数据到缓冲区,就有可能将数据覆盖,因为DMA数据搬运是不受CPU控制的,即使你关闭了CPU中断。

因此严谨的做法需要建立双buffer,CPU和DMA各自使用一块内存交替访问,即乒乓缓存,处理流程为:

  1. DMA先将数据搬运到buf1,搬运完成通知CPU来拷贝buf1数据
  2. DMA将数据搬运到buf2,与CPU拷贝buf1数据不会冲突
  3. buf2数据搬运完成,通知CPU来拷贝buf2数据
  4. DMA继续开始拷贝新数据

STM32大多数型号不提供现成的双缓存机制,但提供“半满中断”,即数据搬运到buf大小的一半时,可以产生一个中断信号。基于这个机制,我们可以实现双缓存功能,只需将buf空间开辟大一点即可。

  1. DMA将数据搬运完成buf的前一半时,触发“半满中断”事件,Callback中通知CPU来拷贝buf前半部分数据
  2. DMA继续将数据搬运到buf的后半部分,与CPU拷贝buf前半部数据不会冲突
  3. buf后半部分数据搬运完成,触发“溢满中断”,Callback通知CPU来拷贝buf后半部分数据
  4. DMA循环拷贝新数据

基于上述描述机制,DMA方式接收串口数据,有三种中断场景需要CPU去将buf数据拷贝到final中,分别是:

  • DMA通道buf溢满(传输完成)场景,触发满溢中断(HAL_UARTEx_RxEventCallback
  • DMA通道buf半满场景,触发半满中断(HAL_UART_RxHalfCpltCallback
  • 串口空闲中断场景,触发空闲中断(UART_FLAG_IDLE
    请添加图片描述
  • 也就是说,代码总共需要考虑以下几种情况:

    1. 数据量未达到半满,触发空闲中断
    2. 数据量达到半满,未达到满溢,先触发半满中断,后触发空闲中断
    3. 数据量刚好达到满溢,先触发半满中断,后触发满溢中断
    4. 数据量大于缓冲区长度,DMA循环覆盖溢出的字节

    对于情况1:在空闲中断中拷贝全部数据

    对于情况2:在半满中断中通知CPU拷贝一半的数据,DMA继续接收剩下的数据,最后在空闲中断中拷贝剩下的数据

    对于情况3:在半满中断中通知CPU拷贝一半的数据,DMA继续接收剩下的数据,最后在满溢中断中拷贝剩下的一半数据

    对于情况4:综合处理

    不少教程会单独在三个中断回调函数中进行数据处理,可以发现代码量还是挺大的。尤其是这么写代码存在一个比较麻烦的逻辑:当DMA接收的数据量大于缓冲区大小RX_BUFFER_SIZE时,由于DMA工作在循环模式,那么溢出的数据会被DMA重新放到缓冲区的开始部分,从而覆盖原有的数据。要处理这部分数据势必要引入比较复杂的判断机制,还要实时更新队首和队尾的指针,导致整个程序变得比较复杂。

    好在HAL库除了普通的HAL_UART_Receive_DMA()HAL_UART_RxCpltCallback()外,HAL库还提供了HAL_UARTEx_RxEventCallback回调。

    /**
      * @brief  Reception Event Callback (Rx event notification called after use of advanced reception service).
      * @param  huart UART handle
      * @param  Size  Number of data available in application reception buffer (indicates a position in
      *               reception buffer until which, data are available)
      * @retval None
      */
    __weak void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
    {
      /* Prevent unused argument(s) compilation warning */
      UNUSED(huart);
      UNUSED(Size);
    
      /* NOTE : This function should not be modified, when the callback is needed,
                the HAL_UARTEx_RxEventCallback can be implemented in the user file.
       */
    }
    

    该回调函数会在“advanced reception service”事件发生后触发,这里的所谓高级接收服务就包括之前需要分开判断的半满中断、满溢中断和空闲中断。这三个中断触发后都会回调HAL_UARTEx_RxEventCallback()函数。因此在拷贝数据时,无需再单独进行中断回调类型的判断。由于DMA工作不依赖CPU,因此在该函数内要做的就是将缓冲区内的数据拷贝至目标地址。形参Size表示当前缓冲区内可用数据长度。

    原先的三个中断中的代码可以合到一个中实现:

    void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
    {
      static uint8_t rx_buf_head = 0;
      static uint8_t rx_size; //待处理数据长度
     
      rx_size = Size - rx_buf_head;
    
      for (uint16_t i = 0; i < rx_size; i++)
      {
          RxFinal[final_index++] = RxBuf[(rx_buf_head + i) % RxBufSize]; // 环形缓冲处理
          if (final_index >= RxFinalSize) final_index = 0; // 避免 RxFinal 溢出
      }
    
      rx_buf_head = rx_buf_head + rx_size;
    
      if (rx_buf_head >= RxBufSize) rx_buf_head = 0;
    }
    }
    

    作者:Akiyama_R1n

    物联沃分享整理
    物联沃-IOTWORD物联网 » STM32 HAL库 UART在循环DMA模式下接收大量不定字长数据并进行乒乓缓存

    发表回复