当先锋百科网

首页 1 2 3 4 5 6 7

写在前面

串口在各种项目中可谓是太常用了,它也是搞嵌入式必须弄懂的一个通信协议,最近维护了很久的一个项目,设备内另一模块程序更新后出现了不稳定的情况,现象就是某个功能有时候正常有时候不正常,经排查是通信接口上出现了丢包导致的,通信的接口正是用的串口,然后经过多次优化,解决了问题,以此记录一下优化过程。

软硬件环境

软件:MDK5、stm32 HAL库

硬件:项目上主控芯片为stm32f407zet6(调试时使用的stm32f103c8t6),整板外设只用了5个串口,2个硬件定时器。

库函数接口

首先看一下用到的库函数接口,不重要的忽略:

__HAL_DMA_GET_COUNTER获取DMA剩余未接收数据
HAL_UART_Transmit串口阻塞方式发送函数
HAL_UART_Transmit_IT串口中断方式发送函数
HAL_UART_Receive_IT串口中断方式接收数据
HAL_UART_Transmit_DMA串口DMA方式发送函数
HAL_UART_Receive_DMA串口DMA方式接收函数
HAL_UART_TxCpltCallback串口发送完成回调函数
HAL_UART_RxCpltCallback串口接收完成回调函数
HAL_UART_RxHalfCpltCallback串口接收过半回调函数

初始实现方式

由于项目中是自定义帧格式,而且每个帧长很短,不超过16字节,所以最开始串口接收使用的是DMA单字节接收,当检测接收到一个完整帧时,将收到的帧写入fifo,然后发送一个信号量,被阻塞的任务得到信号量后从fifo读取帧并作相应操作,大致流程如下:
流程图
这样的实现方式比较简单,在数据速率比较恒定的情况下是没有问题的,但最近与之通信的模块程序更新后,出现了偶尔突发数据量会很大的情况,这样就可能会丢失数据。

第一次优化

知道了问题所在后,进行第一次优化,经过分析有以下方案可以选用:

  1. DMA(或中断)一次接收多字节
  2. DMA加空闲中断

因为最终选用的第二种方式,所以说说为什么第一种方式不行,原因有以下几点:

  • 对于中断方式来说一次接收多字节并未解决频繁中断的问题,还是会一个字节产生一次中断
  • 单纯的DMA(或中断)必须要接收到指定数量的数据才能完全读走数据,否则数据会一直被缓存无法读取
  • 通信数据帧是不定长的

空闲中断会在收到一个字节后指定时间内未收到下一个字节时产生,这样的话就可以在产生空闲中断时将收到的数据读走,而不会一直被缓存着,实现代码如下(在使能DMA接收的前提下):

/* 初始化时使能空闲中断 */
__HAL_UART_ENABLE_IT(&UartHandle, UART_IT_IDLE);
/* 串口中断处理函数中增加对空闲中断的处理 */
void USART_IRQHandler(void)
{
    uint32_t tmp = 0;

    if(__HAL_UART_GET_FLAG(&UartHandle, UART_FLAG_IDLE))
    {
        /* 清空闲中断标志位 */
        __HAL_UART_CLEAR_IDLEFLAG(&UartHandle);
        /* 停止DMA接收 */
        HAL_UART_DMAStop(&UartHandle);
        /* 得到已接收数据长度 */
        tmp = UartHandle.RxXferSize - __HAL_DMA_GET_COUNTER(UartHandle.hdmarx);

        if(0 != tmp)
        {
            /* 存入数据到fifo */
        }
        /* 再次开启DMA接收 */
        HAL_UART_Receive_DMA(&UartHandle, UartHandle.pRxBuffPtr, UartHandle.RxXferSize);
    }

    HAL_UART_IRQHandler(&UartHandle);
}

理论上来说这样的话只要接收缓冲足够大,写入fifo的操作只会发生在产生空闲中断时,应该会大大缓解丢包的情况,但实际测试效果却不明显,并没有很好的处理突发数据的接收。
分析原因应该是由于DMA接收是不受控的,在处理空闲中断时短暂关闭了DMA的接收,而就是在这个关闭的过程中如果有新数据到来,则只能丢弃(并且会丢弃前面已接收的一部分数据),从而产生丢包。

第二次优化

