通信协议应如何设计才好?自定义进制帮你忙
一、背景
前段时间联系了一家纯软件开发公司做上位机系统开发,其中就包含跟下位机的无线组网控制。因为上下位机联动组网与纯逻辑上的组网不同,它通常对实时性有要求,所以制订高效的协议非常重要,甚至还需要一些特殊的控制逻辑。但是令我大跌眼镜的是,软件公司的项目负责人竟然以为,只有百度上能搜索的那些主流协议才是协议,自定义的协议不是协议,他软件上出现的各种问题,大多归过于一个自定义的协议。
我们知道,即使是那些主流的协议再完美,也未必适合我们的应用场景。更何况一些主流协议在完美性上,并不见得有突出性。
下面,我们就从一个常见的通信,向协议的方向开始说起。
例如:一个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所有,任何人未经允许禁止转载。
通信协议应如何设计才好?自定义进制帮你忙相关推荐
- Sql Server 自定义进制转换
Sql Server 自定义进制转换 /*@Str 转化进制的自定义字符 例如 '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ' @Str的长度为转化进制的长度 @valu ...
- 网页设计常用颜色16进制代码
本文转载于:猿2048网站网页设计常用颜色16进制代码 在进行网站网页设计制作的时候,经常需要用到不同的颜色的搭配,效果,网页中标明颜色比较好的是用十六进制数据来进行标注,但是由于颜色很多,这些十六进 ...
- java 10进制转64进制_JAVA中实现十进制与其它自定义进制进行相互转换。 - yz124的日志 - 网易博客...
我们通常用到的数字都是十进制的,日常使用的也是这样,但是在程序中,我们可能还会经常用到二进制.八进制.十六进制的数字.既然程序中会使用到,那么就会有它存在的道理.有些时候,将数字用字符串保存到文件或者 ...
- python 自定义进制转换_[python]从零开始学python——颜色的16进制于RGB之间的转换...
在学习openstack的时候,发现openstack是python开发的:学习mininet自定义拓扑,发现mininet是python开发的:看看ryu,还是python开发的--于是心中升起了自 ...
- 生成大小写字母加数字混合ID与自定义进制转换
有时候可以在别的网站上看到类似于这样的ID : D6pPMSTjOFI, 关于数据库主键的选择园子里面也讨论过许多了,比如这篇 小议数据库主键选取策略(原创). 字符串作ID有时候还是有它的优点的,但 ...
- 实训汇编语言设计——内存多字节10进制数相加
将内存first区多字节10进制数与second区相同10进制数相加,结果保存到dest区 DATA SEGMENT FIRST DB 11H, 22H, 33H, 44H, 55H, 66H, ...
- 数字电路与逻辑设计 学习笔记【进制转换】
0.1近代开关理论:Relay-contact Network Theory: 继电-触点网络理论. 0.2,脉冲信号与数字信号, 模拟量->模拟信号:正弦信号.脉冲信号->脉冲电路 数字 ...
- python 自定义进制转换,Python 内置函数进制转换的用法(十进制转二进制、八进制、十六进制)...
使用Python内置函数:bin().oct().int().hex()可实现进制转换. 先看Python官方文档中对这几个内置函数的描述: bin(x) Convert an integer num ...
- PAT-B 1037. 在霍格沃茨找零钱(20)(20 分)自定义进制转换
https://pintia.cn/problem-sets/994805260223102976/problems/994805284923359232 1037 在霍格沃茨找零钱(20)(20 分 ...
- Hbuilder编程设计开发吸取16进制颜色值
最新版QQ的截图有取色器的功能, ctrl + alt + a截图, 鼠标指向颜色块,再按c就能复制rgb代码, esc退出截图, 粘贴就好啦; 如果按住ctrl,按c,复制的就是#颜色代码
最新文章
- 2019微生物组—宏基因组分析技术专题研讨会第四期
- ubuntu通过apt-get方式搭建lnmp环境以及php扩展安装
- 使用SGD(Stochastic Gradient Descent)进行大规模机器学习
- jQuery$命名冲突问题解决方法
- java时间往后一天_如何在Java中将日期增加一天?
- mysql 支持gbk_MySQL不支持GBK编码的解决方法
- pythonenumapi_python模块之enum_上
- 谷歌修复安卓System 组件中的多个 RCE 漏洞
- java组件名词解释_简述Java EE三类组件的构成及运行环境。
- 免费pdf转换成txt转换器
- C语言printf格式化输出
- java emf 转jpg_JAVA读取EMF文件并转化为PNG,JPG,GIF格式
- matlab读取jpg图片出错,求助,Matlab读取图片进行分类。出现错误
- linux 电源管理 power supply class
- 计算机中¥符号按哪个键,人民币键盘符号怎么打 电脑怎么打人民币符号
- 黎曼Zeta函数,人类文明永恒的纪念
- stm32 esp8266-01使用 get,post 请求数据以及json解析
- 论坛项目小程序和h5登录
- 史上最全软件测试工程师常见的面试题总结(九)【多测师】
- 论文开题报告的研究基础怎么写?