通信プロトコルのベストプラクティスとパターン


19

2つのarduino間で使用されるシリアルプロトコルを設計するたびに、車輪を再発明しているように感じます。人々が従うベストプラクティスやパターンがあるのだろうか。この質問は実際のコードに関するものではなく、メッセージの形式に関するものです。

たとえば、arduinoに最初のLEDを3回点滅させるように指示したい場合、次のように送信します。

^L1,F3\n
  • '^':新しいコマンドを開始します
  • 'L':コマンドを定義します(L:このコマンドをLEDに向けます)
  • 「1」:最初のLEDをターゲット
  • '、':コマンドラインセパレータ、このメッセージの新しい値
  • 'F':Flashサブコマンド
  • 「3」:3回(LEDを3回点滅)
  • '\ n':コマンドを終了します

考え?通常、新しいシリアルプロトコルの作成方法を教えてください。arduino 1からarduino 2にクエリを送信してから応答を受信する場合はどうでしょうか?

回答:


13

必要な機能と必要なエラーチェックの量に応じて、シリアルプロトコルを記述する方法は多数あります。

ポイントツーポイントプロトコルでよく見られるもののいくつかは次のとおりです。

メッセージの終わり

最も単純なASCIIプロトコルは、多くの場合、\rまたは\nEnterキーが押されたときに印刷されるものであるため、メッセージ文字列の終わりを持っています。バイナリプロトコルは0x03、他の一般的なバイトを使用する場合があります。

メッセージの始まり

メッセージの終わりを持っているだけの問題は、メッセージを送信するときにすでに他のどのバイトが受信されているかわからないことです。これらのバイトはメッセージの前に付加され、誤って解釈されます。たとえば、Arduinoがスリープから復帰したばかりの場合、シリアルバッファにゴミが残っている可能性があります。これを回避するには、メッセージシーケンスを開始します。あなたの例では^、しばしばバイナリプロトコルで0x02

エラーチェック

メッセージが破損する可能性がある場合は、エラーチェックが必要です。これは、チェックサムまたはCRCエラーなどです。

エスケープ文字

チェックサムが「メッセージの開始」または「メッセージの終了」バイトなどの制御文字に追加されたり、メッセージに制御文字に等しい値が含まれたりする可能性があります。解決策は、エスケープ文字を導入することです。エスケープ文字は、実際の制御文字が存在しないように、変更された制御文字の前に配置されます。たとえば、開始文字が0x02の場合、エスケープ文字0x10を使用して、メッセージの 0x02をバイトペア0x10 0x12(バイトXOR制御文字)として送信できます。

パケット番号

メッセージが破損している場合、nackで再送信を要求するか、メッセージを再試行できますが、複数のメッセージが送信されている場合は、最新のメッセージのみを再送信できます。代わりに、特定の数のメッセージの後にロールオーバーする番号をパケットに与えることができます。たとえば、この番号が16の場合、送信デバイスは送信された最後の16個のメッセージを保存でき、破損している場合、受信デバイスはパケット番号を使用して再送信を要求できます。

長さ

多くの場合、バイナリプロトコルでは、受信デバイスにメッセージ内の文字数を伝える長さバイトが表示されます。これにより、正しいバイト数が受信されなかった場合にエラーが発生したかのように、別のレベルのエラーチェックが追加されます。

Arduino固有

Arduinoのプロトコルを考え出すとき、最初の考慮事項は、通信チャネルの信頼性です。ほとんどのワイヤレスメディア、XBee、WiFiなどで送信する場合、エラーチェックと再試行が既に組み込まれているため、これらをプロトコルに入れる意味はありません。RS422で数キロメートル送信する場合は、必要になります。私が含めるものは、あなたが持っているように、メッセージの始まりとメッセージの終わりの文字です。私の典型的な実装は次のようになります。

>messageType,data1,data2,…,dataN\n

データ部分をコンマで区切ると、解析が容易になり、メッセージはASCIIを使用して送信されます。ASCIIプロトコルは、シリアルモニターにメッセージを入力できるため便利です。

バイナリプロトコルが必要な場合、おそらくメッセージサイズを短くするために、データバイトが制御バイトと同じになる可能性がある場合は、エスケープを実装する必要があります。バイナリ制御文字は、エラーチェックと再試行の全範囲が必要なシステムに適しています。必要に応じて、ペイロードはASCIIのままにすることができます。


メッセージコードの実際の開始の前のゴミにメッセージ制御コードの開始が含まれている可能性はありませんか?これにどう対処しますか?
CMCDragonkai

@CMCDragonkaiはい。これは、特にシングルバイト制御コードの場合に可能です。ただし、メッセージの解析の途中で開始制御コードが発生した場合、メッセージは破棄され、解析が再開されます。
geometrikal

9

シリアルプロトコルに関する正式な専門知識はありませんが、かなり頻繁に使用しており、このスキームに多少の差はあります。

(パケットヘッダー)(IDバイト)(データ)(fletcher16チェックサム)(パケットフッター)

通常、ヘッダーを2バイト、フッターを1バイトにします。パーサーは、新しいパケットヘッダーを検出するとすべてをダンプし、フッターを検出するとメッセージの解析を試みます。チェックサムが失敗した場合、メッセージは捨てられませんが、フッター文字が見つかりチェックサムが成功するまで追加を続けます。衝突によってメッセージが中断されることはないため、フッターは1バイトで十分です。

