如何为嵌入式系统设计串行命令协议?

43

我有一个嵌入式系统通过串口进行通信。现在的命令结构设计是交互式操作的:它显示提示符,接收几个命令,并以人类可读的形式显示结果。

我正在考虑将其更改为更适合机器使用的格式,这样我就可以通过MATLAB GUI与其交互而不会遇到太多麻烦(现在它在交互式提示和变化的消息长度等方面有问题)。

因此,请问是否有文件或标准描述如何为您的嵌入式系统设计良好的串行命令协议?


请查看https://dev59.com/dXRA5IYBdhLWcg3wzhbZ和https://dev59.com/x3RA5IYBdhLWcg3wzhbZ。 - Rex Logan
什么是嵌入式系统?您打算如何使用串口接口? - Bruce McGee
这是一个主动传感器系统,它生成信号并等待响应。串行接口主要用于控制(以及调试):设置信号参数,操作系统;返回数据。 - jparker
9个回答

56

我在使用RS232控制媒体和显示设备时有一些偏好(和烦恼)。根据你的硬件,其中一些可能不适用:

  • 我认为让协议更加易于自动化是个好主意。如果需要交互式界面(命令行或其他),请单独构建,并使用自动化协议。我不会过多考虑使其对人类可读,但这由你决定。

  • 始终返回响应,即使(特别是)收到无效命令。可以使用简单的$06表示ACK和$15表示NAK,或者如果想要稍微更容易理解,则可以详细说明。

  • 如果可以设置任何值,请确保有某种方法查询相同的值。如果有很多值,则可能需要一段时间才能查询它们所有。考虑只返回几个元查询中的一个,以一次返回多个值。

  • 如果有一些不能设置但很重要的信息(型号、序列号、版本、版权等),请确保可以查询它们而不仅仅在启动或重置时显示一次。

  • 永远不要对有效命令响应错误。你本以为这一点很明显…

  • 说起明显,记录硬件支持的串行设置。特别是如果它将被除你以外的任何人使用,而你不希望他们花30分钟来确定他们无法与设备通信是因为串口、连接、电缆还是软件问题。并不是我很生气…

  • 使用绝对命令而不是切换值。例如,单独发送打开和关闭电源的命令,而不是发送相同的命令并切换电源的开关。

  • 响应应包括它们响应的命令信息。这样,任何程序都不需要记住上次请求的内容以处理响应(参见下面的额外学分选项)。

  • 如果您的设备支持待机模式(关闭,但不是真正的关闭),请确保在该状态下查询仍然有效。

  • 根据您对数据完整性的担忧程度:

    • 将消息包装在信封中。标题可以包括起始字符、消息长度和结束字符。以防您收到部分或格式错误的消息。例如,起始字符可能是$02,结束字符可能是$03。

    • 如果您非常担心消息的完整性,请包括一个校验和。不过这可能会有点麻烦。

    额外加分项:

    • 如果您的硬件设置可以手动更改,则可以通过串行端口发送此更改,就好像用户已经查询了它一样。例如,您可能不希望用户能够更改公共显示器的输入源。

    希望这可以帮到您。

    更新:

    我忘记了一些重要的事情。在您真正使用它之前,特别是在将其提供给他人之前,请在一些琐碎的东西上试用它,以确保它按照您的预期工作,并且(更重要的是)确保您没有遗漏任何东西。如果在进行更大的项目时发现问题,修复它将需要更多的时间和精力。

    无论您是在设计命令协议、Web服务、数据库模式还是类等,这都是一个不错的经验法则。


    6
    我同意此帖的全部内容。虽然我会更强烈地主张使用人类可读格式(Ascii),原因如下:1. 更易于故障排除 2. 不需要特殊的测试工具,只需使用超级终端或Tera Term 3. 更容易向不那么复杂的客户描述如何使用。通常我会在命令周围加上起始字符和结束字符,并在起始字符后面加上长度。如果您想利用完整行解析,则可以将EOP设置为'/n'返回字符。 - simon
    2
    如果这是一个要求,那么我没有任何反对意见。请将您的消息包装在类似方括号的东西中,使长度和任何其他值也易于阅读(不要混合搭配)。我的厌恶来自于一些非常冗长(而且不一致)的协议。 - Bruce McGee
    2
    如果您使用信封,请确保将信封与其包裹的物品分开。我现在有一个协议,他们没有这样做,解析起来非常麻烦。 - Michael Kohne
    6
    除了框架部分,我同意全部内容。我认为框架是必需的而非可选的。每个数据包都应该有一个起始模式、长度和校验和,这是必须的。序列号可以根据情况选择是否需要,而结束模式是可选的。 - old_timer
    4
    无论何时,都要包含一个校验和(最好是一个好的循环冗余校验码)。永远不要假设数据的完整性,特别是当使用高度可靠的校验方法进行检查时。 - Kongress
    显示剩余6条评论

    15

    这里是Eli Benderski关于串行协议数据帧的优秀文章。无论你选择哪种数据包格式,请务必使用转义字符,这样可以使实际数据中包含此类字符并且在数据包出现错误时很容易重新同步。


    文章建议将“特殊”字符前面加上转义字符,但原样发送。这是常见的做法,但我真的不喜欢它,因为它使得“转义开始标志”的序列具有上下文敏感性。 - supercat
    5
    COBS比转义字符更好。 - Jason S
    不知道 COBS。很好。 - Marcelo MD
    3
    MIN协议使用三个0xAA字符连续出现来标记帧的开始,然后使用字节填充确保在其余的帧中不会再出现连续三个0xAA字符。这样,当接收器接收到0xAA 0xAA 0xAA时,容易重置接收器的状态机并使其保持同步(选择0xAA是因为在二进制中它对应10101010,可以用于自动波特率检测中的脉冲串)。 - Ken Tindell

    8
    除非带宽或延迟是个大问题,尽可能使用 ASCII - 这会使得调试更容易。
    我喜欢协议发送消息后紧跟着一个明确的“消息结束”字符(例如“回车符”)。我通常不认为起始信号对于理解有什么帮助(这条线上还有什么?)使用 CR 作为消息结束也使得通过终端程序测试更容易。
    更新:Bruce 在评论中指出,起始数据包字符可以让你在数据包毁坏的情况下稍微快一点找到数据包。没有起始数据包字符,你只能等到下一个数据包的结尾才能知道位置,并且在那时你将丢弃两个数据包而不是一个。

    1
    如果您经常遇到丢失字符或部分数据包的情况,那么起始信封字符非常有用。我们发现RS232可能不可靠。特别是当您处理串口扩展器或串口转TCPIP时更是如此。 - Bruce McGee
    @simon - 在实际串行系统中(这个问题就是关于这个的),你在同一时间只有一个数据包。正如Bruce所说,SOP可能适用于处理丢失的数据包,但对于异步处理来说并不重要- 这取决于能够基于某些内容(通常是信封数据)匹配命令和响应。 SOP与此无关。 - Michael Kohne
    @Bruce - 我同意,多点串行是我以前处理过的事情。但是,串口(或TCP套接字)一次只能有一个数据包,这就是我关心的全部。让不同的设备保持清晰是我想要在信封中实现的,但有很多方法可以解决这个问题。 - Michael Kohne
    @simon - 是的,你可能在ACK之前有多个数据包。但是,你在同一时间只会在电线上有一个数据包,开始/结束数据包字符真的只是为了找到数据包,而不是其他用途。 - Michael Kohne
    除非嵌入式系统代码的大小和复杂性成为问题,否则我会主张尽可能保持嵌入式代码的简单,并将尽可能多的复杂性移至PC软件中。因此,如果您需要一个漂亮的人机界面,请通过PC软件提供它。 - Craig McQueen
    显示剩余4条评论

    7
    我喜欢Bruce McGee的答案。我曾经使用类似的接口,可以提供几个指针:
    • 在数据包中返回数字类型时,请尽量使所有内容格式相同。不要有一些数字是单数,而其他数字是双数。而且不要任意制定!

    • 在ICD中为数据包提供示例。猜测字节顺序甚至位顺序(MSByte先还是后?什么是第一个和最后一个?)非常令人沮丧。提供一个显示数据包与时间的图表(即,0x02最早发送,然后是地址字节,然后是消息ID等)。

    • 如有可能,请不要在数据格式之间切换。我曾经在使用“ASCII编码数字”的系统中工作,在某些消息中您必须剥离字节前面的“3”,然后将它们拼在一起以得到真实数字。(通常情况下,当您需要避免字节序列时,例如0x02、0x04等时,才会使用ASCII编码。 编码的数字将为0x30、0x32、0x30、0x34。如果可能,请使用长度字段来避免这种情况,或者至少始终这样做!)

    • 一定要记录波特率、奇偶校验等信息。如果您正在使用RS-485,请记录总线模式(2线?4线?)或任何将出现在您打算使用该设置的机器上的设置。如有必要,请提供截图。

    此接口可能非常有用于调试。我曾经使用过某些具有调试功能的系统,例如:

    • 程序员制作了一个“已记录参数”列表(内部变量)。您会告诉系统您要报告哪些参数(最多8个),然后当您查询已记录的参数时,它将以一个数据包返回它们。我喜欢这个,但是根据系统的复杂性,您可能无法在运行时指定它们(或者您可以做一些简单的事情,向系统发送一个掩码,以选择您想要返回的参数)。

    • “破坏”行为的数据包,并允许测试系统的部分独立地进行测试(即,在D/A上输出这个电压,在DIO端口上刺激这个字节等)

    祝你好运!


    5
    如果您必须拥有自己的协议,请使用数据包帧格式。SLIP帧格式只需要几行代码,规范在RFC 1055中指定。现在所有的数据包都可以始终看起来像这样。
     <slip packet start>
     message
     message crc
     <slip packet start>
    

    不要发送消息长度。这可能会导致消息损坏,接收方的消息解析器会变得混乱。
    如果接收方的缓冲区很小并且溢出,则只需继续读取直到数据包边界。不会有任何损害。
    有很多简单的2字节CRC校验码;Xmodem的校验码很容易。如果必须,您可以对数据包中的所有字节执行异或操作。
    如果您想成为一个真正好的人,请使用PPP、DDNS和HTTP-REST来进行实际命令。 Jeremy Bentham在C语言中写了一本关于在16系列PIC处理器上完成此操作的精美书籍,名为TCP/IP Lean。
    然后,您可以使用Web浏览器与其通信,或者从C代码中使用类似于libcurl的东西。由于几乎每种编程语言都有用于http的库,因此每个人都可以与您的设备进行通信。

    4
    这里有很多好的建议和想法,需要注意的是不同的目的有不同的解决方法。我曾经使用过许多串行协议,有好的也有坏的,并且也制定了自己的一些协议(好的和坏的...),以下是我的一些建议和评论。
    - 保持简单。我发现基于简单头部的命令-响应“数据包”最为成功。 - 不要担心人类可读的ASCII码。它只在你实际调试协议的几个小时内有用。之后,始终将数据编码为ASCII码会限制数据传输效率。 - 使用错误检查。我更喜欢16位CRC,因为它提供了比校验和更高的保护级别,并且相对于MD5或SHA1等更重的算法来说仍然简单而有效。 - 在命令和响应中使用相同的数据包格式。 - 使用8位数据,无奇偶校验。如果您已经使用CRC或其他数据完整性检查,则串行奇偶校验位不会增加任何保护作用,最多只是一个较差的错误检查。 - 因此,在最简单的形式下,数据包头包括命令或响应、数据包大小、零个或多个数据和错误检查代码(CRC)。

    1
    尽管我倾向于使用非人类可读的协议,但在刚创建时,它们可以具有除调试之外的优点。对于任何编写与设备通信的软件的第三方来说,这可能是有用的。 - Bruce McGee

    3

    FTP是一个可以在交互和自动化中都良好运作的协议示例。其中一个关键点是响应以指示自动化客户端是否需要注意的代码开头。POP3也是如此。

    这些协议的一大好处是,在开发/调试时,您可以使用常规终端驱动通信,或者使用常规shell脚本/批处理文件/等等脚本化通信。

    然而,它们所不能提供的是可靠性——这由通信堆栈的更低层提供。在大多数嵌入式通信路径中,应该考虑这一点。


    3

    你是否看过Modbus(http://www.modbus.org/)?这是一种相当简单的协议,每个消息都包含一个校验和。即使那些不需要“返回值”的命令也会发送响应,因此您可以知道您的命令是否正确接收。选择Modbus后唯一的选择是要将数据存储在哪个寄存器地址以及用户定义函数的格式(MODBUS协议允许)。


    1

    Firmata作为一个示例协议。


    有使用 SCPI 的经验吗? - jparker
    SCIP规范在这里:ivifoundation.org/docs/SCPI-99.PDF - Bruce McGee

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接