硬石社区

 找回密码
 立即注册
查看: 2088|回复: 0

[STM32F4 & F7] stm32使用半满中断实现的高可靠串口数据收发

[复制链接]

2

主题

6

帖子

5704

积分

新世纪之星

Rank: 8Rank: 8Rank: 8Rank: 8Rank: 8Rank: 8Rank: 8Rank: 8

积分
5704
发表于 2021-10-8 10:14:06 | 显示全部楼层 |阅读模式
本帖最后由 1598025967 于 2021-10-8 10:17 编辑

文章目录


写在前面

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


软硬件环境

软件: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读取帧并作相应操作,大致流程如下:

这样的实现方式比较简单,在数据速率比较恒定的情况下是没有问题的,但最近与之通信的模块程序更新后,出现了偶尔突发数据量会很大的情况,这样就可能会丢失数据。


第一次优化

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


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

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


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

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


[C] 纯文本查看 复制代码
/* 初始化时使能空闲中断 */
__HAL_UART_ENABLE_IT(&UartHandle, UART_IT_IDLE);


[C] 纯文本查看 复制代码
/* 串口中断处理函数中增加对空闲中断的处理 */
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结构。


  • 初始状态,head_ptr和tail_ptr都指向缓冲区首部,收到一帧数据后,没有新数据到来,触发空闲中断,在空闲中断回调函数中要做的操作就是读走这部分数据(图中1号区域,head_ptr与tail_ptr之间的数据),并且将head_ptr移动到tail_ptr的位置,模型如下:(只要产生空闲中断,都适用此流程)


[C] 纯文本查看 复制代码
head_ptr = 上次tail_ptr的位置;
/* huart->RxXferSize为接收缓存的总大小,__HAL_DMA_GET_COUNTER获取的是还未接收的数据大小 */
tail_ptr = huart->RxXferSize - __HAL_DMA_GET_COUNTER(huart->hdmarx);


  • 继续接收新数据,此为触发半满中断的情况,在半满中断回调函数中的操作是将head_ptr到tail_ptr之间的数据写入fifo(图中2号区域),然后移动head_ptr到tail_ptr的位置,模型如下:


[C] 纯文本查看 复制代码
head_ptr = 上次tail_ptr的位置;
/* huart->RxXferSize为接收缓存的总大小,(huart->RxXferSize & 1)奇数时为1,偶数时为0 */
tail_ptr = (huart->RxXferSize >> 1) + (huart->RxXferSize & 1);


  • 继续接收数据直到产生满中断,在满中断回调函数中的操作也是将head_ptr到tail_ptr之间的数据写入fifo(图中3号区域),然后移动head_ptr到tail_ptr的位置,模型如下:


[C] 纯文本查看 复制代码
head_ptr = 上次tail_ptr的位置;
/* huart->RxXferSize为接收缓存的总大小 */
tail_ptr = huart->RxXferSize;


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


收发数据模型

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




结尾

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

[C] 纯文本查看 复制代码
/*是否使能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的大小也就越大。


原文链接:https://blog.csdn.net/a1598025967/article/details/120539170

回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

Archiver|手机版|小黑屋|Comsenz Inc. ( 粤ICP备16080364号

GMT+8, 2022-9-26 04:44 , Processed in 0.071024 second(s), 28 queries .

Powered by Discuz! X3.2

© 2001-2013 Comsenz Inc.

快速回复 返回顶部 返回列表