通信协议最佳实践和模式


19

每次我设计要在两个arduino之间使用的串行协议时,我都会感觉就像是在重新发明轮子。我想知道人们是否遵循任何最佳做法或模式。这个问题不是关于实际代码,而是关于消息的格式。

例如,如果我想告诉arduino闪烁它的第一个LED 3次,我可以发送:

^L1,F3\n
  • '^':开始一个新命令
  • 'L':定义命令,(L:将此命令指向LED)
  • '1':瞄准第一个LED
  • ',':命令行分隔符,此消息中要跟随的新值
  • 'F':Flash子命令
  • '3':3次(使LED闪烁3次)
  • '\ n':结束命令

有什么想法吗?您通常如何编写新的串行协议?如果我想将查询从arduino 1发送到arduino 2,然后收到响应怎么办?

Answers:


13

根据可能需要的功能以及所需的错误检查数量,有很多写入串行协议的方法。

在点对点协议中看到的一些常见的事情是:

讯息结束

最简单的ASCII协议,只是有消息字符序列的结束,往往\r还是\n因为这是当进入关键是什么会打印被击中。二进制协议可能使用0x03或其他一些公共字节。

消息开始

仅具有消息结尾的问题是,当您发送消息时,您不知道还接收了哪些其他字节。这些字节然后将被添加到消息的前缀,并导致其被错误地解释。例如,如果Arduino刚从睡眠中醒来,则串行缓冲区中可能会有一些垃圾。为了解决这个问题,您需要先启动消息序列。在您的示例中,^通常采用二进制协议0x02

错误检查

如果消息可能被破坏,我们需要进行一些错误检查。这可能是校验和或CRC错误或其他错误。

转义字符

可能是校验和添加到控制字符上,例如“消息的开始”或“消息的结束”字节,或者消息包含等于控制字符的值。解决方案是引入转义字符。转义字符放置在修改后的控制字符之前,因此实际的控制字符不存在。例如,如果起始字符为0x02,则使用转义字符0x10,我们可以在消息中将 0x02作为字节对0x10 0x12(字节XOR控制字符)发送

包数

如果邮件已损坏,我们可以请求重新发送,其中包含nack或retry邮件,但是如果发送了多封邮件,则只能重新发送最新的邮件。相反,可以为数据包提供一定数量的消息,该消息将在一定数量的消息后翻转。例如,如果此数字为16,则发送设备可以存储最近发送的16条消息;如果有任何消息损坏,则接收设备可以使用数据包号请求重新发送。

长度

通常在二进制协议中,您会看到一个长度字节,该字节告诉接收设备消息中有多少个字符。这就增加了另一级错误检查,好像没有收到正确的字节数,那么就出现了错误。

特定于Arduino

在为Arduino提出协议时,首先要考虑的是通信通道的可靠性。如果您通过大多数无线媒体,XBee,WiFi等进行发送,则已经内置了错误检查和重试功能,因此将这些内容放入协议中毫无意义。如果您要通过RS422发送几公里,则很有必要。我将要包括的内容是消息的开头和结尾的字符。我的典型实现如下所示:

>messageType,data1,data2,…,dataN\n

用逗号分隔数据部分可简化解析,并且使用ASCII发送消息。ASCII协议很棒,因为您可以在串行监视器中键入消息。

如果您想要二进制协议(可能是为了缩短消息大小),则如果数据字节可以与控制字节相同,则必须实现转义。对于需要全面的错误检查和重试的系统,二进制控制字符更好。如果需要,有效负载仍可以是ASCII。


消息代码的实际开始之前的垃圾是否可能包含消息控制代码的开始?您将如何处理?
CMCDragonkai

@CMCDragonkai是的,这是可能的,尤其是对于单字节控制代码。但是,如果在解析消息的过程中遇到启动控制代码,则该消息将被丢弃并重新开始解析。
geometrikal

9

我对串行协议没有任何正式的专业知识,但是我已经使用了很多次,或多或少地决定采用这种方案:

(数据包标题)(ID字节)(数据)(fletcher16校验和)(数据包页脚)

我通常将页眉2个字节和页脚1个字节。当看到新的数据包头时,我的解析器将转储所有内容,并在看到页脚时尝试解析该消息。如果校验和失败,则不会丢弃该消息,而是继续添加,直到找到页脚字符并且校验和成功为止。这样,页脚只需要一个字节,因为冲突不会破坏消息。

