クレア工房 | 電子工作 | AVR | UARTによるデータリンク層の構築案

UARTによるデータリンク層の構築案

はじめに

簡易なMCU実装では透過なバイトストリームそのままでも問題ありません。 どうせ基板上の数センチの配線ですし、壊れることは基本的にありません。 しかし、ケーブルで引き回して 「ちょっとUARTのデータが化けるんだよね」 を心配し始めると、エラー時に再送できるような形で、 もう少し凝ったパケット伝送レイヤが欲しくなります。 OSI参照モデルに倣うなら、これをデータリンク層と呼びます。 データの再送そのものはトランスポート層の仕事です。

今回はデータリンク層のことだけを考え、 トランスポート層のことは別途ということにしたいと思います。 そうする理由はこの上に遠隔でサブルーチンを呼び出す(RPC)ための UDP的なトランスポート層が乗っかる、という想定を置くためです。

パケット構成

UARTはバイトストリームを転送します。 チェックサムを付与する単位を構成するパケットの始まりと、 終わりを認識するマーカーとなるバイト値を予約する必要があります。 とりあえずSTXで始まり、データとなるバイトが続き、ETXで終わるものをパケットとします。

パケットはバイナリ伝送ですからSTXやETXと同じ値をペイロードに含めたい事例は必ず発生します。 そのために、DLEを予約します。 DLEの直後に届くバイトについては、 STX, ETX, DLEと同じ値のバイトが来てもデータとして受信し、 特別な解釈をしません。

自らは再送しない

データリンク層なので自ら再送する機能は組み込みません。 この上にremote procedure call layer (RPC)が乗っかる予定で考えています。 そこでは、クライアントであるホスト側が データにシーケンス番号を振って遠隔手続き呼び出しのリクエストを送ります。 サーバ側であるMCU側ではシーケンス番号を記録し、関数を呼び出し、結果を送り返します。 このときホスト側はタイムアウトを監視して、タイムアウトしたら5回まで再送します。 MCU側では、シーケンス番号が同じである限り関数の再実行をしません。 その場合には直前に実行したはずの結果をただ再送するのみとします。 概要としてはこんなところです。

このため、データリンク層は再送に責任を負いません。

最大ペイロード長

最大ペイロード長は32byteから64byteぐらいまでのレンジを考えています。 受信側では想定を超える長さのデータが届いたら破棄するものとします。 ペイロードはもう少し長くてもよくて、 CRC16の誤り検出能力であれば問題はありません。

チェックサムの選択

チェックサムのアルゴリズムはRFC1171付録に定義されたPPP互換CRC16計算ルーチンと同等であると仮定します。 このアルゴリズムを実装したライブラリルーチンが、 util/crc16.h のヘッダにおいてインラインアセンブリ展開としてAVR libcに収録されています。 AVR側はこれをそのまま参照し、リンクするのが良いでしょう。 ホスト側はRFC1171をテストベクタにして別途実装するかそのまま引用するか等の対応をします。

チェックサムとしての条件を満たすにあたり、CRC16の初期値(IV)はどんな値でも問題ありません。 送信側と受信側の双方において、 チェックサム計算上の初期値(IV)と、 CRC計算上の数学的特徴を利用するためのエンディアン(バイト順序とシフト方向)は合致している必要があります。

RFC1171付録の標準としては初期値(IV)に0xFFFFを使います。

生データにチェックサムを連結し、全体を通して計算したチェックサム計算結果がゼロになれば問題ありません。 簡単なプログラムを実装して検証してください。 チェックサム計算ルーチンそのものにはここでは触れませんので別途調べてください。

チェックサムによって保護される範囲

チェックサムはデータ部分の後ろに配置し、ETXの直前に配置します。 受信側では、既に述べたようにチェックサムは通しで計算します。 生のデータの後ろに生のチェックサムを続けて並べて全体を通して 計算したチェックサムの結果がゼロであることを確認します。 チェックサムの値によっては、送信時においてDLEでエスケープされていなければなりません。 制御バイトであるDLEそれ自身はチェックサムの計算には含めません。

STXをチェックサム計算に含めるか否かは設計上の選択肢があります。 今回はSTXそれ自身はチェックサム計算に含めないことにしましょう。 ETXもチェックサム計算を終えるためのマーカでしかないため、 チェックサムそれ自体に含むことはありません。 もちろん、ETXの後ろにチェックサムを配置することもしません。 STXとETXはパケットを構成するフレーム境界を定めるためのマーカです。 DLEもエスケープするためのマーカです。

制御バイトを含まない生データのみで計算されたCRCというチェックサムが教えてくれることは、 チェックサムが合致しさえすれば同じ生データを構成する バイトシーケンスを送信側と受信側が共有したであろう、ということになります。 送受したいと考えている情報はSTXやETXやDLEそのものではなく生データです。 生データが正しく受け取れたと推定できるなら伝送路としてはそれで十分です。

送信ルーチン

送信側はパケットを構成して送れば良いので、 そのアルゴリズムは自明なこととして、 今のところは話を省きます。 将来的にインライン化を考えたいと思います。

DLEの概念があるため、 パケットのイメージが最大で生データ長の約2倍になりうることを覚悟してください。 実際にはエスケープ対象がSTX, ETX, DLEそれ自身のみになるため、 2倍まで膨れることはほとんどありません。 それでもバッファオーバーフローには注意して実装する必要があります。

構成されたパケットは必要に応じて再送できる必要があるでしょう。

受信バッファ

受信バッファは、受信ルーチンから1バイトずつデータを受け取ります。 パケットが完成すると受信バッファのポインタが上位に引き渡され、 参照されるだけのワークエリアとなります。 上位は参照が終わって用事が済んだバッファを受信ルーチンに返却します。

上位とどのような連携をするのか、工夫する余地のある部分です。

受信ルーチン

受信側の対応を考えましょう。 ここでは有限の状態を持つステートマシンをふたつ構成する必要があります。

それから、後述するDATA状態およびESCAPE状態で稼働するべきタイマーを準備してください。 パケットを受信しているその途中においてバイト受信からバイト受信まで一定時間を超えたときは、 ケーブルが抜けた等のパケット伝送エラーですから、 受信済みパケットの欠片は捨ててIDLE状態に戻らなければなりません。 なお、このタイマーが監視するのはバイト受信からバイト受信であって、 パケット開始からパケットの終了までの時間ではありません。 送信側がもたもたするなどの、大人の事情があるときは適宜見直してください。

実装の都合上、受信バッファには一時的にチェックサムが入ります。 上位が期待するデータ長がたとえば32byteであれば、 チェックサムの2byteを足した34byteのサイズが必要です。

タイマー発火について
タイマーでバイト間受信間隔にエラーを検出したときは、 タイマを止めてIDLE状態に巻き戻します。
IDLE状態
IDLE状態はSTXが届くのをひたすら待ち続けます。
DATA状態
DATA状態はデータを受信する本体です。
ESCAPE状態
ESCAPE状態はデータを受信する補助をします。

受信バイトをバッファに格納するときは格納の可否を判断するための状態を持つ必要があります。

ACCEPT状態
フレームが受け入れられる状態。
DROP状態
フレームを受け入れられない、バッファには書き込めないけれども、 フレーム状態の同期は続ける状態。

Copyright © 2026 clare. All rights reserved.