一种串口软件架构的设想与实现

    串口(UART)是做嵌入式软件开发最常用到的外设。一般情况下,通过串口收发的数据,都是带有协议的。如何高效地组织数据,接收并解析数据,就成为软件开发首要考虑的事情。不同的协议,组包和解包的过程不同,但是如何安排串口数据收发,是能够影响到组包和解包程序的编写的。我这里想重点考虑怎么安排串口收数据的问题。

   最直接的做法,简单粗暴,在RAM中开辟一个数组。一般串口都会设置中断接收,然后在中断服务函数里,直接就把串口收到的一个字节的数据拷贝到数组中,然后索引值index递增。但是这样子话,怎么取数据,就是个问题了。

除非你一次性把收到的数据都读出来,要不然你取一部分数据出来后,index该咋办,在这种思路下,index不能动,只能收到一个字节加累加一次,直到数组的末尾。index不动,就不能指示数据取出来后,数组有了多余的空间存储数据。如果你想取数据出来后,把剩下的数据往左搬移,那这效率也太低了。除此之外,index累加到数组的末尾之后,该咋办,又回到头么,这样子有可能覆盖原来的数据。总之,问题多多。

    一种比较好的方法就是环形缓冲。一样在RAM中开辟一个数组,不同的时候,这次我们定义一个读索引r_index,一个写索引w_index,还有一个计数cnt。收到一个字节的时候。写索引累加,计数累加,读索引不变。这样子我们就可以知道数组里收到了多少个字节的数据。读取数据的时候,也没有限制了,想读多少字节都可以,不要超过计数cnt就好,读的时候,读一个字节读索引累加,计数cnt减一。因为有cnt,我们知道读取之后,数组空间有多余的空间了。之所以叫环形,就是因为,不论是读索引还是写索引,在累加到数组末尾之后,都可以从头开始,不用担心覆盖的问题,当然前提是用cnt来辅助判断。比如说,写索引到尾了,如果计数cnt比数组的大小小,数组头部的数据被读取了,那就可以让写索引回到头。

一种串口软件架构的设想与实现_第1张图片

有了环形缓冲,存取串口收到的数据更有效率了。但是对应协议的解析来说,还不够,还是有点问题。

  第一个问题就是,当你需要查询是否收到数据的时候,你应该读多少个字节出来。要知道很少有协议帧是固定长度的。我刚毕业时去的那家公司,别的工程师就是直接把串口里所有的数据取出来,然后进行解析。如果还没收到完整的一帧,或者收到了一帧半,这个时候就要丢包了。

  一种解决办法,是逐个字节读。协议一般都会有帧头帧尾长度信息,逐个字节读取,如果读到的不是帧头,直接丢弃,直到读到帧头。这时说明接下来是有效帧数据,这个时候可以把读到的数据保存起来。如果协议里有长度信息,可以直接把剩下的读出来,如果没有,就只能读到帧尾为止。但是这种办法仍旧没法解决残缺帧的问题。如果帧收到一半还没收完整,你就读,还是没法顺利解析。一种常见的做法就是延时,意思就是说,我不知道什么时候会收到完整一帧,但是我延时,延到足够长的时间,保证肯定能收到一帧。但是这样软件的实时性就差了。

    说句题外话,上面我讨论的这种架构,都是考虑分层的情况,串口收发数据,不管协议的事。协议层去取串口的数据来解析。工作中有时候会遇到有人不分层的,直接就是串口收到一个字节就调用协议层的解析函数,这种架构咱就不考虑了。

   为了解决残缺帧的问题,使得软件能够做到既不丢包,又能实时响应。我见过别人用了一种这样的方法,在串口中断接收函数中,加入一些跟协议相关的代码,先别喷我说不分层。虽然加入了协议相关的代码,但是很克制,并不解析数据,只是判断帧头帧尾。具体是这样地,假如说协议的帧头是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;
    }
}
/**

 

你可能感兴趣的:(代码片段)