5

所以我试图为串行通信定义一个通信协议,我希望能够向设备发送 4 个字节的数字,但我不确定如何确保设备开始在正确的字节上接收它。

例如,如果我想发送

0x1234abcd 0xabcd3f56 ...

我如何确保设备不会在错误的位置开始读取并将第一个单词设为:

0xabcdabcd

有没有聪明的方法来做到这一点?我想过使用标记作为消息的开头,但是如果我想发送我选择的数字作为数据怎么办?

4

2 回答 2

7

如果您知道数据将有多大,为什么不发送一个start-of-message字节后跟一个字节?length-of-data

或者,像其他二进制协议一样,只发送具有固定标头的固定大小的包。假设您只会发送 4 个字节,那么您知道在实际数据内容之前您将有一个或多个字节的标头。

编辑:我认为你误解了我。我的意思是客户端应该始终将字节视为标头或数据,而不是基于值,而是基于流中的位置。假设您要发送四个字节的数据,那么一个字节将是标题字节。

+-+-+-+-+-+
|H|D|D|D|D|
+-+-+-+-+-+

客户端将是一个非常基本的状态机,大致如下:

int state = READ_HEADER;
int nDataBytesRead = 0;
while (true) {
  byte read = readInput();
  if (state == READ_HEADER) {
    // process the byte as a header byte
    state = READ_DATA;
    nDataBytesRead = 0;
  } else {
    // Process the byte as incoming data
    ++nDataBytesRead;
    if (nDataBytesRead == 4) 
    {
      state = READ_HEADER;
    }
  }
} 

关于这个设置的事情是,确定字节是否是标题字节的不是字节的实际内容,而是流中的位置。如果您想要可变数量的数据字节,请在标头中添加另一个字节以指示其后面的数据字节数。这样,如果您发送的值与数据流中的标头相同,则无关紧要,因为您的客户端永远不会将其解释为数据以外的任何内容。

于 2012-02-27T16:47:16.377 回答
5

网串

对于这个应用程序,也许相对简单的“ netstring ”格式就足够了。

例如,文本“hello world!” 编码为:

12:hello world!,

空字符串编码为三个字符:

0:,

可以表示为一系列字节

'0' ':' ','

一个网络字符串中的单词 0x1234abcd(使用网络字节顺序),后跟另一个网络字符串中的单词 0xabcd3f56,编码为一系列字节

'\n' '4' ':' 0x12 0x34 0xab 0xcd ',' '\n'
'\n' '4' ':' 0xab 0xcd 0x3f 0x56 ',' '\n'

(每个网络字符串前后的换行符 '\n' 是可选的,但更容易测试和调试)。

帧同步

如何确保设备不会在错误的位置开始读取

帧同步问题的一般解决方案是读入一个临时缓冲区,希望我们已经在正确的位置开始读取。稍后,我们对缓冲区中的消息运行一些一致性检查。如果消息未通过检查,则说明出现问题,因此我们丢弃缓冲区中的数据并重新开始。(如果是重要信息,我们希望发送者重新发送)。

例如,如果串行电缆插入到第一个网络字符串的一半,接收器会看到字节字符串:

0xab 0xcd ',' '\n' '\n'  '4' ':' 0xab 0xcd 0x3f 0x56 ',' '\n'

因为接收者足够聪明,可以在期待下一个字节是有效数据之前等待“:”,所以接收者能够忽略第一个部分消息,然后正确接收第二个消息。

在某些情况下,您会提前知道有效的消息长度是多少;这使得接收器更容易检测到它在错误的位置开始读取。

发送消息开始标记作为数据

我想过使用标记作为消息的开头,但是如果我想发送我选择的数字作为数据怎么办?

发送网络字符串标头后,发送器按原样发送原始数据——即使它恰好看起来像消息开始标记。

在正常情况下,接收器已经具有帧同步。网络字符串解析器已经读取了“长度”和“:”头,因此网络字符串解析器将原始数据字节直接放入缓冲区中的正确位置——即使这些数据字节恰好看起来像“:”头字节或“,”页脚字节。

伪代码

// netstring parser for receiver
// WARNING: untested pseudocode
// 2012-06-23: David Cary releases this pseudocode as public domain.

const int max_message_length = 9;
char buffer[1 + max_message_length]; // do we need room for a trailing NULL ?
long int latest_commanded_speed = 0;
int data_bytes_read = 0;

int bytes_read = 0;
int state = WAITING_FOR_LENGTH;

reset_buffer()
    bytes_read = 0; // reset buffer index to start-of-buffer
    state = WAITING_FOR_LENGTH;

