嵌入式平台如何从数据流中提取一帧数据?
通过实例来说明问题:
A作为主机(上位机软件),B作为从机(STM32)硬件和电机连接,A和B之间通过串口通讯,使A能间接的控制电机正反转,速度等。
协议初定:
问题一:怎么判断帧数据开始信息?
通过定义一个固定的帧头,作为数据开始信号。记为0x01。
问题二:如何判断帧的长度?
帧的长度可以选择固定长度或可变长度。固定长度逻辑简单,检测到帧头后,往后读取固定的长度即可。可变长度灵活度高,一帧数据可以携带更多的信息量。那么可以在帧头后增加一个字节作为长度,记为0xXX,以便从机判断帧长。
问题三:数据格式?
数据格式按需编写,这里用16bit功能+16bit数据的方式。一组数据共4个字节。
问题四:如何校验帧完整性?
可以用两种方式:
1、增加帧尾作为数据结束标志,验证帧尾就知道数据是否完整。但是存在数据=帧尾、通讯异常的情况,这时候可能提取到了错误的数据。
2、采用通用校验方式,工业控制一般使用CRC16-MODBUS。这里增加两个字节作为检验码。这种方式可以保证数据不会出错。
至此:我们可以将通讯协议制定下来:
帧头 | 帧长 | 功能1 | 数据1 | 功能2 | 数据2 | 功能… | 数据… | CRC16高位 | CRC16低位 |
---|---|---|---|---|---|---|---|---|---|
0x01 | len | 0xXX | 0xXX | 0xXX | 0xXX | 0xXX | 0xXX | CRC_H | CRC_L |
在理想状态下,从机在串口中断里面判断帧头,然后循环往后读取len
长度的字节到数组里面去,最后验证一下CRC是否一致即可。
但实际使用往往面对更复杂的情形:
问题五:如果一帧数据没有提取完成,下一帧数据又过来了怎么处理?
这个问题其实就是要解决两件事情:什么时候接收数据?什么时候处理数据?
假设提取数据的函数为:UserExtractFrameData();
该函数执行时间为1us。
那么由简入繁看看几种方式:1、中断接收中断处理。(弊端太明显不再赘述)
2、中断保存到buff,中断外处理buff,处理完后buff清空。uint8_t RecvBuff[50]; uint8_t RecvIndex=0; void USART1_IRQHandler(void) //串口1中断服务函数 { if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) { RecvIndex ++; RecvBuff[RecvIndex] = (uint8_t)USART_ReceiveData(USART1); USART_ClearITPendingBit(USART1,USART_IT_RXNE); } } void UserExtractFrameData() { if(RecvIndex > FRAME_LEN) { ... RecvIndex = 0; } }
当传输速率>
UserExtractFrameData()
处理速度时,必然会导致丢数据。因为RecvIndex
被清0了,这篇文章重点不是这个,这里不再赘述。
3、使用环形缓冲区。
环形缓冲区原理网上能找到很多,具体实现方式可以看这里 C环形缓冲区实现使用环形缓冲区就可以很好的解决这个问题,将接收和处理彻底解耦,接收数据自动填入缓冲区,处理数据自动从缓冲区读取数据。当瞬间传输速率很大时,数据被保存到缓冲区不会丢失,等到传输空闲时就有足够的时间去处理。
void USART1_IRQHandler(void) //串口1中断服务程序 { u8 temp; if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) { temp =USART_ReceiveData(USART1); RingBuffWriteByte(temp); USART_ClearITPendingBit(USART1,USART_IT_RXNE); } } void UserExtractFrameData() { uint8_t len = RingBuffGetLength() if(len > FRAME_LEN) { RingBuffReadData(len,buff); .... } }
至此通过CRC和缓冲区,我们已经可以保证数据的完整性和正确性了,只需要从缓冲区拿数据出来比对协议就可以了。
那么接下来再分析一下更加复杂的情形:
问题六:假如主机采用的是一对多的方式,那么必然有大量的无效数据和类似数据,如何处理?
处理方式有很多,这里推荐一种字节流的处理方式。
处理思路:从缓冲区复制一个字节,如果是帧头,继续复制一个字节判断长度n,然后继续往后复制一个字节,判断是否是正确的功能码,再然后继续往后复制n个字节,直到判断CRC是否通过。只要遇到错误就读取帧头不处理即可。
无论主机发送多少数据,内存消耗只是环形缓冲区大小+有效数据大小。未检测到帧头前,处理速度>传输速率,所以环形缓冲区的大小可以不用设很大。但是有一种极端情况,就是主机发送的数据含有大量的帧头,且帧头后的功能码检测也是通过的,那就没救了,只能增大缓冲区大小。
经过验证可以高效率提取有效数据。3KB字节中提取最长32byte的有效数据,时间消耗100ms~220ms左右,主要是传输过程占用的时间。内存消耗仅100+32byte。
因为是给别人做的项目,这部分代码就不贴了~
问题七:假设数据长度较长,传输时间>>1us,传输数据到一半时,进入了处理数据的函数,处理函数由于没有接收到len
长度的数据而判断为数据不完整错误,如何处理?
这个问题很好解决,我们可以再增加一个缓冲区
pbuff[]
,先将数据搬到pbuff
,直到接收到了len
长度的数据,再去处理pbuff
即可。
但这不是最好的解决方法,假如数据大小是1KB,那么环形缓冲区大小需要>1KB,再来一个pbuff
也需要1KB内存,增加了一倍内存消耗。
因此在C环形缓冲区实现这里,我增加两个接口
uint8_t RingBuffCopyByte(uint8_t *pdata);
uint8_t RingBuffCopyData(uint8_t len, uint8_t *pdata);
处理函数将环形缓冲区数据复制到内部进行处理,处理结束后才会读掉n个字节,缺点是程序执行时间有所增大。