串行通信中的字节对齐

van*_*joe 2 serial-port communication-protocol

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

例如,如果我想发送

0x1234abcd 0xabcd3f56 ...
Run Code Online (Sandbox Code Playgroud)

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

0xabcdabcd
Run Code Online (Sandbox Code Playgroud)

这样做有一个聪明的方法吗?我想过用一个标记来开始一条消息,但如果我想发送我选择的数字作为数据呢?

Der*_*all 7

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

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

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

+-+-+-+-+-+
|H|D|D|D|D|
+-+-+-+-+-+
Run Code Online (Sandbox Code Playgroud)

然后客户端将是一个非常基本的状态机,类似于:

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;
    }
  }
} 
Run Code Online (Sandbox Code Playgroud)

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

  • 所以这是假设您不删除任何数据,对吗?因为否则你不知道你在什么位置,也无法弄清楚标题在哪里。还是我还是误会了? (2认同)

Dav*_*ary 5

网串

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

例如,文字“你好,世界!” 编码为:

12:hello world!,
Run Code Online (Sandbox Code Playgroud)

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

0:,
Run Code Online (Sandbox Code Playgroud)

可以表示为一系列字节

'0' ':' ','
Run Code Online (Sandbox Code Playgroud)

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

'\n' '4' ':' 0x12 0x34 0xab 0xcd ',' '\n'
'\n' '4' ':' 0xab 0xcd 0x3f 0x56 ',' '\n'
Run Code Online (Sandbox Code Playgroud)

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

帧同步

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

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

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

0xab 0xcd ',' '\n' '\n'  '4' ':' 0xab 0xcd 0x3f 0x56 ',' '\n'
Run Code Online (Sandbox Code Playgroud)

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

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

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

我曾想过要在邮件的开头使用一个标记,但是如果我想发送选择的数字作为数据怎么办?

发送完netstring标头后,发送器会按原样发送原始数据-即使它看起来像消息开始标记。

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

伪码

// 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();
Run Code Online (Sandbox Code Playgroud)

更多提示

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

帧同步(再次)

我曾想过要在邮件的开头使用一个标记,但是如果我想发送选择的数字作为数据怎么办?

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

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

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

其他协议

其他系统使用简单的Fletcher-32校验和或更复杂的东西来检测netstring格式无法检测到的许多错误(ab),即使没有前同步码也可以同步。

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

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

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

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

您是否会介意在Serial Programming Wikibook上讨论处理首部起始字节的各种方式的讨论,包括这两种方式,并对其进行编辑以使其更完善?