前編、後編(完結編)と来て、続編です……。ゴメンナサイ、本当にこれで終わりにしますm(1__)m。
今回はXMODEMを実装して、高速ソフトウェアシリアルライブラリ(HsswSerial)のスループットやエラーレートなどを評価したいと思います。
XMODEMと言うのは、「パソコン通信」と呼ばれていた時代、伝送路に接続された2機のパソコン間で、バイナリデータを転送するためのプロトコルとして使われた方式の一つです。
前回、前々回で作成した高速ソフトウェアシリアル通信を実現するHsswSerialクラスの評価を、もう少し定量的に実行したいと思いました。まず、テキスト(ASCII 7ビット)データの転送だと、データの最上位ビットが常に0であり、データに偏りがあります。第二に、転送したデータ(文字列)の整合性(正しさ)を評価しにくいことが、問題でした。
バイナリデータの転送を行うプロトコルを使うと、データは0x00〜0xffまで満遍なく転送可能ですし、チェックサムなどによって、正しく転送できたかどうかを評価することも可能となります。ある程度の大きさのデータを転送すれば、スループットを評価可能なことも嬉しい。
バイナリデータ転送プロトコルの中で、XMODEMを選択したのは、単に、「実装が簡単そう」だったから、です。
今回の目的は、「HsswSerialクラスのスループットやエラーレートなどを定量的に評価すること」、であって、「大量のデータを伝送路を通して転送すること」ではないから。RAMを2Kバイトしか持たないArduino UNOでは、大きなバイナリデータをやり取りする機会は、まず、無いでしょうから、難しい(複雑な)プロトコルを頑張って実装しても徒労に終わるだけです。
よって、ハングアップ検出(タイムアウト)他、エラーチェックなども必要最小限に留めます。通信相手にWindowsのTera Termを想定して、Tera Termとの間でXMODEM転送ができたらOKとします。更に、「通信速度の定量的評価」が目的なので、
と、します。
XMODEMの仕様を確認しておきましょう。
XMODEMの仕様を検索する(別ウインドウが開きます)
要約すると、概ね以下の様になります。
プロトコル | |||
受信側 | 方向 | 送信側 | 説明 |
転送開始 | |||
NAK | → |
受信側から送信側へ「送信要求」 プロトコルはNAKから始まる |
|
← | ブロック#1 | 最初のブロックを送信 | |
ACK | → | 正しく受け取れたことを通知 | |
← | ブロック#2 | 次のブロックを送信 | |
NAK | → | エラーがあったことを通知 | |
← | ブロック#2 | 前の送信がエラーだったので、同じブロックを再送信 | |
ACK | → | 正しく受け取れたことを通知 | |
← | ブロック#3 | 次のブロックを送信 | |
: | 繰り返し | ||
← | ブロック#N | 最後のブロックを送信 | |
ACK | → | 正しく受け取れたことを通知 | |
← | EOT | 転送終了を通知 | |
ACK | → | 了解 | |
転送終了 | |||
CAN | → | (受信側からのキャンセル要求、必要に応じて) | |
← | CAN | (送信側からのキャンセル要求、必要に応じて) |
現在転送中のブロックに対する再送信要求しか出せないので、何らかの原因でブロック番号が飛んでしまった場合等は、抜けた部分のデータを再送信要求する術が無いなど、品質の悪い伝送路での使用には色々と課題が多いのですが、シンプルで解り易いプロトコルです。
特に解説することの無いほどシンプルなので、具体的な実装に関しては完成版プログラム(test_xmodem.ino)でご確認ください。
※後日、「#6-6 ポータブル気温・湿度ロガ」にて、XMODEMを使って、実際のバイナリデータファイルを送受信できるようにプログラムを改造しました。非同期シリアル通信経由でバイナリデータの送受信を行いたい場合は、そちらをご参照ください。
XMODEMの転送関数の中で、転送したデータ数と、何らかのエラーが発生した回数をカウントしておき、転送終了後に表示します。また、スループットを計測するために、転送開始から終了までに要した時間を計測する必要があります。
いつもなら、millis()関数を使って時間を計測するのですが、肝心なHsswSerialクラスが、受信中にシステムタイマを停止する仕様なので、millis()では正しい時間を計測できません。
そこで、「#2-5 リアルタイムクロックを使ってみる」で使用したRTC(DS1302)モジュールを使って、開始/終了時刻を読み取ることにします。この時、秒カウンタを以下の様に定義して、時刻から秒カウンタへ変換することで、転送時間を秒単位で計測します。
秒カウンタ=(HH×60+MM)×60+SS
millis()と異なり、秒単位での計測になるので、ある程度大きなデータを転送しないと精度良い計測ができないものと考えられます。
参考までに、115,200baudの理論上のスループットは以下の通りです。
スループット=115,200baud/(10[ビット/フレーム])=11.25Kバイト/s
生のバイナリデータをひたすら転送した場合の理論上最大値です。
※XMODEMなどのプロトコルを使うと、プロトコルに使用する通信も115,200baudの内数となりますし、エラーが発生して再送信すると、その分もスループットが落ちることになります。
例えば、60s程度で転送することを考えると、60×11.25≒675Kバイト程度のファイルを転送すれば良い計算ですね。PC(Tera Term)側からファイルを送る場合には、適切に偏りのないバイナリデータを送信した方が良いので、ZIPやJPG等、データ圧縮されたファイルが適切です。今回の実験用プログラムでは、Arduinoから転送するデータは乱数なので、これも適切に偏りのないバイナリデータと考えることができます。
そこで、最初にArduinoからデータを受信しておき、受信したファイルを、Arduinoへ送信するデータとして使うこととします。
Arduino、USB⇔UART変換モジュール(FTDI)、RTCモジュールを以下の様に接続します。
UNO | Micro | 接続 | FTDI |
GND | ⇔ | GND | |
D8(RXD) | D0(RXD) | ← | TXD |
D9(TXD) | D1(TXD) | → | RXD |
D10(RTS) | (なし) | → | CTS |
接続 | RTC(DS1302) | ||
5V | VCC | → | VCC |
GND | ⇔ | GND | |
D5 | → | RST | |
D6 | ⇔ | DAT | |
D7 | → | CLK |
※FTDIのジャンパは5V側。
※ソフトウェアシリアルの評価はUNO、ハードウェアシリアルの評価はMicroを使います。「同時に接続する」という意味ではないので注意。
実験用プログラムをリンクしておきます。
実験用プログラム:test_xmodem.ino
プログラムの先頭付近に、以下の様な#defineがありますので、評価するシリアルクラスと通信速度を設定してから、Arduinoへコンパイル&書き込みします。
#define USE_HSSW | // | HsswSerial | 高速ソフトウェアシリアルを使います | |
#define USE_SW | // | SoftwareSerial | 標準ソフトウェアシリアルを使います | |
#define USE_HW | // | HardwareSerial | ハードウェアシリアルを使います(要Micro等) | |
#define USE_USB | // | UsbSerial | USB⇔UART変換を使います | |
#define SERIAL_BAUD | 115200 | // 通信速度[baud]を指定します |
USE_*系はどれか一つを残して、残りをコメントにしてください。SERIAL_BAUDとTera Termのシリアル設定を合わせることをお忘れなく。
Tera Termを起動します。
Tera Termの「Fileメニュー ⇒ New Connection」で、FTDIの通信ポートと接続します。
Tera Termの「Setupメニュー ⇒ Serial port」で以下図のように設定します。
HsswSerial以外のシリアルクラスを使うときには、「Flow Control=none」に変更してください。通信ポート名は環境によって異なりますので、FTDIの通信ポート名を設定してください。
すべてが正しく構成されていれば、Tera TermのウインドウがArduinoと接続されます。
実験サンプル:【 】内はTera Termからの送信データ
【who】
OK: test_XMODEM ver.1.00
【xsend,128】 …… Arduinoからランダムなデータ128KバイトをXMODEMで送信します。
OK+ ready, waiting...
「waiting」と表示されたら、Tera Termの「Fileメニュー ⇒ Transfer ⇒ XMODEM ⇒ Receive」を実行してください。受信するデータのファイル名を問い合わせるウインドウが開きますので、適切な名前を入力してください。進行状況のウインドウが表示され、転送したデータ数がカウントアップされて行きます。ウインドウが自動的に閉じたら、成功です。
実験サンプル:続き
OK: xsend bytes=131072 errs=3 time=86
bytesが転送したデータ数、errsは転送中に発生したエラー数、timeが転送に要した時間[s]です。
Tera TermでXMODEMの転送が始まると、Arduinoからのメッセージは全てXMODEMに吸い取られてしまうので、万一、回復不可能なエラーが起きて途中で転送を終了した場合には、UNOのオンボードLEDが点灯するようになっています。その場合は、Tera Termの進行表示ウインドウをCancelボタンで閉じてください。……転送は失敗しています。
実験サンプル:【 】内はTera Termからの送信データ
【xrecv】 …… ArduinoがバイナリデータをXMODEMで受信します。
OK+ ready, waiting...
「waiting」と表示されたら、Tera Termの「Fileメニュー ⇒ Transfer ⇒ XMODEM ⇒ Send」を実行してください。送信するデータのファイル名を問い合わせるウインドウが開きますので、適切なファイルを指定してください。進行状況のウインドウが表示され、転送したデータ数がカウントアップされて行きます。ウインドウが自動的に閉じたら、成功です。
実験サンプル:続き
OK: xrecv bytes=131072 errs=3 time=14
これを、必要なクラスライブラリと通信速度に対して計測して、まとめてみます。
データを取りながら、「あ〜、やっぱりエラーは消えないな〜。バイチャンスで失敗するのだな〜」などと、肩を落としながら、「何とかならないものか?」と、頭はフル回転……。
ピンっと来た!ひらめいちゃった。
課題を解決可能なアイディアが思い浮かんでしまいました。「これなら行ける」、確信がありました。居ても立っても居られず、指がスルスルとviを駆り、ものの10分で修正(アイディアの具現化)が終了しました。
浮かんだアイディアと言うのは、RXDに使用する端子の端子変化割り込み(PCINT0)を使っていたところを、TC1のインプットキャプチャ割り込み(ICI1)に変更する、それだけ。
もちろん、割り込み要因を変更するだけだったら、同じことです。TC0割り込みとの競合で失敗するのは変わらないでしょう。しかし、ICI1に変更するのは、割り込み要因だけの問題ではありません。インプットキャプチャ、します。
インプットキャプチャと言うのは、外部端子ICP1(D8)の変化イベントを捕まえて、端子変化が起きた時点のカウンタ値TCNT1を、インプットキャプチャレジスタICR1に取り込む機能なのです。この処理はハードウェアで自動的に実行されます。また、インプットキャプチャが起きた時に割り込みを要求する機能があるので、同時に割り込み(ICI1)を要求します。
上図の様に、TC0割り込みが先行して、ICI1割り込みが遅れて発動したとしても、インプットキャプチャはハードウェアによって、立ち下がりエッジ(スタートビット)が起きた時のカウンタ値TCNT1をICR1レジスタに取り込んでいます(緑○)。ICI1割り込みのISR内最初の命令で、その時のTCNT1(青○)とICR1の値を引き算すると、立ち下がりエッジから、どのくらいの時間が経過したか?を、知ることができます。
タイマカウンタTCNT1は、受信したフレームのスタートビット(立ち下がりエッジ)を起点(0x0000)に設定して、タイミングを取りたいので、TCNT1=TCNT1−ICR1に設定すると、目的を果たすことができます。
※旧プログラムでrx_cnt0に相当する処理を、より正確に、かつ、TC0割り込みとの競合有無も含めて実行することができます。
この場合、最悪、TC0割り込み処理がスタートビット(115,200baudで約8.7μs)を食い潰してしまったとしても、最初のデータ(ビット0)のサンプリングタイミングまでにICI1割り込み処理が間に合えば、データは取りこぼすことなく、受信できるはずです。
では、改定後のHsswSerialクラスライブラリをリンクしておきます。
HsswSerialクラスライブラリ:HsswSerial.zip
前回「#5-2 高速ソフトウェアシリアル(後編)」参照したのと同じ元データを差し替えたので、どちらからダウンロードしても改定版をダウンロードできます。
既に旧バージョンをインストール済みの場合、更新版の.ZIPファイルを指定して、「.ZIP形式のライブラリをインストール」しても、「HsswSerial というライブラリは既に存在します。」と言われて上書きインストールしてくれません。まず、旧バージョンのインストールディレクトリを手動で削除してください。
スケッチがセーブされるディレクトリにLibrariesというディレクトリがありますので、その下のHsswSerialディレクトリを削除してから、更新バージョンのZIPファイルをインストールして使ってください。
改定版では、最悪、スタートビットをミスするので、フレームエラー(HsswErrFrame0)が発生する場合がありますが、何とかデータ部分は正常に取り込めているようです。SerialHssw.errcodを参照してエラー処理する時に注意していただければ、問題なく使用できるはずです。……具体的には、フレームエラー(HsswErrFrame0)を無視する。
ノーエラーで115,200baud通信ができることもさることながら、何より、影響(ペナルティ)の大きかったTC0停止が必要なくなったので、本格的に応用することができると思います。
※ここで使う「ノーエラー」とは、「実データにエラーが無いこと」と言う意味で使用しています。TC0との競合などで設定されるフレームエラー(HsswErrFrame0)は、エラーとしてカウントしません。
改定版HsswSerialクラス使用上の注意事項をまとめておきます。
ん?ってことは、普通にmillis()で時間を計測できるのか……。まあいいや、修正するのも面倒なので、このままRTCで測定してしまおう。
次節に掲載するのは、改定版HsswSerialでの評価結果となります。
以下、筆者環境で計測した結果のまとめ表です。「複数回測定して平均」とか、していないので、そのつもりで眺めてください。
以下、考察を交えながら、デバッグと評価の結果について雑感です。
プログラム(test_xmodem.ino)をご覧いただければわかりますが、XMODEM部分のコードは全く共通で使っていて、使用するシリアルクラスだけをマクロ等で切り替えています。よって、使うクラスライブラリ以外の条件は同じです。
絶対的な指標とはなり得ないまでも、相対的指標としては、十分に意味があると思っています。
いくつか制約はありますし、貴重な16ビットタイマ(TC1)を占有使用してしまうという、ペナルティも意外と大きいかもしれません。しかし、HsswSerialクラスの特長としては、何と言っても115,200baudで通信可能なことです。ハードウェアシリアルが実質使用不可能なUNO等の機体で、高速シリアルインターフェイスが必要な場合のソリューションになり得ると思います。
話は変わりますが……、久しぶりにRTCを接続して日時を取得してみたところ、+15分程度ずれていました。はっきりとは覚えていませんが、前回時刻を設定したのは、4か月くらい前だったろうか?記憶が正しければ、日差+7.5s……。
処理速度等の関係で、RXDとTXDの制御は、直接ポートレジスタを叩きたいのですが、RTSとCTSの制御は、それほど処理速度を要求していないため、digitalWrite()/digitalRead()関数で制御すると共に、begin()メソッドのパラメータで使用する端子番号を指定可能としました(ver.1.12)。
HsswSerialクラスライブラリ:HsswSerial.zip
いつもの様に、オリジナルを差し替えてあるので、どのリンクからダウンロードしても、最新版が入手できます。
新しいbegin()メソッドの仕様は以下の通りです。
既に旧バージョンをインストール済みの場合、更新版の.ZIPファイルを指定して、「.ZIP形式のライブラリをインストール」しても、「HsswSerialというライブラリは既に存在します。」と言われて上書きインストールしてくれません。まず、旧バージョンのインストールディレクトリを手動で削除してください。
スケッチがセーブされるディレクトリにLibrariesというディレクトリがありますので、その下のHsswSerialディレクトリを削除してから、更新バージョンのZIPファイルをインストールして使ってください。
インストール方法については、こちらを参照してください。
※本ページ文中の記述は変更していませんので、更新版プログラムと異なる記述が残っている事に、ご注意ください。