所以我试图定义串行通信的通信协议,我希望能够向设备发送4字节数,但我不确定如何确保设备开始在正确的字节上拾取它。
例如,如果我想发送
0x1234abcd 0xabcd3f56 ...
如何确保设备不会从错误的位置开始读取并将第一个单词作为:
0xabcdabcd
是否有一个聪明的方法来做到这一点?我想过使用标记作为消息的开头,但是如果我想发送我选择的数字作为数据呢?
如果你知道数据有多大,为什么不先发送一个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;
}
}
}
关于这个设置的事情是,决定字节是否是头字节的不是字节的实际内容,而是在流中的位置。如果希望具有可变数量的数据字节,请在头中添加另一个字节,以指示其后面的数据字节数。这样,如果你在数据流中发送与头相同的值也没关系,因为你的客户端永远不会将其解释为数据以外的任何东西。
netstring
对于这个应用程序,也许相对简单的"netstring"格式就足够了。
例如,文本"hello world!"编码为:
12:hello world!,
空字符串编码为三个字符:
0:,
可以表示为一系列字节
'0' ':' ','
一个netstring中的单词0x1234abcd(使用网络字节顺序),后面跟着另一个netstring中的单词0xabcd3f56,编码为字节序列
'n' '4' ':' 0x12 0x34 0xab 0xcd ',' 'n'
'n' '4' ':' 0xab 0xcd 0x3f 0x56 ',' 'n'
(每个netstring前后的换行字符'n'是可选的,但使其更易于测试和调试)。
帧同步
如何确保设备不会从错误的位置开始读取
帧同步问题的一般解决方案是读入临时缓冲区,希望我们已经在正确的位置开始读取。稍后,我们对缓冲区中的消息运行一些一致性检查。如果消息没有通过检查,说明出了问题,所以我们扔掉缓冲区中的数据,重新开始。(如果是很重要的信息,我们希望发送者能重新发送)
例如,如果串口线在第一个netstring的中间插入,接收方看到字节字符串:
0xab 0xcd ',' 'n' 'n' '4' ':' 0xab 0xcd 0x3f 0x56 ',' 'n'
因为接收方足够聪明,在期望下一个字节是有效数据之前等待':',所以接收方能够忽略第一部分消息,然后正确接收第二条消息。
在某些情况下,您提前知道有效的消息长度将是多少;这使得接收器更容易发现它在错误的位置开始读取。
发送消息起始标记作为数据
我想使用标记作为消息的开始,但是如果我想发送我选择的数字作为数据呢?
发送netstring报头后,发送器按原样发送原始数据——即使它碰巧看起来像消息开始标记。
在正常情况下,接收方已经有帧同步。netstring解析器已经读取了"length"one_answers":"报头,所以netstring解析器将原始数据字节直接放入缓冲区中的正确位置——即使这些数据字节恰好看起来像":" header字节或"," footer字节。
// 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校验和或更复杂的东西来检测netstring格式无法检测的许多类型的错误(a, b),并且即使没有序言也可以同步。
许多协议使用特殊的"数据包开始"标记,并使用各种"转义"技术来避免在传输数据中实际发送文字"数据包开始"字节,即使我们想要发送的实际数据恰好具有该值。(一致开销字节填充,位填充,引用可打印和其他类型的二进制到文本编码等)。
这些协议的优点是接收方可以确定当我们看到"数据包的开始"标记时,它是数据包的实际开始(而不是碰巧具有相同值的某些数据字节)。这使得处理同步丢失变得容易得多——只需丢弃字节,直到下一个"数据包开始"标记。
许多其他格式,包括netstring格式,允许任何可能的字节值作为数据传输。所以接收方必须更聪明地处理报头起始字节,可能是一个实际的报头起始字节,或者可能是一个数据字节——但至少他们不必处理"转义"或所需的惊人的大缓冲区,在最坏的情况下,转义后保存"固定的64字节数据消息"。
选择一种方法并不比另一种方法简单——它只是把复杂性推到了另一个地方,正如水床理论所预测的那样。
你介意浏览一下关于处理头字节开始的各种方法的讨论吗,包括这两种方法,在串行编程维基书中,编辑这本书是为了让它更好吗?