通信协议应如何设计才好?自定义进制帮你忙

一、背景

        前段时间联系了一家纯软件开发公司做上位机系统开发,其中就包含跟下位机的无线组网控制。因为上下位机联动组网与纯逻辑上的组网不同,它通常对实时性有要求,所以制订高效的协议非常重要,甚至还需要一些特殊的控制逻辑。但是令我大跌眼镜的是,软件公司的项目负责人竟然以为,只有百度上能搜索的那些主流协议才是协议,自定义的协议不是协议,他软件上出现的各种问题,大多归过于一个自定义的协议。

        我们知道,即使是那些主流的协议再完美,也未必适合我们的应用场景。更何况一些主流协议在完美性上,并不见得有突出性。

        下面,我们就从一个常见的通信,向协议的方向开始说起。

        例如:一个RS485总线网使用9600、n、8、1方式通信,使用主从应答式控制架构(如图1)。


 

        我们粗估一下,按一个字节1起始位、1停止位、8数字位估算,串口传输一个字节需占10bit,这样一来,9600bps的满负荷传输的情况下,一秒钟也就能传输960个字节。如果n=16,每一秒钟,每次主从应答的最多传输字节数就是60字节。假如一次主从应答需要20字节,那么,一次问答的时间td=20/960=0.0208s,那么1s钟内这个网络正常运行的繁忙度Eb就是:33.3%,空闲度Ei就是:100%-Eb=66.7%(如图2)。

二、事实

        显然,图2的描述不是真相。真相应该是一轮接一轮的信号传输(如图3)。

        从图3中我们可以看出,关注繁忙度已经没有意义了,取而代之的应是一个网络通信周期,也就是每一个从节点相邻两次通信的时间间隔。示例中的通信周期tc就是0.333s,如果说成响应速率,那就是3次/s,或1次/0.333s。

        然而,图3这是太理想了,也不能代替真相。因为这些节点在实际工作中很可能会出现异常情况(如图4)。

        很显然,我们不能为此一直等下去。为了处理故障,我们得规定一个策略,我们可以规定一个等待超时机制。即当某个节点没有响应时,主机等待一会儿。这个一会儿是多大一会儿呢?我们定个上限tx,比如10倍的正常问答时间(td×10=0.208s)。

        这样一来,问题显然就变得有点扑朔迷离了。因为谁也无法预知哪个节点什么时候会出现通信障碍,也就很难预知tc是多少,不过,tc的范围还是可以确定的,即td×n~tx×n。因此运行的状态,就会发生一些变化(如图5)。

        当然,无论是图3或图5,网络的实际运行状态都是Eb≈100%,而当tc出现波动时,就会给网络的使用体验也跟着发生变化,严重时,还会出现卡顿现象。

        实际使用时,慢可能对用户体验的伤害不一定是最大的,而那种时通时卡可能会让用户更加难以接受。因为慢得稳定,可能只是产品的档次问题,而一卡一顿的显然就是故障。

        这种通信思想还有一个问题就是没有联线状态。这样一来,当某个从机出现故障时,主机无法预知其是否能够恢复而将其踢出扫描队列,它必须在每轮轮到时继续对其发命令,等待响应,直到超时。

        显然这种通信思想很呆板,但是却因其简单易懂而极有市场。

三、变通

        尽管省事使人快乐,但发展的绚烂总能吸引躁动的智商。

        现在我们提出一个新需求:即使16个从机坏了15个,另外一个也能稳定工作。这是否能够做到呢?

        答案是肯定的。

        即,为每一个节点分配固定的使用时间tt,tt由必需时间tm与备用时间tn构成,通常情况下,tn=k×tm(k>2)。因此,tt=a×tm(a=k+1,常数)。在tt内,节点无论是正常还是异常,做什么处理,都不会改变tt的大小。然后,tc就固定成tt×n了(如图6)。

        当然,如果按这样的方式通信,实践中还是行不通的,因为我们不能忽视了用户体验。比如,我们对其中某一个节点操作,或观察某一个节点的变化,得有连续性。如果每0.2s响应一下用户的干预,用户还能错觉为有即时性,但如果超过0.5s才能响应一下,我们就会明显感觉到迟滞。

        因此,若要让一个网络控制具有即时感,就必须减少的tm值。

        减少tm的值,有两个办法。其一是提高通信速度,以速度换取时间。比如,当无线对等节点通信(通信道)中,可用小忙闲比减少与解决通信碰撞(如图7)。

        当然,提高通信速度,是需要付出代价的。而且有时候,在某个地方的一点点改变,还会引出一系列的连锁影响。所以这时候,我们更希望有一种付出的代价小,但是却收效明显的办法,这就是其二了:设计高效的通信协议。

