一 需求:
1.每帧的数据量:20~1045字节之间
2.数据频率:10ms级别
3.能够识别错误帧和正确帧,并丢弃错误帧,留下正确帧
4.数据采用两个字节的头码(7E 7E),中间有数据长度,最后一个字节为CRC8校验码
二 问题
由于发送端的数据通过串口到达接收端时,每帧数据不是完整一次性发过来的,可能会分成2包、3包
或者4包数据发过来(也就是说一帧数据分成多少包发过来,我们是不知道的),那么如何接收到一帧
完整的数据,则是我们必须要解决的了。
三 解决思路
一开始遇到这个问题时就想,既然一帧数据分成几包发过来,那么也就是说其实想要收到一帧完整的数据,
是需要一定的时间的,那么只要给它设置个定时不就行了吗?思路如下:
在串口接收到数据时,就启动一个定时器,开始计时,待定时时间一到,就从接收缓冲区中取出这帧数据,
同时将定时器清0,以便下一帧数据到来时启用。
有了思路,代码就好办了。敲好代码,编译,上电,测试。发现还是有效果的,但是尴尬的事情来了。
当通讯的每帧数据的长度相差不大时,这个方法还是挺奏效的,能够正确分出每帧数据。但当通讯的每帧
数据长度相差悬殊时,如20个字节和1045字节,结果就比较尴尬了。这个时候,定时的时间长度就不好定了。
因为接收1045和20个字节的时间势必不一样,这时如果为了接收1045字节的数据而把定时时间定长了,则会出现
粘包的情况(指接收时,原本发送是两帧数据的,接收成一帧数据了);而出现粘包的情况,则是因为定时的时间
定长了,于是将定时时间改短点,则接收较长的数据时,则出现了断帧的现象。总之,一句话,定时时间不好把握。
这个方法夭折了。下面看看另外一种解决方法。
观察每帧数据,其实都是由头码、数据长度、数据内容、CRC校验码组成,那么我们是否可以从这些特征中着手呢?
当然可以。只要每次去判断缓冲区中出现7E 7E(头码)的地方,然后从这个地方开始读,先获取到这一帧的数据长
读(存放数据长度的位置是固定的),再根据这个长度去读取这一帧数据,这样就拿到一帧完整数据了。当然是否可行
还得测试之后才知道。敲好代码,编译,上电,测试。效果不错,长数据和短数据都能够顺利接收。但这样的接收处理
逻辑还不足以胜任,因为一旦数据内容中也含有头码的内容(数据内容也出现7E 7E,暂称伪头码),而当我们搜索7E 7E
是通过遍历的方式的,也就是说,数据内容中的7E 7E 有可能会被我们误认为是头码,这时我们会将伪头码之后的数据当
成一帧正确的数据(其实是错误的数据)交给程序进行解析处理。那么在这里加上CRC校验码进行检测该帧数据是否为正确
数据则是非常必要的了。下面再完善下这个接收处理逻辑:
有一个缓冲区(uartbuf)用来存放串口每次接收到的数据,串口收到数据后,我们就直接判断缓冲区的头两个字节是否为
头码内容,如果符合要求,则根据数据长度接收完这帧数据,之后进行CRC校验判断,若能满足,则表示这帧数据是对的。
若这帧数据不能满足CRC校验,则说明这是一帧错误是数据,有可能我们拿到的头码是伪头码,此时我们需要重新处理这帧
数据,将伪头码遗弃,再在缓冲区中遍历出现头码的地方,然后将其以及之后的数据复制到缓冲区的头部(主要是为了方便处理了)。
如果我们一开始判断缓冲区的头两个字节不是头码内容时,则我们判断第三、第四个字节,以此类推,直至找到头码内容。有个原则,
我们要保证缓冲区的头两个字节为头码内容,这样做只是为了方便处理。
下面贴出代码:
dataIndex += uart_length; //dataindex为缓冲区索引,而uart_length则表示此次串口接收到的数据长度
if(dataIndex >= UART_BUF_SIZE){ //做防边界溢出处理
dataIndex = 0;
continue;
}
if(dataIndex >= 18 && uartbuff[0] == 0x7E && uartbuff[1] == 0x7E){ //判断当前缓冲区中的数据长度是否满足18,主要是为拿到该帧的数据长度 &&判断是否满足头码内容
rxlenth = ((uint16_t)uartbuff[16] << 8)+(uint16_t)uartbuff[17]+19; //拿到数据长度
if(rxlenth == dataIndex){
if(uartbuff[rxlenth-1] == CalcCheckSum((uint8_t *)&uartbuff[2] , rxlenth-3)){ //CRC校验检测
memcpy(uartbuf , uartbuff , rxlenth);
recvFlag = 0x8000;
dataIndex = 0;
}else{ //若不能满足CRC校验,则丢弃伪头码
if(uartbuff[2] == 0x7E){ //需要注意此时有可能是3个头码内容连在一起,即7E 7E 7E,有可能后两个头码内容才是正确的
memcpy(uartbuff , &uartbuff[1] , dataIndex-1);
dataIndex = dataIndex -1;
}else{
memcpy(uartbuff , &uartbuff[2] , dataIndex-2);
dataIndex = dataIndex -2;
}
}
}
else if(rxlenth < dataIndex){ //有可能缓冲区中数据长度大于当前帧的数据长度,即含有下一帧数据的部分内容
if(uartbuff[rxlenth-1] == CalcCheckSum((uint8_t *)&uartbuff[2] , rxlenth-3)){
memcpy(uartbuf , uartbuff , rxlenth);
recvFlag = 0x8000;
memcpy(uartbuff , &uartbuff[rxlenth] , dataIndex-rxlenth); //需要复制到缓冲区前部,与下一次串口接收到的内容进行组帧
dataIndex = dataIndex-rxlenth;
}else{
if(uartbuff[2] == 0x7E){
memcpy(uartbuff , &uartbuff[1] , dataIndex-1);
dataIndex = dataIndex -1;
}else{
memcpy(uartbuff , &uartbuff[2] , dataIndex-2);
dataIndex = dataIndex -2;
}
}
}
}else{ //用来处理当缓冲区的头两个字节不是头码内容的情况,这时需要遍历缓冲区,并将头码内容以及之后的数据复制到缓冲区前部
if(dataIndex >= 18){
for(i = 0;i<dataIndex-1;i++){
if(uartbuff[i] == 0x7E && uartbuff[i+1] == 0x7E){
memcpy(uartbuff , &uartbuff[i] , dataIndex-i);
dataIndex = dataIndex-i;
i = 0;
break;
}
}
if(i == dataIndex-1){ //用来处理当遍历完缓冲区,找不到头码内容时,需要将这些数据都丢弃
dataIndex = 0;
}
}