ID是任意的,有时数据段的长度为底部半字节(4位)。可以使用第二个长度位,但是我通常不会打扰,因为不需要知道长度就可以正确解析,因此看到给定ID正确的长度就可以进一步确认消息是正确的。

fletcher16校验和是一个2字节校验和,与CRC几乎具有相同的质量,但易于实现。这里有一些细节。代码可以像这样简单:

for(int i=0; i < bufSize; i++ ){
   sum1 = (sum1 + buffer[i]) % 255;
   sum2 = (sum2 + sum1) % 255;
}
uint16_t checksum = (((uint16_t)sum1)<<8) | sum2;

我还使用了呼叫和响应系统来处理关键消息,其中PC将每隔500毫秒左右发送一条消息,直到收到包含整个原始消息的校验和作为数据(包括原始校验和)的OK消息为止。

当然,此方案不适合像您的示例那样键入到终端中。您的协议似乎仅限于ASCII,这很不错,对于可以直接读取和发送消息的快速项目,我敢肯定。对于较大的项目,最好具有二进制协议的密度和校验和的安全性。


由于“ [您的解析器将在看到新的数据包头时将转储所有内容”,我想知道是否偶然在数据内部遇到了头,这是否不会产生问题?
humanityANDpeace

@humanityANDpeace丢弃它的原因是,当数据包被切断时,它将永远无法正确解析,那么您何时确定其垃圾并继续前进呢?最简单且据我所知,解决方案是在下一个标头出现时立即丢弃一个错误的数据包。我一直在使用16位标头而没有问题,但是如果确定性更重要,则可以将其更长带宽。
BrettAM,2015年

因此,您所说的标头有点像是魔术16位组合。即010101001 10101010,对不对?我同意这只是1/256 * 256的更改,但是它也禁用了在数据内部使用此16bit的功能,否则它会被误解为标头,并且您丢弃了消息,对吗?
humanityANDpeace

@humanityANDpeace我知道已经过了一年,但是您需要引入一个转义序列。在发送之前,服务器会检查有效载荷中是否有任何特殊字节,然后使用另一个特殊字节对其进行转义。在客户端,您必须将原始有效负载放回原处。这确实意味着您不能发送固定长度的数据包,并且会使实现复杂化。有许多串行协议标准可供选择,所有这些都可以解决这个问题。这是关于该主题的很好的读物
RubberDuck

1

如果您符合标准,那么可以看看ASN.1 / BER TLV编码。ASN.1是一种用于描述数据结构的语言,专门用于通信。BER是对使用ASN.1构建的数据进行编码的TLV方法。问题在于ASN.1编码充其量可能是棘手的。创建一个成熟的ASN.1编译器本身就是一个项目(这是一个特别棘手的项目,请花几个月的时间)。


最好仅保留TLV结构。TLV基本上由三个元素组成:标签,长度和值字段。标签定义数据的类型(文本字符串,八位字节字符串,整数等),长度定义的长度。

在BER中,T还表示该值是TLV结构本身的集合(构造的节点)还是直接是值(原始节点)。这样,您可以像XML(但没有XML开销)那样以二进制形式创建树。

例:

TT LL VV
02 01 FF

是一个整数(标记02),长度为1(长度01),值为-1(值FF)。在ASN.1 / BER中,整数是符号大端数字,但您当然可以使用自己的格式。

TT LL (TT LL VV, TT LL VV VV)
30 07  02 01 FF  02 02 00 FF

是长度为7的序列(列表),其中包含两个整数,一个的值为-1,一个的值为255。这两个整数编码共同构成了序列的值。

您也可以将其扔到在线解码器中,不是很好吗?


您还可以在BER中使用不确定的长度,这将允许您流式传输数据。在那种情况下,您确实需要正确解析树。我认为这是一个高级主题,您需要了解广度优先和深度优先解析。


使用TLV方案基本上可以让您考虑任何类型的数据结构并对其进行编码。ASN.1的功能远不止于此,它为您提供了唯一的标识符(OID),选项(与C-union相似),包括其他ASN.1结构等,但是对于您的项目而言,这可能是过大的选择。当今最著名的ASN.1定义结构可能是浏览器使用的证书。


0

如果没有,您已经掌握了基础知识。您的命令可以由人和机器创建和读取,这是一大优势。您可能添加校验和以检测格式错误的命令或传输中的一个损坏,尤其是在您的频道包括长电缆或无线电链路的情况下。

如果您需要工业实力(您的设备不能引起或使某人受伤或死亡;您需要高数据速率,故障恢复,丢失数据包检测等),则请参考一些标准协议和设计实践。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.