串口(UART)是做嵌入式软件开发最常用到的外设。一般情况下,通过串口收发的数据,都是带有协议的。如何高效地组织数据,接收并解析数据,就成为软件开发首要考虑的事情。不同的协议,组包和解包的过程不同,但是如何安排串口数据收发,是能够影响到组包和解包程序的编写的。我这里想重点考虑怎么安排串口收数据的问题。
最直接的做法,简单粗暴,在RAM中开辟一个数组。一般串口都会设置中断接收,然后在中断服务函数里,直接就把串口收到的一个字节的数据拷贝到数组中,然后索引值index递增。但是这样子话,怎么取数据,就是个问题了。
除非你一次性把收到的数据都读出来,要不然你取一部分数据出来后,index该咋办,在这种思路下,index不能动,只能收到一个字节加累加一次,直到数组的末尾。index不动,就不能指示数据取出来后,数组有了多余的空间存储数据。如果你想取数据出来后,把剩下的数据往左搬移,那这效率也太低了。除此之外,index累加到数组的末尾之后,该咋办,又回到头么,这样子有可能覆盖原来的数据。总之,问题多多。
一种比较好的方法就是环形缓冲。一样在RAM中开辟一个数组,不同的时候,这次我们定义一个读索引r_index,一个写索引w_index,还有一个计数cnt。收到一个字节的时候。写索引累加,计数累加,读索引不变。这样子我们就可以知道数组里收到了多少个字节的数据。读取数据的时候,也没有限制了,想读多少字节都可以,不要超过计数cnt就好,读的时候,读一个字节读索引累加,计数cnt减一。因为有cnt,我们知道读取之后,数组空间有多余的空间了。之所以叫环形,就是因为,不论是读索引还是写索引,在累加到数组末尾之后,都可以从头开始,不用担心覆盖的问题,当然前提是用cnt来辅助判断。比如说,写索引到尾了,如果计数cnt比数组的大小小,数组头部的数据被读取了,那就可以让写索引回到头。
有了环形缓冲,存取串口收到的数据更有效率了。但是对应协议的解析来说,还不够,还是有点问题。
第一个问题就是,当你需要查询是否收到数据的时候,你应该读多少个字节出来。要知道很少有协议帧是固定长度的。我刚毕业时去的那家公司,别的工程师就是直接把串口里所有的数据取出来,然后进行解析。如果还没收到完整的一帧,或者收到了一帧半,这个时候就要丢包了。
一种解决办法,是逐个字节读。协议一般都会有帧头帧尾长度信息,逐个字节读取,如果读到的不是帧头,直接丢弃,直到读到帧头。这时说明接下来是有效帧数据,这个时候可以把读到的数据保存起来。如果协议里有长度信息,可以直接把剩下的读出来,如果没有,就只能读到帧尾为止。但是这种办法仍旧没法解决残缺帧的问题。如果帧收到一半还没收完整,你就读,还是没法顺利解析。一种常见的做法就是延时,意思就是说,我不知道什么时候会收到完整一帧,但是我延时,延到足够长的时间,保证肯定能收到一帧。但是这样软件的实时性就差了。
说句题外话,上面我讨论的这种架构,都是考虑分层的情况,串口收发数据,不管协议的事。协议层去取串口的数据来解析。工作中有时候会遇到有人不分层的,直接就是串口收到一个字节就调用协议层的解析函数,这种架构咱就不考虑了。
为了解决残缺帧的问题,使得软件能够做到既不丢包,又能实时响应。我见过别人用了一种这样的方法,在串口中断接收函数中,加入一些跟协议相关的代码,先别喷我说不分层。虽然加入了协议相关的代码,但是很克制,并不解析数据,只是判断帧头帧尾。具体是这样地,假如说协议的帧头是0XA5。那么在串口的中断函数里,判断收到的数据是不是0xA5,如果不是就直接丢弃。在收到0XA5后,接下来收到的数据就都存储,但是也有加判断,判断帧尾是否到达,或者没有帧尾但是有长度信息,靠长度信息来判断,一帧收完没。收到后,就又回到开始判断接收的数据是不是0XA5,并且这个时候可以通过一些手段来通知上层软件有帧到达,具体可以是消息或者信号量都行。甚至这时候,还可以知道收到几个字节了。这样子就可以做到解决残缺帧,又能及时响应。
但是总归是在底层中掺杂了上层的东西,有些别扭。我是想法是把这个机制封装一下。串口部分,提供一个接口,让上层传递一个函数指针给它。它在串口收到数据后,调用这个函数。而上层,单独把判断帧头帧尾的功能独立成一个函数,函数力求精简,再把这个函数传递给底层的串口。除此之外,串口还提供接口,可以查询缓冲收到多少帧,可以按帧直接读取数据。想想可以按帧读取数据,上层解析协议的时候多方便,不用管要取多少数据,反正一取就是一帧。帧头帧尾的判断都可以省下来了,只要检验帧有没有出错,没有错就可以开始解析。
数据结构如下,frame,frameInfo都定义为环形缓冲区,一个用来存储帧,一个用来存储帧的长度。frameLen,是用在接收帧的过程中记录帧长度。filterFun是函数指针,就是上面说的协议的帧头帧尾判断。
/////////////////TYPE DEFINE ////////////////////
typedef union{
uint8_t Bytes[2];
uint16_t HalfWord;
}FrameLen_t;
//双环形缓冲区,一个存储串口输入的数据,一个存储帧长度信息。
typedef struct{
cbuf frame; //存储串口输入的数据
cbuf frameInfo; //存储帧长度信息
uint16_t lastWIndex; //环形缓冲区中当前帧的开始位置
FrameLen_t frameLen;
SerialFilter filterFun;
}SerialfCache_t;
下面这个函数在串口中断中调用。对了,上层提供的过滤函数,返回的结果定为三种,一种是无效,则收到的数据可以丢弃,一种是有效,则进行存储,并记录帧长度信息,最后一种是帧结束,则把帧长度信息记录下来,保存到环形缓冲中。
/**
* @brief 串口通用回调函数,所有的串口接收中断调用
* @param data 串口收到的数据
* serialNum 串口编号
* @retval
*/
void SerialCommCallback(uint8_t serialNum,uint8_t data)
{
uint8_t res;
//调用过滤函数,判断是否需要存储数据
if(SerialCacheTbl[serialNum].filterFun == 0){ //如果不启用过滤,则直接存储
res = FILTER_STORE;
}
else{
res = SerialCacheTbl[serialNum].filterFun(data);
}
switch(res)
{
case FILTER_REMOVE:
{
if(SerialCacheTbl[serialNum].frameLen.HalfWord != 0)//如果之前收到一帧的数个字节,之后过滤函数返回丢弃
{ // 则写指针要回退到帧起始的位置,相当于丢弃之前接收的半帧
SerialCacheTbl[serialNum].frame.wIndex = SerialCacheTbl[serialNum].lastWIndex;
}
break;
}
case FILTER_STORE:
{
if( CBufPutChar(&SerialCacheTbl[serialNum].frame,data) == 1)
{
SerialCacheTbl[serialNum].frameLen.HalfWord++;
}
break;
}
case FILTER_FRAME: //接收完一帧,则要把则一帧的长度存入帧信息缓存
{
CBufPutChar(&SerialCacheTbl[serialNum].frame,data);
SerialCacheTbl[serialNum].frameLen.HalfWord++;
CBufPutData(&SerialCacheTbl[serialNum].frameInfo,SerialCacheTbl[serialNum].frameLen.Bytes,2);
SerialCacheTbl[serialNum].lastWIndex = CBufGetWIndex(&SerialCacheTbl[serialNum].frame);
SerialCacheTbl[serialNum].frameLen.HalfWord = 0;
break;
}
default:
break;
}
}
上层取数据的时候,就从帧信息的环形缓冲中,取出帧长度的信息,然后从帧数据的环形缓冲中,取出一帧。
/**
* @brief 帧数据读取
* @param serialNum 串口号
* buff 接收指针
* bufflen 接收长度指针
* @retval 1 读取成功
0 读取失败
*/
uint8_t SerialReadFrame(uint8_t serialNum,uint8_t* buff,uint16_t* bufflen)
{
FrameLen_t len = {0};
if(CBufGetDataLen(&SerialCacheTbl[serialNum].frameInfo) == 0)
{
return 0;
}
else
{
CBufGetData(&SerialCacheTbl[serialNum].frameInfo,len.Bytes,2);
CBufGetData(&SerialCacheTbl[serialNum].frame,buff,len.HalfWord);
*bufflen = len.HalfWord;
return 1;
}
}
/**