一、背景

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

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

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

例如:一个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. Sql Server 自定义进制转换

    Sql Server 自定义进制转换 /*@Str 转化进制的自定义字符 例如 '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ' @Str的长度为转化进制的长度 @valu ...

  2. 网页设计常用颜色16进制代码

    本文转载于:猿2048网站网页设计常用颜色16进制代码 在进行网站网页设计制作的时候,经常需要用到不同的颜色的搭配,效果,网页中标明颜色比较好的是用十六进制数据来进行标注,但是由于颜色很多,这些十六进 ...

  3. java 10进制转64进制_JAVA中实现十进制与其它自定义进制进行相互转换。 - yz124的日志 - 网易博客...

    我们通常用到的数字都是十进制的,日常使用的也是这样,但是在程序中,我们可能还会经常用到二进制.八进制.十六进制的数字.既然程序中会使用到,那么就会有它存在的道理.有些时候,将数字用字符串保存到文件或者 ...

  4. python 自定义进制转换_[python]从零开始学python——颜色的16进制于RGB之间的转换...

    在学习openstack的时候,发现openstack是python开发的:学习mininet自定义拓扑,发现mininet是python开发的:看看ryu,还是python开发的--于是心中升起了自 ...

  5. 生成大小写字母加数字混合ID与自定义进制转换

    有时候可以在别的网站上看到类似于这样的ID : D6pPMSTjOFI, 关于数据库主键的选择园子里面也讨论过许多了,比如这篇 小议数据库主键选取策略(原创). 字符串作ID有时候还是有它的优点的,但 ...

  6. 实训汇编语言设计——内存多字节10进制数相加

    将内存first区多字节10进制数与second区相同10进制数相加,结果保存到dest区 DATA   SEGMENT FIRST  DB 11H, 22H, 33H, 44H, 55H, 66H, ...

  7. 数字电路与逻辑设计 学习笔记【进制转换】

    0.1近代开关理论:Relay-contact Network Theory: 继电-触点网络理论. 0.2,脉冲信号与数字信号, 模拟量->模拟信号:正弦信号.脉冲信号->脉冲电路 数字 ...

  8. python 自定义进制转换,Python 内置函数进制转换的用法(十进制转二进制、八进制、十六进制)...

    使用Python内置函数:bin().oct().int().hex()可实现进制转换. 先看Python官方文档中对这几个内置函数的描述: bin(x) Convert an integer num ...

  9. PAT-B 1037. 在霍格沃茨找零钱(20)(20 分)自定义进制转换

    https://pintia.cn/problem-sets/994805260223102976/problems/994805284923359232 1037 在霍格沃茨找零钱(20)(20 分 ...

  10. Hbuilder编程设计开发吸取16进制颜色值

    最新版QQ的截图有取色器的功能, ctrl + alt + a截图, 鼠标指向颜色块,再按c就能复制rgb代码, esc退出截图, 粘贴就好啦; 如果按住ctrl,按c,复制的就是#颜色代码

最新文章

  1. 2019微生物组—宏基因组分析技术专题研讨会第四期
  2. ubuntu通过apt-get方式搭建lnmp环境以及php扩展安装
  3. 使用SGD(Stochastic Gradient Descent)进行大规模机器学习
  4. jQuery$命名冲突问题解决方法
  5. java时间往后一天_如何在Java中将日期增加一天?
  6. mysql 支持gbk_MySQL不支持GBK编码的解决方法
  7. pythonenumapi_python模块之enum_上
  8. 谷歌修复安卓System 组件中的多个 RCE 漏洞
  9. java组件名词解释_简述Java EE三类组件的构成及运行环境。
  10. 免费pdf转换成txt转换器
  11. C语言printf格式化输出
  12. java emf 转jpg_JAVA读取EMF文件并转化为PNG,JPG,GIF格式
  13. matlab读取jpg图片出错,求助,Matlab读取图片进行分类。出现错误
  14. linux 电源管理 power supply class
  15. 计算机中¥符号按哪个键,人民币键盘符号怎么打 电脑怎么打人民币符号
  16. 黎曼Zeta函数,人类文明永恒的纪念
  17. stm32 esp8266-01使用 get,post 请求数据以及json解析
  18. 论坛项目小程序和h5登录
  19. 史上最全软件测试工程师常见的面试题总结(九)【多测师】
  20. 论文开题报告的研究基础怎么写?

热门文章

  1. 如何调整视频会议系统的声音?
  2. HTML基础标签和框架结构
  3. 微众银行备用金怎么取出来
  4. android中使用JSOUP如何解析网页数据详述
  5. 吃豆人C语言开发—Day1可行性分析
  6. 新能源汽车行驶记录仪行业细分品类应用、区域趋向预见分析
  7. RocketMQ与Kafka差异全面对比
  8. P1893 山峰暸望
  9. Compaq Visual FortranV 6.7 正式版
  10. SQL:COMP9311 SQL