void check_for_incoming_byte()
    if( inWaiting() ) // Has a new byte has come into the UART?
        // If so, then deal with this new byte.
        if( NEW_VALID_MESSAGE == state )
            // oh dear. We had an unhandled valid message,
            // and now another byte has come in.
            reset_buffer();
        char newbyte = read_serial(1); // pull out 1 new byte.
        buffer[ bytes_read++ ] = newbyte; // and store it in the buffer.
        if( max_message_length < bytes_read )
            reset_buffer(); // reset: avoid buffer overflow

        switch state:
            WAITING_FOR_LENGTH:
                // FIXME: currently only handles messages of 4 data bytes
                if( '4' != newbyte )
                    reset_buffer(); // doesn't look like a valid header.
                else
                    // otherwise, it looks good -- move to next state
                    state = WAITING_FOR_COLON;
            WAITING_FOR_COLON:
                if( ':' != newbyte )
                    reset_buffer(); // doesn't look like a valid header.
                else
                    // otherwise, it looks good -- move to next state
                    state = WAITING_FOR_DATA;
                    data_bytes_read = 0;
            WAITING_FOR_DATA:
                // FIXME: currently only handles messages of 4 data bytes
                data_bytes_read++;
                if( 4 >= data_bytes_read )
                    state = WAITING_FOR_COMMA;
            WAITING_FOR_COMMA:
                if( ',' != newbyte )
                    reset_buffer(); // doesn't look like a valid message.
                else
                    // otherwise, it looks good -- move to next state
                    state = NEW_VALID_MESSAGE;

void handle_message()
    // FIXME: currently only handles messages of 4 data bytes
    long int temp = 0;
    temp = (temp << 8) | buffer[2];
    temp = (temp << 8) | buffer[3];
    temp = (temp << 8) | buffer[4];
    temp = (temp << 8) | buffer[5];
    reset_buffer();
    latest_commanded_speed = temp;
    print( "commanded speed has been set to: " & latest_commanded_speed );
}

void loop () # main loop, repeated forever
    # then check to see if a byte has arrived yet
    check_for_incoming_byte();
    if( NEW_VALID_MESSAGE == state ) handle_message();
    # While we're waiting for bytes to come in, do other main loop stuff.
    do_other_main_loop_stuff();

更多提示

在定义串行通信协议时,我发现如果协议始终使用人类可读的 ASCII 文本字符,而不是任何任意二进制值,它会使测试和调试变得更加容易

帧同步(再次)

我想过使用标记作为消息的开头,但是如果我想发送我选择的数字作为数据怎么办?

我们已经介绍了接收器已经具有帧同步的情况。接收器还没有帧同步的情况非常混乱。

最简单的解决方案是发送器发送一系列无害字节(可能是换行符或空格字符),即最大可能有效消息的长度,作为每个网络字符串之前的前导码。无论插入串行电缆时接收器处于什么状态,这些无害的字节最终都会将接收器驱动到“WAITING_FOR_LENGTH”状态。然后当发送方发送数据包头(长度后跟“:”)时,接收方正确识别为数据包头并恢复帧同步。

(发送器实际上没有必要在每个数据包之前发送该前导码。也许发送器可以发送 20 个数据包中的 1 个;然后保证接收器在串行电缆连接后以 20 个数据包(通常更少)恢复帧同步插入)。

其他协议

其他系统使用简单的 Fletcher-32 校验和或更复杂的东西来检测网络字符串格式无法检测的多种错误(ab),并且即使没有前导码也可以同步。

许多协议使用特殊的“包开始”标记,并使用各种“转义”技术来避免在传输数据中实际发送字面“包开始”字节,即使我们要发送的真实数据恰好有那个值。(一致的开销字节填充位填充引用打印和其他类型的二进制到文本编码等)。

这些协议的优点是接收者可以确定当我们看到“数据包开始”标记时,它是数据包的实际开始(而不是碰巧具有相同值的某些数据字节)。这使得处理同步丢失变得更加容易——只需丢弃字节,直到下一个“数据包开始”标记。

许多其他格式,包括 netstring 格式,允许任何可能的字节值作为数据传输。因此,接收者必须更聪明地处理可能是实际的头开始字节,或者可能是数据字节——但至少他们不必处理“转义”或在最坏的情况下,转义后需要惊人的大缓冲区来保存“固定的 64 字节数据消息”。

选择一种方法实际上并不比另一种更简单——它只是将复杂性推到了另一个地方,正如水床理论所预测的那样。

您是否介意浏览一下串行编程 Wikibook中有关处理开头字节的各种方法(包括这两种方法)的讨论,并编辑该书以使其更好?

于 2012-06-23T19:00:30.687 回答