所以我正在尝试为串行通信定义通信协议,我希望能够向设备发送4个字节的数字,但我不确定如何确保设备在右侧开始拾取它字节。
例如,如果我想发送
0x1234abcd 0xabcd3f56 ...
如何确保设备无法在错误的位置开始阅读并获得第一个单词:
0xabcdabcd
有一种聪明的方法吗?我想过用一个标记来开始一条消息,但如果我想发送我选择的数字作为数据呢?
答案 0 :(得分: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;
}
}
}
关于这种设置的事情是确定字节是否是头字节的不是字节的实际内容,而是流中的位置。如果要拥有可变数量的数据字节,请在标头中添加另一个字节以指示其后的数据字节数。这样,如果您发送与数据流中的标题相同的值,则无关紧要,因为您的客户端永远不会将其解释为除数据之外的任何内容。
答案 1 :(得分:4)
<强>净字符串强>
对于这个应用程序,也许是相对简单的&#34; netstring&#34;格式是足够的。
例如,文本&#34; hello world!&#34;编码为:
12:hello world!,
空字符串编码为三个字符:
0:,
可以表示为字节序列
'0' ':' ','
一个netstring中的字0x1234abcd(使用network byte order),后跟另一个netstring中的字0xabcd3f56,编码为字节序列
'\n' '4' ':' 0x12 0x34 0xab 0xcd ',' '\n'
'\n' '4' ':' 0xab 0xcd 0x3f 0x56 ',' '\n'
(换行字符&#39; \ n&#39;在每个netstring之前和之后是可选的,但是更容易测试和调试。)
帧同步
如何确保设备无法在错误的位置开始阅读
frame synchronization问题的一般解决方案是读入临时缓冲区,希望我们已经开始在正确的位置读取。 稍后,我们对缓冲区中的消息运行一些一致性检查。 如果消息未通过检查,则出现问题, 所以我们扔掉缓冲区中的数据并重新开始。 (如果这是一条重要信息,我们希望发射机重新发送它。)
例如,如果串行电缆插入第一个网络串的中途, 接收器看到字节串:
0xab 0xcd ',' '\n' '\n' '4' ':' 0xab 0xcd 0x3f 0x56 ',' '\n'
因为接收器足够聪明,可以等待&#39;:&#39;在期望下一个字节是有效数据之前,接收方能够忽略第一部分消息,然后正确接收第二条消息。
在某些情况下,您提前知道有效消息长度是多少; 这让接收器更容易检测到它已经开始在错误的位置读取。
将信息开头标记作为数据发送
我想过用一个标记来开始一条消息,但如果我想将我选择的数字作为数据发送怎么办?
发送netstring标头后,发送器按原样发送原始数据 - 即使它看起来像是开始消息标记。
在正常情况下,接收器已经具有帧同步。 netstring解析器已经读取了&#34;长度&#34;和&#34;:&#34;头, 所以netstring解析器 将原始数据字节直接放入缓冲区中的正确位置 - 即使这些数据字节看起来像&#34;:&#34;标题字节或&#34;,&#34;页脚字节。
<强>伪代码强>
// 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文本字符,而不是任意二进制值,我发现它使测试和调试更多更容易。
帧同步(再次)
我想过用一个标记来开始一条消息,但如果我想将我选择的数字作为数据发送怎么办?
我们已经介绍了接收器已经具有帧同步的情况。 接收器还没有帧同步的情况非常混乱。
最简单的解决方案是让发射机发送一系列无害的字节 (可能是换行符或空格字符), 最大可能有效消息的长度, 作为每个netstring之前的序言。 无论插入串行电缆时接收器处于什么状态, 那些无害的字节最终驱使接收器进入 &#34; WAITING_FOR_LENGTH&#34;州。 然后当发送者发送数据包标题(长度后跟&#34;:&#34;)时, 接收器正确地将其识别为包头并恢复了帧同步。
(发送器在每个数据包之前发送该前导码并不是必需的。 也许发射机可以发送20个数据包中的1个;然后接收器保证在插入串行电缆后以20个数据包(通常更少)恢复帧同步。)
其他协议
其他系统使用简单的Fletcher-32校验和或更复杂的东西来检测netstring格式无法检测的多种错误(a,b), 即使没有序言也可以同步。
许多协议使用特殊的&#34;数据包开始&#34;标记,并使用各种&#34;逃避&#34;避免实际发送文字&#34;数据包开始的技术&#34;传输数据中的字节,即使我们要发送的实际数据恰好具有该值。 (Consistent Overhead Byte Stuffing,bit stuffing,quoted-printable和其他binary-to-text encoding等等。)
这些协议的优势在于,当我们看到&#34;数据包开始时,接收方可以确定#34;标记,它是数据包的实际开始(而不是某些数据字节恰好碰巧具有相同的值)。 这使得处理同步丢失变得更加容易 - 简单地丢弃字节直到下一个&#34;数据包的开始&#34;标记
许多其他格式(包括netstring格式)允许任何可能的字节值作为数据传输。 因此,接收器必须更聪明地处理头部开头的字节,可能是一个实际的头部开头,或可能是一个数据字节 - 但是在至少他们不必处理&#34;逃避&#34;或者,在最坏的情况下,需要一个令人惊讶的大缓冲区来保存一个固定的64字节数据消息&#34;逃跑后。
选择一种方法实际上并不比另一种更简单 - 它只是将复杂性推向另一个地方,正如waterbed theory预测的那样。
你是否会在Serial Programming Wikibook处略过关于处理头部起始字节的各种方法的讨论,包括这两种方式, 并编辑该书以使其更好?