四、高效通信协议设计

        我们知道,一个通信协议通常要发送两种内容,即命令与数据。这是典型的数字技术形式,跟汇编指令相同。

        而就通信而言,它并不管你当前传输的是命令还是数据,统统都只是一种数据,并且常常是以字节为单位。显然,这就要求我们必须在字节上做文章,设计一种约定,让字节既能表示数据,又能表示命令。这个约定就是我们要设计的协议,它是由一定数量的字节组成的一个数据集,也称为帧。

        在宽松的情况下,设计协议可以用定长的。即用最长的数据集作为通用格式,这样每一条命令或数据帧都能使用这个通用格式表示。这样一帧通常有下列要素,每一个帧要素都由若干个字节组成(如图8):

        显然,这种定长协议容易造成帧数据浪费,因为常常会有很多指令并不需要太多字节。可不要小看一个帧里浪费几个字节的恶果,比如一个只需要5字节的命令被你套入一个10字节的通用格式里,你的帧通信速率就下降了一倍。

        所以,设计高效的通信协议,唯一的途径就是减少数据帧的字节数,增加字节的涵义。

        做过通信的人都知道,保证帧数据的正确性与可验证性是数据传输的基本责任。因此一些人在设计帧头的时候,为了让帧头显得独具一格,而使用一个多字节的组合,这显然是不可取的。

        高效的数据帧要求每一个帧要素能用一个字节表示,决不能用两个字节,用两个字节表示的,尽量想办法改用一个字节表示,同时还要兼顾数据传输的正确性与可验证性。这看似很难做到,但是我们分析需求,一切皆有可能。

        首先,我们研究一下字节。一个无符号字节,能表示0~255,共256,我们称这个数据范围为一个字节的表示空间。很显然一个字节的数据空间能表示256个数据。

        其次,我们研究一下数据量。比如我们要以厘米为单位表示身高,身高的范围是10~200,很显然一个字节足以表达身高量。但是如果以毫米为单位,则身高的范围是100~2000,这显然就超出了一个字节的表达能力,因此必须就使用两个字节了。而两个字节的表示空间则是216,能表达62236个无符号整数,用来表示100~2000,这显然是太过奢侈,但似乎又别无选择。

        第三,我们研究一下帧元素。

        1) 帧头:作为一帧数据的唯一性标志,它对应的计算机数(二进制数)应该是唯一的。为了达到这个目标,有些协议选择了ASCII码的帧格式。因为ASCII有丰富的字符集,随便抽出一个非数字字母字符来表示帧头,就能与要传输的数据区分开来,例如‘*’,‘;’等,都可以作为帧头,也可以保证该字符在帧数据中的唯一性。但是这样做带来的副作用是,如果要表示数据100,则需要用‘1’、‘0’、‘0’三个字符,占3个字节,这显然就造成了大量浪费,这种浪费会随着数据量的增加而显著增大。有些协议为了不让数据表示浪费字节,就在帧头上下功夫,将帧头复杂化,使用二进制帧格式,从而大大降低帧头与数据重复概率。例如用AB、BC、CD三个字节组合称帧头。很显然3字节帧头比1字节帧头多用了2个字节。

        2) 命令:这是网络控制的重要手段,因此,设计合理高效、组合力强的命令系统也非常重要。命令数量的多少,决定所需命令空间的大小,因此在设计命令时决不能大手大脚,随便定义。而且,在网络控制中,网络通信是处理速度最慢的环节,因此我们应多倾向于提升网络通信速度,牺牲MCU的处理速度(影响极小),多用简单多义命令,让装帧、解析多花点时间,通信少花点时间。例如一个设备要对另一个设备写数据、设置参数,有临时设置参数、永久设置参数、写入状态数据、记录测量数据等不同的写操作,此时我们可以使用不同的命令来表达这些操作,也可以用一条写命令加上统一编址的方式来实现这些操作,这样做会大大减少指令数量,从而对减小指令空间有益,而至于两边的MCU如何switch..case,则完全可以忽略不计。

        3) 校验:校验也通常是多字节的,但如果单字节能够胜任的话,当然是选择单字节。当然,字节越多,通常严谨性也越高,但是并不是我们一定要使用严谨性最高的方式就是最好。校验尽管很重要,但并不是必须的。例如你有一条命令协议,由帧头与命令两个字节组成,而在实际解析时,帧头与命令都时确定的,在解析时都会精确检查,如果帧头错或命令错,都会导致接收失败,这个时候你如果加个校验,就毫无意义。校验只适合那种非精确检查或无法精确检查的场合。

        4) 帧尾:帧尾并不是必须的。但是帧头配帧尾看起来让帧显得严实。有帧尾可以明确通知接收方一个数据帧结束。但是在一些场合,帧尾存在的意义不大。一是定长帧,只要通过统计帧长度即可判断帧结束;另一是魔法师观点,为了提高多线程并行能力,降低某个线程一次执行过多占用MCU,在接收帧时边接收边预处理,能确定变长协议中的当前接收帧的确定长度。当然,有一种习惯是接收端接收帧的过程中只储存数据,不做任何其他处理,这时候靠帧尾标识帧结束就非常重要,但是一旦通信出错,无法收到一个帧的结束标志,就会导致一直等待,很容易造成错帧,如果帧缓冲区没有足够余量还容易造成溢出。当然,在情况不糟糕的情况下,如果帧头具有清晰的唯一性,还是能在下一帧被准确同步的,如果帧头可能与数据撞码,可能就只能靠超时机制去结束无法完整结束的帧了。

        有了对协议帧的这些分析,现在我们要考虑协议的设计了。

        首先,我们考虑核心的数据对空间的需求。我们还是以身高体重测量结果为传输内容为例。身高以毫米为单位,测值范围为0~2000,一个字节放不下,至少要两个字节的空间。体重以0.1公斤为单位,测值范围为5~2000,同身高一样,也需要两个字节。但是,很显然,我们用两个字节的空间表示身高体重十分浪费。因此,我们应该考虑将一个字节的空间一分为二,来提高字节空间的利用效率了。

        接着,我们应考虑控制命令对空间的需求。简单地说,就是我们有多少条命令我们得规划清楚,规划不清楚的,我们得预留一定的扩充余地。对于身高体重测试仪的常用命令有测量、取消测量、读取成绩、设置参数、启动自检、读取设备信息、读取电量等,所以命令数量在10条左右。

        根据这个需求,我们可以通过引入自定义进制,来将命令与数据,压缩在一个字节中表示。

        人类传统的计数进制是十进制。

        而计算机技术中流行的进制有二进制、八进制、十六进制,依此推广开来,我们在这里引入128进制。128是27,即占用7bit,表示数的范围是0~127,正好是一个字节空间的一半。这样一来,一个字节空间就被我们一分为二,一半用于数字,一半用于命令。这是个美好的设想,下面我们只需来验证一下这个设想的可行性即可。

        首先,我们仍用两字节各7bit表示数字,则此时的两字节的数字空间为0~128×128-1,即0~16383。这个范围完全能容纳身高体重的测量值,所以这样做数字空间没有问题。

        128进制使用7位二进制位,在装帧与解析时可以避免一些乘除运算,这样对于一些低端MCU可以减少运算上的额外负担。

        其次,一个字节的另一半空间(128~255)用于表达命令,可以表达128种不同的命令,这对于我们网络控制身高体重测试仪仅需的10来个命令来说,具有充足的余地。

        既然命令空间十分宽敞,我们甚至可以将帧头这种特殊的控制字符也认为是一种命令。这样帧头与命令同在一个空间,自然就可以保证帧头的唯一性特征。而当控制命令极少时,甚至可以将帧头与命令同用一个字节,此时只需用半字节帧头、半字节命令即可。

        从上面的分析我们可以看出,128进制对我们这个例子有百利而无一害,因此我们就可以进行协议的着手设计。我们采用半字节帧头半字节命令方式,变长帧,帧分命令与应答两种。

        〇、说明:

                * 字节:

                * 帧:

                * 帧中数字均用十六进制表示

                * F&C:帧头/命令。长度1B,高半字节为帧头(bit7=1),低半字节为命令

                * Addr:仪器地址,1~127,0为通用地址。长度1B,bit7=0

                * P:参数。长度1B,bit7=0

                * D:长度1B,单字节数据(bit7=0)/后续数据包长度(bit7=1,长度不计bit7)

                * DiL:第i个数据低字节(i≥0),长度1B,bit7=0

                * DiH:第i个数据高字节,长度1B,bit7=0

                * Chk:校验。Chk=(T0^T1^…Tn)&7F,长度1B,bit7=0

        一、控制命令帧1:

        F_C:A?

                A1:开始测量

                A2:测量停止/中止

                A3:读取成绩

                A4:读取型号

                A5:读取电量

        二、控制命令帧2:

        F_C:A?

                AA:设置地址,P为新地址

        三、应答制命令帧1:

        F_C:B? (0≤?≤9)

                B0:OK、确定

                B1:ERR、取消

        四、应答命令帧2:

        F_C:B? (A≤?≤F)

                BA:返回数据

