电子说
封装STM32串口的底层时,遇到了串口帧同步的问题。虽然以前也遇到类似场合,写出来的代码基本能够解决问题,但是在逻辑上总是不能彻底的解释一些细节。
当前的工作环境:
由于代码想用在一个简单的PID闭环上,做在线的参数整定。假设当前PID解算周期是1ms,即每1ms,做一次串口的收包,解包,Pid解算,数据采集,然后打包,发包。也就是说是固定步长的解包。
串口的方案是开启收发的DMA以及DMA的中断。(坚决不考虑直接使用串口中断。一个字节中断一次太费资源)。DMA数组作为串口的FIFO队列(并不是真正意义上的队列)。
当前的需求:
1、时间节拍到来时,检查是否有收到数据。没有则跳出,有则进入下一步
2、检查数据中的包格式,比如包头是否正确,帧长度是否对齐,CRC(目前还没有做进去)等
3、包格式检查出错误,回包时添加标志位,声明包格式错误请求重发。包格式没有错误则进行解包并设置对应的寄存器和赋值。
4、具有合理的接收缓冲区,大于缓冲区的数据进行放弃。
5、能够及时检测出丢字节,多字节等帧长度出错的问题。
几套尝试过的方案:
1、DMA数组的长度和帧长度相等。
触发条件:DMA计数值减到0(即已经收满一个帧的长度的数据)产生DMA中断,将触发标志位写1。PC机上可以通过开启一个线程监视缓冲区数量实现。
解包操作:设置共用体,其中结构体为帧协议,同时公用一个u8 数组作为DMA数组。判断触发条件,若满足,读取共用体中的包头包尾,若正确,继续读取成员,解包赋值。
缓冲机制:无。DMA设置为normal模式,计数减到0后即停止。有新的数据到来也不会被传入数组。PC机上可以手动关闭串口。
报错机制:
帧错位:在包头检查中会发现,舍弃当前帧,设置重发标志位请求重发。
字节缺失:字节缺失的帧发送完成后不会满足触发条件,等到第2帧的数据的前几个字节填满缺失的帧后,触发解包操作。在检查包围的时候,报错响应。舍弃字节缺失帧,但是难以保证字节缺失帧的后几帧能顺利接收。而且出错和报错响应不同步。即报错响应出现在错误的下一帧。
字节超出:字节超出的帧会及时响应,并且由于包尾错误,会立即响应报错并请求重发。
解包过快:不会出现解包速度大于收包速度。因为数据满一个帧长度才会解包。
2、DMA数组指向元素类型为帧结构体的链表
触发条件:DMA计数值减到0(即已经收满一个帧的长度的数据)产生DMA中断,DMA中断中对List进行Push_back操作,增加一个element,然后将DMA的内存地址指向新的element的首地址。触发条件是List的size大于1(在没有收到任何报文之前,得有一个空element用于放置马上要到来的报文);
解包操作:检查List第1个元素的包头包尾,如果正确,读取成员解包赋值,然后对List进行pop_front,直到list的size等于1.
缓冲机制:链表天然的缓冲机制,唯一担心的是堆溢出,可以设置一个上限,在中断里判断。
报错机制:
帧错位:在包头检查中会发现,但是需要丢弃缓冲区内错位帧之后所有的帧。因为后边的必然都错位了。
字节缺失:第2帧到来时,检查包尾时发现。同样存在报错响应不同步的问题。
字节超出:报错同步响应。丢弃缓冲区中所有帧
解包过快:不存在这个问题。理由同 方式1.
3、DMA数组指向多倍于帧长度的数组首地址
触发条件:缓冲队列非空。触发响应后,立即将缓冲队列memcpy到临时数组进行解包。同时清空队列。
解包操作:在临时数组中搜索包头的第1个字节,一单满足,立即检查:包头第2字节,包尾是否在缓冲区长度内,包尾是否正确。如果4个条件均满足,立即开始解包赋值。完成后重复上一步,在数组中搜索第2个包头。直到最后在缓冲区末端,残留帧的前一部分,舍弃该无尾帧。
缓冲机制:由缓冲队列作为缓冲。
报错机制:
帧错位:在临时数组中不存在帧错位的概念,帧错位完全可以被正确解包。
字节缺失:在解包步骤中被检测到包尾有误,则请求重发。而且能同步响应。
字节超出:同字节缺失
解包过快:由于触发方式为缓冲队列非空。如果查询触发条件时,恰好接收了部分帧,则仍然能满足触发条件。那么此时这个接收了一部分的帧会作为字节缺失的帧被舍弃并进行报错
小结:
三种方式对比下来,第3种方式有着较优越的性能,而且能够很好地移植到PC机上实现。但是对于解包过快的问题,仍然需要讨论。
字节缺失同步响应和解包过快的矛盾:
问题可以被化简为:一个10字节的帧,解包时,如果包里只有9个字节,那这一帧到底是没发完还是字节缺失。
如果使用“已收到大于10个字节的数据”作为解包触发条件,那么解包时永远有10个字节,判断最后一个字节是否是包尾,即可。但是字节缺失永远只能在下一帧响应。
如果使用“缓冲区数据多于0”作为解包触发条件,虽然字节缺失能立即被响应,但是也有可能将未发完的帧误判。
因此需要针对当前的应用进行分析。目前对于单片机的帧率和帧长度为:
波特率:115200
发送帧率:5f/s
发送帧长:20 Bytes
接收帧检测周期:1ms
接收帧长度:10 Bytes
传送1Byte数据,由于没有校验位,1个停止位,因此需要10bits。
那么传输速率为11520B/s(约11KB/S),即传输1Byte需要86.8us(约0.1ms)。
发送帧每一帧20Bytes,需要1.736ms(约2ms)
接收帧每一帧10Bytes,需要0.858ms(约1ms)
因此对于当前的情况下单片机的接收条件,1ms解包一次,完全不需要缓冲区,但是却有很大可能发生在帧截断。
因此应该采用“已收到等于帧长度个字节的数据”作为触发条件。放弃字节缺失帧的同步响应。
但是对于PC机端,如果同样为1ms间隔检测触发条件,接收帧的时常变为1.736毫秒,那么一个间隔内是必然收不满1帧的。
因此同样可以采用“已收到等于帧长度个字节的数据”作为触发条件。放弃字节缺失帧的同步响应。
但是对于Qt上的串口类,现在还没有摸清他的工作原理,尚无法讨论何种方法比较合适。
/**********************************11月1日更新分割线****************************************/
一个能够提高缺字节帧报错响应速度的方案:
判断帧是丢字节还是未发完的区分方式其实是在时间上。
比如之前提到的115200波特率,20Bytes的帧,其传送时间应该小于2ms。
因此,当:接收缓冲区有数据,单数据未到达20Bytes时,若这种状态维持超过2ms,则说明传输已经完成,缺字节。
而程序本身的step timer已经有了计时的功能。因此,实现方式如下。
声明一个标志位1:FIFO队列有数据但不满帧长度。
声明一个计数器1:标志位1的计数。
当FIFO队列数据从0跳变到1时,set标志位1。
CheckMailBox时,标志位1已置位,则将计数器1的值加1。
由于20Bytes的帧在2ms内应该发送完。而解算周期为1ms。
故,当计数器1的值大于2时,如果FIFO队列数据长度仍然没有达到帧长度,说明该包有数据丢失。set报错标志位。
即可检测出丢字节的帧。
全部0条评论
快来发表一下你的评论吧 !