硬汉嵌入式论坛

 找回密码
 立即注册
查看: 2031|回复: 7
收起左侧

[客户分享] stm32使用半满中断实现的高可靠串口数据收发

  [复制链接]

5

主题

16

回帖

31

积分

新手上路

积分
31
发表于 2021-10-8 09:26:41 | 显示全部楼层 |阅读模式
本帖最后由 1598025967 于 2021-10-8 10:10 编辑

文章目录


写在前面

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


软硬件环境

软件: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接收的前提下):


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

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

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

  20.     HAL_UART_IRQHandler(&UartHandle);
  21. }
复制代码

理论上来说这样的话只要接收缓冲足够大,写入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的位置,模型如下:(只要产生空闲中断,都适用此流程)


  1. head_ptr = 上次tail_ptr的位置;
  2. /* huart->RxXferSize为接收缓存的总大小,__HAL_DMA_GET_COUNTER获取的是还未接收的数据大小 */
  3. tail_ptr = huart->RxXferSize - __HAL_DMA_GET_COUNTER(huart->hdmarx);
复制代码

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


  1. head_ptr = 上次tail_ptr的位置;
  2. /* huart->RxXferSize为接收缓存的总大小,(huart->RxXferSize & 1)奇数时为1,偶数时为0 */
  3. tail_ptr = (huart->RxXferSize >> 1) + (huart->RxXferSize & 1);
复制代码

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


  1. head_ptr = 上次tail_ptr的位置;
  2. /* huart->RxXferSize为接收缓存的总大小 */
  3. tail_ptr = huart->RxXferSize;
复制代码

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


收发数据模型

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




结尾

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

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

  5. #if UART_USE_DMA_RX
  6.     /*是否使能空闲中断*/
  7.     #define UART_USE_IDLE_IT 1
  8. #endif

  9. /*配置接收缓冲区的大小*/
  10. #define UART_BUF_SIZE 64
复制代码

推荐:DMA+空闲中断方式


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

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


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

评分

参与人数 2金币 +70 收起 理由
hqgboy + 20 赞一个!
eric2013 + 50 赞一个!

查看全部评分

回复

使用道具 举报

1万

主题

6万

回帖

10万

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
106611
QQ
发表于 2021-10-8 09:30:04 | 显示全部楼层
谢谢楼主分享,图做的很好。
回复

使用道具 举报

5

主题

16

回帖

31

积分

新手上路

积分
31
 楼主| 发表于 2021-10-8 09:39:27 | 显示全部楼层
eric2013 发表于 2021-10-8 09:30
谢谢楼主分享,图做的很好。

硬汉哥,什么时候让论坛帖子支持markdown格式呀,好难调格式呀。
回复

使用道具 举报

1万

主题

6万

回帖

10万

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
106611
QQ
发表于 2021-10-8 10:43:09 | 显示全部楼层
1598025967 发表于 2021-10-8 09:39
硬汉哥,什么时候让论坛帖子支持markdown格式呀,好难调格式呀。

这个得研究下怎么支持。

你重新整理后很给力了。
回复

使用道具 举报

13

主题

86

回帖

125

积分

初级会员

积分
125
发表于 2021-10-8 14:00:00 | 显示全部楼层
串口我也用的多,一直都是用DMA,不过和楼主使用用不同,我不用DMA中断,直接用大缓存DMA循环模式,再加个空闲中断,或者不用空闲中断直接时间轮询。
回复

使用道具 举报

5

主题

192

回帖

212

积分

高级会员

积分
212
发表于 2021-10-14 15:03:07 | 显示全部楼层
1598025967 发表于 2021-10-8 09:39
硬汉哥,什么时候让论坛帖子支持markdown格式呀,好难调格式呀。

/* 停止DMA接收 */
HAL_UART_DMAStop(&UartHandle);

停止DMA接收应该用
HAL_UART_AbortReceive(huart);

HAL_UART_DMAStop 会同时关闭发送和接收。如果DMA刚好正在发送也会被中止
因此只是关闭接收的话使用
HAL_UART_AbortReceive 更加合适

  1. HAL_StatusTypeDef HAL_UART_DMAStop(UART_HandleTypeDef *huart)
  2. {
  3.   ...省略
  4.   /* Stop UART DMA Tx request if ongoing */
  5.   if ((HAL_IS_BIT_SET(huart->Instance->CR3, USART_CR3_DMAT)) &&
  6.       (gstate == HAL_UART_STATE_BUSY_TX))
  7.   {
  8.     ...省略
  9.     UART_EndTxTransfer(huart);
  10.   }

  11.   /* Stop UART DMA Rx request if ongoing */
  12.   if ((HAL_IS_BIT_SET(huart->Instance->CR3, USART_CR3_DMAR)) &&
  13.       (rxstate == HAL_UART_STATE_BUSY_RX))
  14.   {
  15.     ...省略
  16.     UART_EndRxTransfer(huart);
  17.   }
  18.   return HAL_OK;
  19. }
复制代码


HAL_UART_AbortReceive 如果开启了DMA会关闭DMA如果没有开启则不操作,
使用HAL_UART_AbortReceive 也可以配合 串口空闲中断在不使用DMA的时候来实现 idle 接收
F1的UART5没有DMA通道。
回复

使用道具 举报

5

主题

16

回帖

31

积分

新手上路

积分
31
 楼主| 发表于 2021-10-14 21:49:03 | 显示全部楼层
旮旯旭 发表于 2021-10-14 15:03
/* 停止DMA接收 */
HAL_UART_DMAStop(&UartHandle);

全部的DMA接口我都测试过了,我想讲的重点不在这里,所以接口随便写了个;不管是pause还是stop都会大量丢包,不然我也不会更加复杂的加个半满中断的处理了。
回复

使用道具 举报

0

主题

19

回帖

19

积分

新手上路

积分
19
发表于 2021-11-6 10:07:38 | 显示全部楼层
直接配合DMA+空闲中断即可,在回调函数里面能够获取此次接收的数据长度,做好标记即可,其核心就是构造一个不定长接收的BUFFER
回复

使用道具 举报

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

本版积分规则

QQ|小黑屋|Archiver|手机版|硬汉嵌入式论坛

GMT+8, 2024-4-26 10:59 , Processed in 0.329815 second(s), 26 queries .

Powered by Discuz! X3.4 Licensed

Copyright © 2001-2023, Tencent Cloud.

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