既然知道了是因为短暂关闭DMA导致的,那就一步到位,想办法不关闭DMA就能解决了,stm32的DMA除了满中断还有个半满中断,也就是接收数据过半时产生中断,那就可以在接收数据过半时将已收到的前半数据写入fifo,然后产生满中断时将后半数据写入fifo,此时DMA会自动将写指针移动到接收缓存的头部继续接收,循环这个过程,就不必关闭DMA,大致流程如下:
流程图
按照如上的流程进行优化后,的确没发现丢包的情况了,这种和stm32f4支持的多缓冲原理类似,但是多缓冲的话在stm32f103c8t6上面没有这个功能,我是在stm32f103c8t6上面验证的,所以没有使用多缓冲。
就在我以为这样就结束了时,又发现了新的问题,前面说过DMA要收到指定数量的数据时才会产生中断,那这里还漏了一种情况,那就是如果发送方发送过来的帧长不足以让DMA产生中断,那数据就会被缓存,直到满足条件才能读取,这样的话肯定不行,所以就得空闲中断上场了。

最后的修改

现在只需要将空闲中断加入上面那个流程,就能够应对各种情况了,因为是不定长帧,所以空闲中断会在接收的任意期间产生,我用一个全局变量head_ptr来保存缓冲区的读起始偏移tail_ptr来保存缓冲区的读结束偏移(注意tail_ptr是虚拟的,它的值有三种情况,后面会讲到),这样实现一个类循环fifo结构。

  1. 初始状态,head_ptrtail_ptr都指向缓冲区首部,收到一帧数据后,没有新数据到来,触发空闲中断,在空闲中断回调函数中要做的操作就是读走这部分数据(图中1号区域head_ptrtail_ptr之间的数据),并且将head_ptr移动到tail_ptr的位置,模型如下:(只要产生空闲中断,都适用此流程)
    流程1
head_ptr = 上次tail_ptr的位置;
/* huart->RxXferSize为接收缓存的总大小,__HAL_DMA_GET_COUNTER获取的是还未接收的数据大小 */
tail_ptr = huart->RxXferSize - __HAL_DMA_GET_COUNTER(huart->hdmarx);
  1. 继续接收新数据,此为触发半满中断的情况,在半满中断回调函数中的操作是将head_ptrtail_ptr之间的数据写入fifo(图中2号区域),然后移动head_ptrtail_ptr的位置,模型如下:
    流程2
head_ptr = 上次tail_ptr的位置;
/* huart->RxXferSize为接收缓存的总大小,(huart->RxXferSize & 1)奇数时为1,偶数时为0 */
tail_ptr = (huart->RxXferSize >> 1) + (huart->RxXferSize & 1);
  1. 继续接收数据直到产生满中断,在满中断回调函数中的操作也是将head_ptrtail_ptr之间的数据写入fifo(图中3号区域),然后移动head_ptrtail_ptr的位置,模型如下:
    流程3
head_ptr = 上次tail_ptr的位置;
/* huart->RxXferSize为接收缓存的总大小 */
tail_ptr = huart->RxXferSize;

至此各种情况就都考虑完整了,一个相对可靠的串口接收程序就实现了。

收发数据模型

此文源码我放在了我的码云仓库上,有需要的可以自行下载(https://gitee.com/wei513723/stm32-stable-uart-transmit-receive),源码中可以通过宏进行选择使用中断接收DMA接收DMA加空闲中断接收三种方式,使用的程序收发数据模型如下:
中断收模型
DMA收模型
DMA加空闲中断模型
发送模型

结尾

关于源码中这几个宏的配置须知:

/*是否使能DMA接收*/
#define UART_USE_DMA_RX 1
/*是否使能DMA发送*/
#define UART_USE_DMA_TX 1

#if UART_USE_DMA_RX
    /*是否使能空闲中断*/
    #define UART_USE_IDLE_IT 1
#endif

/*配置接收缓冲区的大小*/
#define UART_BUF_SIZE 64

推荐:DMA+空闲中断方式

  • 中断方式:缺点是中断频繁,每收到一个字节都会产生一次中断;必须接收到指定长度数据才能读走数据;适合定长数据帧;用不了DMA才推荐此种方式
  • DMA方式:缺点是必须接收到指定长度数据才能读走数据;适合定长数据帧
  • DMA加空闲中断:最优解

接收缓冲区大小根据自己需求而定,波特率越高,接收缓冲区大小相对的也应更大,接收fifo和发送fifo的大小也就越大。

欢迎扫码关注我的微信公众号