rtthread 串口dma接收_如何写一个健壮且高效的串口接收程序?(六)

上篇文章介绍了串口数据帧检查问题,今天说说另两个问题,串口空闲和通信量问题。


4、 串口空闲

前面一直提到串口空闲,也大概明白串口的作用,但是一些细节问题还是需要好好说一下的。

第一个问题,如何清除串口空闲中断标志位?

很多人会使用 USART_ClearFlag 标准库函数进行清除,但是当你跳转到该函数原型时,你会看到如下说明:

2d5963f8342fb21435b8b4162dfaa311.png

你会看到很多标志位是无法通过该函数清除的。

那么该如何清除 IDLE 标志呢?其实上面的注释已经进行了说明。

PE、FE、NE、ORE、IDLE 标志位的清除是通过一个软件序列进行清除的:首先通过 USART_GetFlagStatus 读取 USART_SR 寄存器的值,然后通过 USART_ReceiveData 函数读取 USART_DR 的值即可。

那么这里就有一个问题,是否这些标志问题的清除都要单独编写清除序列呢?

答案是否定的。

因为这些标志位都是由同一种序列进行清除的,所以只要一个清除序列就会把所有的标志位都进行清除了(同样一旦执行了这个序列,也就意味着你无法再通过 USART_SR 寄存器获得标志位了)。

为了保证获取标志位,我们可以在清除序列之前把 USART_SR 寄存器的值保存到副本中,然后再读取 USART_DR 寄存器的值保存到副本来实现清除功能,注意该序列应该无条件执行(不在某个判断语句中)。这样后续我们就可以使用这个 USART_SR 的副本判断哪一个标志置位了,同样也可以使用 USART_DR 的副本获取串口数据,而为了实现以上效果,USART_GetFlagStatus 这个函数就不合适了,只能直接操作寄存器去实现。

第二个问题,在线调试时对空闲中断会有影响吗?

我们知道,KEIL 能够将一个结构体的数据全部读取出来,而库函数将串口模块的所有寄存器都封装在一个结构体中,这样就会出现一个问题,如果你的窗口是实时刷新的,当你使用 KEIL 读取串口模块寄存器的时候(不管是使用 peripheral 窗口还是 Watch 窗口),就会出现先读取 SR 再读取 DR 的情况, 这样就有可能出现 KEIL 和单片机 CPU 读取这两个寄存器冲突的情况。

如果全速运行时,KEIL 先执行了这个序列(通过调试器读取这两个寄存器的值),单片机 CPU 再读取 SR 寄存值,必然是无法读取到正确标志位的,因为这些标志位已经被 KEIL 的读取序列清除了(这个情况鱼鹰确实碰到过,当时明明下发了数据,但是单片机无法获取标志位),所以在调试串口时,注意不要让 KEIL 去读取这些寄存器(即关闭这些窗口,只有在必须的情况下才开启),防止出现莫名其妙的情况。

第三个问题,空闲中断能准确触发吗?

如果从接收端考虑的话,如果触发了空闲中断,那么必然满足了条件才触发的,而不是意外触发的(嗯,我们要相信 STM32),但从发送端考虑的话,有可能出现一帧数据断续发送,导致一帧数据触发多次空闲中断,所以如果是简单的 DMA+空闲中断方式接收是很有问题的(空闲出现就认为一帧结束了,就会把一帧数据当成两帧处理,这样肯定无法通过数据检查的)。

那么先来分析为什么会出现一帧数据多次触发空闲中断情况。我们知道 linux、windows 系统并不是实时系统,当应用程序需要发送一帧数据时,可能并没有连续发送,而是发送完一个字节后去处理其他事情后才发送下一个字节,这样一来,如果耽误的时间够长,就会触发串口的空闲中断,从而一帧数据当成两帧处理了。

那有什么方法可以解决呢?鱼鹰提供两种解决思路。

第一种,使用两个缓存空间,一个缓存空间专门用于接收串口数据,将接收到的数据存放到另一个缓存,这个缓存采用字节队列的方式进行管理,应用程序从缓存队列中一个字节一个字节的取出数据进行处理(注意检查数据有效性),这样就能保证及时处理。但是因为空闲中断不再可靠,所以空闲中断不再作为判断一帧数据结束的依据(根据长度信息判断),而是只在空闲中断中将已接收数据复制到字节队列缓存中,这样就可以处理意外的空闲中断。