五、协议分析

        上面的协议设计非常简练,但也考虑了可扩充性。如果考虑自定义扩充的话,几乎能适用于各种场合。

        但是,简练既是优点,也是缺点。

        因为信息简练,就造成了应错能力下降,但是换来的是通信速度的提升,特别是在一些网络容量大的无线通信场合。

        上面的应答采用了不同的帧头,这样应答机制鲜明,不过这样会增加一点解析的复杂度。

        如果在另外一些场合,出现了数据空间不够,我们在考虑指令空间够用的情况下,可以把进制数增加,以减少指令空间来增加数据空间。例如我们使用240进制。

        240(F0)进制下,一个字节的数据空间为0~239,这样两个字节的数据空间便为0~57599,明显比0~16383这个范围大了数倍。

        此时,F0~FF为命令空间,共有16个可用数,能胜任最多到16条指令的控制场合。当然,如果仅仅将指令空间的16个数作为特殊控制字符,用来标志帧头或帧尾,那么就可以通过增加一个字节的方式来定义更多的命令,此时的命令空间最大可以定义到0~255。

六、仪器端解析示例代码
#define RBLEN                8                // 定义接收缓冲区大小

unsigned char RcvBuff[RBLEN];        // 接收缓冲区定义

unsigned char RcvPtr = 0;                // 接收指针定义



