2026-02-02 0.1版 (受信記載)
2026-02-02 0.2版 (送信記載)
2026-02-05 0.3版 (CRC-IV)
2026-02-07 0.4版 (記事調整)
簡易なMCU実装では透過なバイトストリームそのままでも問題ありません。 どうせ基板上の数センチの配線ですし、壊れることは基本的にありません。 しかし、ケーブルで引き回して 「ちょっとUARTのデータが化けるんだよね」 を心配し始めると、エラー時に再送できるような形で、 もう少し凝ったパケット伝送レイヤが欲しくなります。
今回はこのデータリンク層のことだけを考え、 トランスポート層のことは別途ということにしたいと思います。 そうする理由はこの上に遠隔でサブルーチンを呼び出す(RPC)ための UDP的なトランスポート層が乗っかる、という想定を置くためです。
用途としてはホストから小型MCUをコマンド操作する何か、を考えています。 コマンドを送ったらレスポンスが帰って来るべきであり、 従って本質的には半二重通信をすることになります。 もっとも、 ここで扱う送信アルゴリズムはRS-232/RS-422などの全二重のハードウェアを前提にしています。 RS-485などの半二重バスの物理層を目的にするためには送信アルゴリズムでの完了処理を もう少し捻る必要があります。
UARTはバイトストリームを転送します。 チェックサムを付与する単位を構成するパケットの始まりと、 終わりを認識するマーカーとなるバイト値を予約する必要があります。 とりあえずSTXで始まり、データとなるバイトが続き、ETXで終わるものをパケットとします。
パケットはバイナリ伝送ですからSTXやETXと同じ値をペイロードに含めたい事例は必ず発生します。 そのために、DLEを予約します。 DLEの直後に届く1バイトについては、 STX, ETX, DLEと同じ値のバイトが来てもデータとして受信し、 特別な解釈をしません。
本稿では特定のバイト値は指定しません。 オシロスコープなどのデバッグ機能で追いかけやすい値として、伝統的にはSTX=0x02, ETX=0x03になるでしょう。 ここで想定されるバイナリ通信では0x02等はデータとして頻出値であり、 通信が非効率になりやすいため、 効率を気にするならPPP等に倣って0x7E等の値を使ったほうが良いかも知れません。 今回はそこまでは考えていません。
STX+ETX+DLEでは最悪時に約2倍のパケット長になります。 これを嫌ってCOBSと呼ばれるエンコード方法も存在します。 それではどちらが有利なのかというと、アプリケーション依存です。 なぜなら、COBSはオンザフライでエンコードすることが難しいアルゴリズムだからです。 一般論としては、COBSを採用するときには、 生データのバッファからエンコード済みバッファにデータをコピーするための タスクレットを実装することになります。 これがMCUのリソースを消費するなどの実装上の負荷を生みます。
COBSはSTX+ETX+DLEと比較すると、 ワイヤ効率が安定している特徴があります。 そのかわり、COBSは元の生データに追加で エンコード済みバッファを必要とします。 MCU実装では、このバッファは通常静的に確保されます。 このほか、メモリコピー処理に伴うCPU負荷の有無という違いもあります。
ここではSTX+ETX+DLEを採用することにし、 COBS方式のフレーミングおよびエンコードやデコードのアルゴリズムについては触れません。
データリンク層なので自ら再送する機能は組み込みません。 この上にremote procedure call layer (RPC)が乗っかる予定で考えています。 そこでは、クライアントであるホスト側が データにシーケンス番号を振って遠隔手続き呼び出しのリクエストを送ります。 サーバ側であるMCU側ではシーケンス番号を記録し、関数を呼び出し、結果を送り返します。 このときホスト側はタイムアウトを監視して、タイムアウトしたら5回まで再送します。 MCU側では、シーケンス番号が同じである限り関数の再実行をしません。 その場合には直前に実行したはずの結果をただ再送するのみとします。 概要としてはこんなところです。
このため、データリンク層は再送に責任を負いません。
最大ペイロード長は32byteから64byteぐらいまでのレンジを考えています。 受信側では想定を超える長さのデータが届いたら破棄するものとします。 ペイロードはもう少し長くてもよくて、 CRC16の誤り検出能力であれば問題はありません。 実装用の仕様とするときは、ペイロード長をいずれかに定めて統一してください。 この資料はアルゴリズムと設計思想を提供する目的の文書であることから、 ここではペイロード長は固定しません。
チェックサムのアルゴリズムはRFC1171付録に定義されたPPP系統のCRC16計算ルーチンと同等であると仮定します。 このアルゴリズムを実装したライブラリルーチンが、 util/crc16.h のヘッダにおいてインラインアセンブリ展開としてAVR libcに収録されています。 AVR側はこれをそのまま参照し、リンクするのが良いでしょう。 ホスト側はRFC1171付録の記述をテストベクタにして別途実装するか、 そのまま引用するか等の対応をします。
CRC16を計算するときに送信側と受信側の双方において、 初期値(IV)とエンディアン(バイト順序とシフト方向)は合致している必要があります。 チェックサム計算結果のワードの格納順序は、 ここではAVR MCUにおける実装の単純さを優先し、 送受信の規約として下位バイト先行を選ぶものとします。 もっとも、後述の送信ロジックでは結局はバイト単位で取り出して送っているので、 ここで敢えて上位バイト先行を選んでも計算コストの差異はほとんどありません。 実装する場合はどちらかに統一してください。
CRC16の初期値(IV)については、RFC1171付録の標準としては0xFFFFを使います。 AVR MCUの特性から言うと、 ここでは0x0000つまりゼロ値を初期値にしたいという強い誘惑に駆られるはずです。 しかし、できるだけ有効な誤り検出をしたいという目的からは、 CRC16の初期値(IV)に0x0000つまりゼロ値を選んではいけません。 なぜならば、 初期値(IV)がゼロ値のCRC16を採用した場合には、 先頭にゼロのバイトが続くパケットについてそのゼロの長さが変わってもCRC16の計算結果が変わらない、 という問題があるからです。 これでは先頭ゼロ挿入と先頭ゼロ欠落のエラー検出能力がない(劣る)ということになります。 通信規約として脆弱性とみなされる余地において、 除外できるものは除外したほうが良いと考えられます。
PPP系統でもXMODEM系統でも、CRC16を計算するための生成多項式そのものは CRC16 CCITTと呼ばれるものと同じものです。 ここでは詳細には触れませんので、気になるようであれば別途調査してください。
チェックサムアルゴリズムの選定としては、 生データにチェックサムを連結し、 全体を通して計算したチェックサム計算結果がゼロになるように選ぶものとします。 これは後述する送受信アルゴリズムの前提になっています。 簡単なプログラムを実装して検証してください。 チェックサム計算ルーチンの詳細そのものにはここでは触れませんので別途調べてください。
誤解の余地がないように次のパラメタを書き残しておきます。 ホスト用または他のMCUのために既存ライブラリを設定する際の参考としてください。
reflect in = true
reflect out = true
polynomial = 0x1021
initial value = 0xFFFF
final xor = 0x0000
先にPPP系統のチェックサムと書きましたが、 この資料で策定し利用するチェックサムアルゴリズムは PPP用途の計算ルーチンを流用しているに過ぎません。 念のため、ご注意ください。
チェックサムはデータ部分の後ろに配置し、ETXの直前に配置します。 受信側では、既に述べたようにチェックサムは通しで計算します。 生のデータの後ろに生のチェックサムを続けて並べて全体を通して 計算したチェックサムの結果がゼロであることを確認します。 チェックサムの値によっては、送信時においてDLEでエスケープされていなければなりません。 制御バイトであるDLEそれ自身はチェックサムの計算には含めません。
STXをチェックサム計算に含めるか否かは設計上の選択肢があります。 今回はSTXそれ自身はチェックサム計算に含めないことにしましょう。 ETXもチェックサム計算を終えるためのマーカでしかないため、 チェックサムそれ自体に含むことはありません。 もちろん、ETXの後ろにチェックサムを配置することもしません。 STXとETXはパケットを構成するフレーム境界を定めるためのマーカです。 DLEもエスケープするためのマーカです。
制御バイトを含まない生データのみで計算されたCRCというチェックサムが教えてくれることは、 チェックサムが合致しさえすれば同じ生データを構成する バイトシーケンスを送信側と受信側が共有したであろう、ということになります。 送受したいと考えている情報はSTXやETXやDLEそのものではなく生データです。 生データが正しく受け取れたと推定できるなら伝送路としてはそれで十分です。
送信側はパケットを構成して送れば良いのですが、 どんな細分化をするかによっていくつか選択肢があります。 ここではMCU向けのロジックを提示します。
DLEの概念があるため、POSIX向けなどにベタな実装をするときは、 パケットのイメージが最大で生データ長の約2倍になりうることを覚悟してください。 実際にはエスケープ対象がSTX, ETX, DLEそれ自身のみになるため、 2倍まで膨れることはほとんどありません。 それでもバッファオーバーフローには注意して実装する必要があります。
受信側においてバイト間タイマーで制限を掛けていますので、 送信側がPOSIX系ホストであれば、 カーネルが許すバッファの範囲で「まとめて送信」する必要があります。
構成されたパケットは必要に応じて再送できる必要があるでしょう。
パケットを送信するロジックを考えます。 このルーチンは送信用バッファを上位から受け取り、 UART送信レディ割り込みで1バイトずつ送信するものとします。 ひたすら回り続ける受信ロジックと異なり、送信ロジックには初期化ハンドラがあります。 ルーチンとして空のフレームを送ることができてしまいますが、 それを回避する責任までは送信ルーチンでは負わないことにします。 本ルーチンでは送信バッファには生データしか入らないのでコンパクトです。
割り込みから割り込みの間などで大域的に保持されるべき主要な変数は次の通り。
初期化ハンドラは次の手続きを実行します。 初期化ルーチンをBUSY状態で呼ぶ場合の動作は未定義とします。 そうならないよう、呼び出し元が責任を持たねばなりません。
送信割り込みハンドラは次の手続きを実行します。 1呼び出しにつき1バイトだけ送信できることに注意して実装します。
受信側の対応を考えましょう。 受信処理としては同期を維持し、何らかのトラブルになったら静かに受信フレームを破棄します。 受信バイト列が破綻していない限り、バッファ溢れ等で受信が続行できなくても、 受信フレームの追跡は行い、同期は維持されます。 整合性が確認されたパケットだけが、上位層に引き渡されます。
受信ルーチンにおいてペイロード長がゼロであった場合はどうしたら良いでしょうか。 この場合は上位層にペイロード長がゼロであることを伝え、 判断を上位層に委ねるものとするのが良いでしょう。 実装仕様を定めるためのオプションとして、 上位層の処理をサボるためにペイロード長ゼロのパケットは破棄するという選択もあり得ます。
ここでは有限の状態を持つステートマシンを構成する必要があります。
後述するDATA状態およびESCAPE状態で稼働するべきタイマーを準備してください。 パケットを受信しているその途中においてバイト受信からバイト受信まで一定時間を超えたときは、 ケーブルが抜けた等のパケット伝送エラーですから、 受信済みパケットの欠片は捨ててIDLE状態に戻らなければなりません。
なお、このタイマーが監視するのはバイト受信からバイト受信であって、 パケット開始からパケットの終了までの時間ではありません。 送信側がもたもたするなどの、大人の事情があるときは適宜見直してください。
タイマーでバイト間受信間隔にエラーを検出したときは、 タイマを止めてIDLE状態に巻き戻します。
必須となる変数
注意事項
受信ルーチンはUART受信割り込みで捌かれるものと想定します。 つまり、割り込み内で1バイトだけ処理して、リターンする構造です。

先に届いていたはずのDLEバイトそれ自身をチェックサムに含めてはいけません。