第二种,还是一个缓存空间,还是 DMA+空闲方式处理,但是需要增加额外的条件。就是当进入空闲中断后,不再直接处理,而是获取当前接收时刻,然后在处理数据的时候根据这个时刻来判断是否达到足够的空闲时间,只有在进入空闲中断后并达到一定延时之后才认为一帧数据结束了,这样可以避免一些非常短的空闲时间(鱼鹰公众号提供过的代码使用这种方式)。

以上问题是就是鱼鹰以前使用空闲中断从未考虑的问题,鱼鹰并不知道使用空闲中断还可能出现误触发的情况,但是既然知道了,就要想办法解决。但是为什么以前使用空闲中断时没有出现通信问题呢?

事实上不是没有问题,而是有可能把分散的一帧数据的两部分直接丢弃了而已,因为有重发机制,所以即使丢弃一帧数据,也能通信正常,而且这种一帧数据分散成两部分的概率还是挺低的,ubuntu(linux 系统)下大概千分之三左右的样子。

第四个问题,如果单片机没有空闲中断又该如何做?

当我们使用 RXNE 的同时其实我们也可以使用空闲中断,这样也能确定一帧数据的结束(但是要注意前面的误触发问题)。但是如果有些低端单片机(如 51 )没有空闲中断又该怎么办?

其实我们可以从 stm32 的空闲中断得到相应的启发。

所谓空闲中断,就是当串口接收到数据后,在应该接收数据的时刻,发送方并没有发送数据,所以串口模块置位空闲标志位,从而引起空闲中断。

那么我们是否可以软件模拟串口模块的这个功能,从而确定一帧数据的结束呢?

答案是肯定的(前提是每一帧数据之间有空闲时间)。

我们可以使用一个定时器,定时器向上计数。当接收到一个字节数据后,初始化计数器并启动定时器,这样一旦有一段时间没有接收到串口数据(也就不再初始化计数器),那么定时器溢出,进入溢出中断,而这个溢出中断就类似于串口的空闲中断(在溢出中断中关闭定时器以达到清除空闲中断标志的作用),这样就达到了串口空闲中断的效果(和前面问题的第二种解决方案类似)。

5、 通信吞吐量

在以上分析过程中,都是采用主机发送,从机接收后再回复主机的方式进行通信,虽然通信正常,但实际上效率比较低下,单位时间传输的数据量较少,如下图所示:

0d405b88f5f20a59c46908aea5e736d8.png

红色部分就是必要的空闲时间,可以看到左右两张图的通信频率是有差异的,右图中从机必须等待前一帧数据发送完毕才能处理数据,而左图可以在接收当前帧时处理上一帧数据,类似 CPU 的指令执行流水线。

b2e4260497c835bece99688803cd6448.png

(图片来源于《权威指南》)

我们也可以将串口接收分为二级流水线:接收、处理,如此一来,我们最少需要两个缓存空间,当一个缓存在接收时,另一个缓存就进行数据处理。发送端可能不等接收端发送完应答数据,它就已经开始发送下一帧数据了,只要相邻两帧数据保证一定发送间隔,就能正常触发中断。

同理,因为接收端也不再慢悠悠的等待接收数据,而是可能有好几帧数据等着它处理,所以为了确保发送端能正常触发空闲中断,也需要控制发送间隔。

为了最大程度利用串口,我们可以使用队列管理很多缓存空间(当只有两个缓存时,可以直接使用异或运算进行缓存切换),比如 uCOS II 中我们可以利用系统的内存管理服务和队列服务实现有效管理,并且当有非常紧急的通信任务时,还可以插入到队头优先处理。

但是增大吞吐量时,对重发机制和从机数据的确认有一定影响,需要考虑清楚。

对于如何提高通信量,鱼鹰经验不多,就不多说了(或许可以从网络通信过程中得到答案)。

如果要用一句话总结本篇笔记内容,那就是使用 空闲中断+DMA+队列+内存管理+定时控制 方式接收串口数据会是不错的选择。


串口遇到的那些问题到此总结完毕,如果喜欢鱼鹰的笔记,记得关注哦,后期将不定时分享新的笔记。

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值