// 接收时,负责按照所定义的协议检查数据,对于无效数据直接丢弃

// 若收到合法协议帧,则发出通知

// 一帧数据接收完后必须在下一个数据到来之前处理完毕,否则会造成丢帧

void USART1_IRQHandler(void)

{

        static unsigned char Csc = 0;

        static unsigned char Cmd = 0;



        if(USART1->SR&(1<<5))

        {

                unsigned char Rd=0;

                Rd=USART1->DR;

                if((Rd & 0xF0) == 0xA0)        // 帧头识别

                {        // 因为仪器只受控,所以只解析控制命令

                        RcvPtr = 0;                // 无条件同步

                        Csc = 0;

                        Cmd = 0;

                }

                if(RcvPtr<4)

                {

                        RcvBuff[RcvPtr++] = Rd;        // 帧数据存入缓冲区

                        Csc ^= Rd;                // 计算校验

                        switch(RcvPtr)

                        {

                                case 2:

                                        Cmd = Rd & 0x0F;

                                        break;

                                case 3:

                                        if(Cmd>0 && Cmd<6)                // 合法命令

                                        {

                                                if((Csc&0x7F)==Rd)         // 校验

                                                {

                                                        RcvPtr |= 0x80;                // bit7=1为一帧接收完毕标记

                                                }

                                        }

                                        break;

                                case 4:

                                        if(Cmd==0x0A)                // 合法命令

                                        {

                                                if((Csc&0x7F)==Rd)         // 校验

                                                {

                                                        RcvPtr |= 0x80;                // bit7=1为一帧接收完毕标记

                                                }

                                        }

                        }

                }

        }

        

        if(USART1->SR&(1<<6))                                // 发送中断发生

        {

                USART1->SR &= ~(1<<6);                // 清除中断标记

        }

}

---------------------
作者:yyy71cj
链接:https://bbs.21ic.com/icview-3249728-1-1.html
来源:21ic.com
此文章已获得原创/原创奖标签,著作权归21ic所有,任何人未经允许禁止转载。 

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值