IDは任意であり、データセクションの長さが下部ニブル(4ビット)である場合もあります。2番目の長さのビットを使用できますが、適切に解析するために長さを知る必要がないため、通常は気にしません。したがって、特定のIDに対して正しい長さを確認することは、メッセージが正しいことを確認するだけです。

fletcher16チェックサムは、CRCとほぼ同じ品質の2バイトのチェックサムですが、実装がはるかに簡単です。詳細はこちら。コードは次のように簡単にすることができます。

for(int i=0; i < bufSize; i++ ){
   sum1 = (sum1 + buffer[i]) % 255;
   sum2 = (sum2 + sum1) % 255;
}
uint16_t checksum = (((uint16_t)sum1)<<8) | sum2;

また、重要なメッセージには呼び出しと応答システムを使用しました。PCは、データとして元のメッセージ全体のチェックサム(元のチェックサムを含む)を含むOKメッセージを取得するまで、500msごとにメッセージを送信します。

もちろん、このスキームは、例のように端末に入力するのにはあまり適していません。あなたのプロトコルはASCIIに限定されているのでかなり良いようで、私はあなたが直接メッセージを直接読んで送信できるようにしたい簡単なプロジェクトのためにより簡単だと確信しています。大規模なプロジェクトでは、バイナリプロトコルの密度とチェックサムのセキュリティを備えていると便利です。


「[あなたの]パーサーは新しいパケットヘッダーを検出するとすべてをダンプする」ので、データ内で偶然ヘッダーが検出されても問題が発生しないのだろうか?
humanityANDpeace

@humanityANDpeaceドロップする理由は、パケットが切断されるとパケットが正しく解析されないため、いつごみを決めて先に進むのですか?最も簡単な、私の経験では十分な解決策は、次のヘッダーが入ったらすぐに不良パケットをドロップすることです。私は問題なく16ビットのヘッダーを使用していましたが、帯域幅。
BrettAM

したがって、ヘッダーと呼ぶものは、マジック16ビットの組み合わせです。すなわち、010101001 10101010、そうですか?ヒットするのは1/256 * 256の変更だけであることに同意しますが、データ内でこの16ビットを使用することも無効にします。
humanityANDpeace

@humanityANDpeaceそれは1年後ですが、エスケープシーケンスを導入する必要があります。送信する前に、サーバーは特別なバイトがないかペイロードをチェックし、別の特別なバイトでそれらをエスケープします。クライアント側では、元のペイロードを元に戻す必要があります。これは、固定長のパケットを送信できないことを意味し、実装を複雑にします。これに対処するために選択する多くのシリアルプロトコル標準があります。このトピックに関する非常に良い読み物があります
ラバーダック

1

標準に興味がある場合は、ASN.1 / BER TLVエンコーディングをご覧ください。ASN.1は、特に通信用に作成されたデータ構造を記述するために使用される言語です。BERは、ASN.1を使用して構造化されたデータをエンコードするTLV方式です。問題は、ASN.1エンコーディングがせいぜいトリッキーな場合があることです。本格的なASN.1コンパイラの作成は、それ自体がプロジェクトです(そして、特に注意が必要なのは何ヶ月も考えています)。


TLV構造だけを保持する方がおそらく良いでしょう。TLVは基本的に、タグ、長さ、値フィールドの3つの要素で構成されます。タグは、データのタイプ(テキスト文字列、オクテット文字列、整数など)と値の長さ定義します

BERでは、Tは、値がTLV構造自体のセット(構築されたノード)であるか、直接値(プリミティブノード)であるかを示します。そうすれば、XMLによく似た(ただしXMLのオーバーヘッドなしで)バイナリでツリーを作成できます。

例:

TT LL VV
02 01 FF

02値の長さが1(長さ01)および値-1(値FF)の整数(タグ)です。ASN.1 / BERでは、整数はビッグエンディアン符号になりますが、もちろん独自の形式を使用できます。

TT LL (TT LL VV, TT LL VV VV)
30 07  02 01 FF  02 02 00 FF

含む長さ7を有する配列(リスト)である2つの共に整数値を有するもの-1と値255と1つの2つの整数エンコーディングシーケンスの値を構成します。

これを単にオンラインデコーダーに投げ込むこともできます。


BERで無制限の長さを使用して、データをストリーミングすることもできます。その場合、ツリーを正しく解析する必要があります。私はそれを高度なトピックと考えています。1つは、幅優先解析と深さ優先解析について知っておく必要があります。


TLVスキームを使用すると、基本的に、あらゆる種類のデータ構造を考えてエンコードできます。ASN.1はそれよりもはるかに進んでおり、一意の識別子(OID)、選択肢(Cユニオンによく似ています)、他のASN.1構造体のインクルードなどを提供しますが、プロジェクトにとってはやり過ぎかもしれません。おそらく、今日最もよく知られているASN.1定義の構造は、ブラウザで使用される証明書です。


0

そうでない場合は、基本をカバーしています。コマンドは、人間と機械の両方で作成して読み取ることができ、大きなプラスになります。特にチャネルに長いケーブルまたは無線リンクが含まれている場合、不正な形式のコマンドまたは送信中に破損したコマンドを検出するために、チェックサムを追加できます。

産業用の強度が必要な場合(デバイスが誰かを傷つけたり死なせたりさせたりしてはいけません;高データレート、障害回復、欠落パケット検出などが必要です)、いくつかの標準プロトコルと設計手法を確認してください

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.