「#5-1 高速ソフトウェアシリアル(前編)」では、どうにか、プロトタイプの完成までこぎつけました。今回はその後編となる完結編です。
プロトタイプを使って検証を進めたところ、まず、送信側は余裕がある様です。スピードのボトルネックは受信側。115,200baudは、ギリギリ受信しています。
Windowsで動作するTera Termというソフトウェアを使って、テストをしてみました。UNOとFTDI(USB⇔UART変換モジュール)を以下表の様に接続します。
UNO | 接続 | FTDI |
GND | ⇔ | GND |
D8(RXD) | ← | TXD |
D9(TXD) | → | RXD |
D10(RTS) | → | CTS |
D11(CTS) | × | (RTS) |
※FTDIの電圧ジャンパを5V側に設定します。FTDIからRTSが出力されていますが、たぶん必要ないので接続しなくてOK。ちなみに、FTDIのRTS出力は端子には引き出されていなくて、両脇のスルーホールの中の1つに割り当たっています。
Tera Termの「Fileメニュー ⇒ New Connection」で、FTDIの通信ポートと接続します。
Tera Termの「Setupメニュー ⇒ Serial port」で以下図のように設定します。
※「Flow control」を「hardware」にします。通信ポート名は環境によって異なるので、FTDIの通信ポート名を指定してください。
実験サンプル:【 】内はTera Termからの送信データ
【abcdefghijklmnopqrstuvwxyz】 …… a〜zまでキーボードで打ち込んでENTER
>abcdefghijklmnopqrstuvwxyz(1/1)
【abcdefghijklmnopqrstuvwxyz】 …… 上の"abcdefghijklmnopqrstuvwxyz"をコピー&ペースト
>abcイYヨ+Kk境ヒ
・ノヘムユルン砒?(2/2)
う〜ん、惜しいんですけどね……。キーボードで1文字づつ打ち込んだ場合は、ちゃんと送受信できているようです。コピー&ペーストすると、恐らく、非常に短い間隔(現フレームのストップビットの直後に、次フレームのスタートビットが送信されるような間隔)で、データが送信されてくるのだと思いますが、その場合はうまく受信できていません。
何度かコピー&ペーストを繰り返してみると、どうやら、最初の方の数文字は、正しく受信できている模様。中には、「abc〜l」くらいまで受信できたケースがありましたが、文字数はバラバラです。
テストログ#1:ar52_test_1.log.txt
上のログを眺めてみると、途中から復活しているケースも見て取れます。受信コードの心配なところを変更してみたり、タイミングを微調整してみたり、コードの実行速度が向上するように手を入れてみたりしましたが、あまり状況は変わらず……。
なんとなく、ですが、「受信コード自体は、期待通りに動いているのではないか?」と思い始めました。
何か、見えない力が働いて、邪魔をしている?でも、割り込みも禁止しているし、動いているのは、このプログラムだけのはず……、!?、あっ。
ありました。もう一つ、システムが起動した時からバックグラウンドで勝手に動作しているコードが。
システムタイマ(TC0)のオーバフロウ割り込みです。millis()関数の返り値等をカウントするための割り込みが、約1ms毎に発生しているはずです。
「でも、割り込み禁止しているのでしょう?」
はい、禁止しています。
「PCINT0割り込み処理(ISR)が開始された時」に。
そうです。RXDにスタートビットの立ち下がりエッジが発生する、その直前に、タイマ(TC0)割り込みが発生していたらどうでしょうか?
ATmega328では、割り込み処理中に他の割り込み要求があっても、保留され待たされてしまいます。結果、先発のTC0割り込み処理中に発生したPCINT0割り込み要求は保留され、TC0割り込み処理が終了した後で、ようやく割り込みがかかります。
直前に処理していた割り込みの処理量次第ですが、PCINT0のISRを実行開始した時には、既に数ビット行き過ぎているかもしれません。→受信失敗する。
実験です。TC0のカウントを停止してみましょう。結果として、TC0の割り込みは発生しなくなります。
// stop TC0 (system timer)
TCCR0B &= (~0x07);
もう一度挑戦です。
実験サンプル:【 】内はTera Termからの送信データ
【abcdefghijklmnopqrstuvwxyz】 …… a〜zまでキーボードで打ち込んでENTER
>abcdefghijklmnopqrstuvwxyz(1/1)
【abcdefghijklmnopqrstuvwxyz】 …… 上の"abcdefghijklmnopqrstuvwxyz"をコピー&ペースト
>abcdefghijklmnopqrstuvwxyz(2/2)
【abcdefghijklmnopqrstuvwxyz】 …… 上の"abcdefghijklmnopqrstuvwxyz"をコピー&ペースト
>abcdefghijklmnopqrstuvwxyz(3/3)
【abcdefghijklmnopqrstuvwxyz】 …… 上の"abcdefghijklmnopqrstuvwxyz"をコピー&ペースト
>abcdefghijklmnopqrstuvwxyz(4/4)
正しく送受信できています!TC0のオーバフロウ割り込みが黒に確定した瞬間です。
盲点でした。TC0のタイマ割り込みは、約1ms毎に発生します。115,200baudでデータを連続受信した場合、1フレームのデータを受信するのにかかる時間はおよそ、10ビット/115,200baud≒86.8μs。すなわち、1ms/86.8μs≒11.5フレーム(11.5文字)受信毎に1回、TC0割り込みと競合する計算になります。
先ほどのa〜zまで26文字のコピー&ペーストテストで、途中から受信を失敗したのは、TC0割り込みと競合したところで失敗するためだったのです。
TC0割り込み処理は大したことはやっていないみたい(wiring.c)ですが……、longサイズの演算を実行しているので、退避・回復するレジスタ数も相当数になるでしょうし、1回当たり数〜10数μsの処理時間がかかっているのではないでしょうか?
1ビットの転送時間が1÷115,200baud=8.7μsという高速通信において、数〜10数μsの出遅れは、軽く1ビット以上取り逃しますので、受信を失敗しても全く不思議ではありませんね。通信速度を落として、TC0割り込みの処理時間が誤差範囲に突入すると、全く問題なく受信できるようになるはずです。
こんな時に優先順位(マスクレベル)付き割り込み機能があると何とかなるのですが、ATmega328にはその機能がありません。根本的な問題にぶち当たってしまいました。
TC0を停止してTC0割り込みが発生しないようにすると、115,200baudの受信が正常に動作することがわかったので、ここで諦める手はありません。と言って、さすがにTC0を完全に停止してしまうと、影響範囲が大きくて、実質使い物にならない恐れもありますね……。
問題になるのは、データを受信している間に発生するTC0割り込みなので、苦肉の策ですが、「受信中はTC0タイマを停止する」ではどうでしょうか?
前回説明に使った図と似ていますが、TC0の停止期間を追加しました。受信を開始して、TC2のカウンタをクリアした辺りから、TC2のカウンタがオーバフロウするまでの間、TC0を停止します。
「受信終了まで」ではなく、「TC2のオーバフロウまで」というのがミソです。常に、「受信終了」<「TC2のオーバフロウ」という関係になっているので、1フレーム受信した後も、暫くの間TC0は停止しています。その間に、連続してデータを受信すると、再びTC2がクリアされてカウントを続けるので、TC0は停止したままです。
つまり、連続して受信し続けている間、TC0は停止し続けるので、TC0割り込みと競合することはありません。
実現するために、TC2のオーバフロウ割り込みISRを登録します。
ISR (TIMER2_OVF_vect)
{
// TOI2 interrupt service routine
// TC0 resume
SerialHssw.Hssw_resume_tc0 ();
// TOI2 disable
(*Hssw_timsk2)=0x00;
}
TC2がオーバフロウすると、上のISRが実行されます。ISR内では、TC0タイマのリジューム(再開)と、TC2オーバフロウ割り込み(自分自身)の禁止を実行して終了します。TC2オーバフロウ割り込みの有効化は、PCINT0のISR(受信関数)で設定します。
// TOI2 enable
(*Hssw_tifr2)=0x01; // TOV2 flag clear
(*Hssw_timsk2)=0x01; // TOIE2 set
TC2の割り込みがフリーランしていると、また割り込みの競合相手を増やしてしまうだけで、本末転倒だからです。必要になった時だけ割り込みを設定、不要になったら割り込みを禁止、と、最小限に留める工夫をする必要があります。
ただし、ここまでやっても、最初の一回を受信開始するまで、TC0はカウントしていて割り込みが発生するので、最初の1バイトだけは割り込み競合の問題(受信失敗につながる)が残ることに注意してください。
※実はTC2割り込みにも、競合の可能性が発生することに注意。受信間隔が少し空いてしまって、丁度TC2割り込みが発生した瞬間に受信してしまうとアウト。両方ともレアケースなので、最後は「エラーレート」に織り込んで使用します。
副作用として、受信中Arduinoシステムの時間が停止します。具体的には、delay()、millis()等の関数に影響が出ます。十分に影響範囲を考慮した上でお使いください。
デフォルトでは、競合する可能性のある割り込みはTC0割り込みだけのはずなので、今回TC0を一時的に停止したことで、競合する割り込みは(ほぼ)無くなります。ただし、ハードウェアシリアル含め、SPI、I2C等の通信ユニットも割り込みを使うので、これらのライブラリと同時に使おうとすると、割り込みが競合して受信を失敗するケースが出てきます。
その場合はあきらめて、ソフトウェアシリアルの通信速度を落として使うしかありません。
ZIP形式のライブラリをリンクしておきます。
HsswSerialクラスライブラリ:HsswSerial.zip
ファイルをダウンロードしたら、Arduino IDEを起動し、「スケッチメニュー ⇒ ライブラリをインクルード ⇒ .ZIP形式のライブラリをインストール」を実行してください。ZIPファイルのパスを尋ねるウインドウが開きますので、ダウンロードしたファイルを指定すると、インストールが完了します。
「スケッチメニュー ⇒ ライブラリをインクルード 」のメニューにHsswSerialと言う項目が追加されていますので、これを選択すると、使用可能となります。
※すでに旧バージョンのHsswSerialクラスライブラリをインストール済みの場合、<My Documents>\Arduino\libraries\HsswSerialディレクトリを手動削除してから、上のインストールを実行してください。
以下、簡単な、HsswSerialクラスの説明です。
HsswSerialクラスが占有するリソースは、以下の通りです。
ターゲットはATmega328を搭載したArduino UNO,Pro Mini等となっています。他のアーキテクチャでは、コンパイル時にエラーとなります。ATmega32U4(Leonardo,Micro等)ですら対象外ですが、Leonard(Micro)では、そもそもハードウェアシリアルが使えるので、HsswSerialの出番は無かろう、と言う判断です。
少し負荷をかけてテストしてみます。以下テスト用のプログラムをUNOへコンパイル&書き込みし、プロトタイプのテスト時と同じ様にFTDIと接続してください。
ロードテストプログラム:test_hsswserial.ino
WindowsでTera Termを起動します。
Tera Termの「Fileメニュー ⇒ New Connection」で、FTDIの通信ポートと接続します。
Tera Termの「Setupメニュー ⇒ Serial port」で以下図のように設定します。
ここまでは、プロトタイプのテスト時と同じです。
Tera Termの「Setupメニュー ⇒ Recurring command」を実行して、以下の様に入力します。
文字列入力(Command)フィールドには、以下の文字列(62文字)を入力します(コピー&ペースト推奨)。
テスト用文字列:ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz
すると、Tera Termから1秒毎に上の文字列がUNOのソフトウェアシリアルに向けて送信されます。UNOで実行してあるtest_hsswserial.inoは、受信したデータを期待値と比較するのと、受信データをセンドバックするようなテストプログラムとなっています。
繰り返し実行を途中で停止するには、Tera Termの「Controlメニュー ⇒ Recurring Command」を選択して、チェックマークを外します。同じ設定で再開する時は、もう一度選択してチェックマークを付けます。
以下が、筆者がテストしたときのテストログです。
テストログ:ar52_test_2.log.txt
※だらだらとロードテストのログをご覧に入れても、データ量の無駄ですので、問題なかった部分は圧縮してあります。
時々、失敗しますね……。計63バイト(62文字+LF)の送受信180回において、期待値との比較不一致を1回、フレームエラー(HsswErrFrame0)を2回検出しました。TC0割り込みとの競合は確率問題なので、早い時期に失敗することもあれば、全く失敗しない時もあります。
エラーが起きているということは、順風満帆とは言い難いですね。ほんのちょっとした競合で、ドカッと失敗することも考えられます。それに、原理的にTC0割り込みとの競合が完全に0になったわけでは無いので、低確率ですが確実に失敗する競合タイミングが存在していることも忘れてはなりません。
なお、ロードテストの結果より、前編で議論した微調整#2のカウンタ初期値rx_cnt0の値が、もう少し大きい方が安定することが分かったので、値を変更しました。
// PCINT0 to ISR 1st instruction delay
const int intr_ovhd=int (16*4.0 + 0.5);
rx_cnt0=(intr_ovhd + prescl/2)/prescl;
こんな微妙な調整で、受信が安定したり、不安定になったり……、兎に角、115,200baudをソフトウェアで実現するのはシビアなことみたいです。
以下図はArduino側信号波形です。
最初のRXDでTera Term(FTDI)から改行コード(CR+LF)を受信しています。それに対して、「>(13/32)<CR><LF>」というレスポンスをArduino→FTDI(Tera Term)へ返した(送信した)時の波形です。Arduinoからの送信中にRTS信号がネゲートされる制御が確認できます。
もう一つ、受信バッファがオーバフロウしないようにRTSをネゲートする様子を確認しておきましょう。まずは、スペース(0x20)を3回受信したときのArduino UNO側RXD波形から。
スペースのASCIIコード0x20には、"1"が1ビットしか存在せず、後は全て"0"のビットです。LSBファーストで受信するので、上図の様に、唯一の"1"が49μs辺りのHIGHパルスで、74μs辺りのHIGHパルスはストップビットです。すぐにスタートビットが来て、連続して3回のスペースを受信していることがわかります。
この波形を頭に叩き込んだ状態で、以下図を眺めます。
※上図をクリックすると、別ウインドウが開いて拡大表示します。
スペースを受信した時の波形を思い出すと、HIGHパルスが2本、角の様に出るのでしたね。一番上段のRXD波形を眺めると、2本角の波形が連続しています。そう、上図はスペースを128バイト程連続で受信した時の波形となっています。
始めの方は問題なく受信していますが、後半に入ると受信動作が間に合わなくなり、時折RTS(最下段)がネゲートされています。RTSネゲート部分から矢印を引いておきましたが、その部分でストップビット後の"1"状態が延長され、暫くFTDIからの送信が停止された様子も見られます。
※HsswSerial(Arduino)がRTSをネゲートしてから、FTDIが送信を一時停止するまで、1フレーム程度オーバランしてしまっています。RTSをネゲートしても、「急には止まれない」と、言うことでしょうね。あんまりギリギリのタイミングでネゲートしても間に合わない(相手側の制御次第)、ということは覚えておきましょう。
受信側のRTS制御も、期待通りに実行できているようです。
さあ、だいぶ安定して受信できるようになったとは言えど、ハードウェアシリアルの場合ほどの安定度には程遠い感じです。実際のアプリケーションでどうか?ですね。
「#4-10 ArduinoとESP32の非同期シリアル通信」で作成した、「TELNET(もどき?)サーバwith ESP32」がArduino UNOで使用できるように、#ifdefを追加して試してみましょう。
#ifdef ARDUINO_AVR_LEONARDO
#define USE_SERIAL
#define SerialESP32 Serial1
#elif ARDUINO_AVR_PRO
#define SerialESP32 Serial
#elif ARDUINO_AVR_UNO
#include <HsswSerial.h>
#define SerialESP32 SerialHssw
#endif // ARDUINO_AVR_LEONARDO, ARDUINO_AVR_PRO, ARDUINO_AVR_UNO
// esp32
#ifdef HsswSerial_h
SerialESP32.begin (SERIAL_BAUD, SERIAL_8N1 | Hssw_RTS_ENBL | Hssw_LGC_NEGA);
#else // HsswSerial_h
SerialESP32.begin (SERIAL_BAUD);
#endif // HsswSerial_h
UNOとESP32との接続は、以下の通りです(UNOの場合、端子名はスラッシュ(/)の後ろ側を使います)。
フロウ制御を持たないハードウェアシリアルのために、ソフトウェアで制御したRTS信号を出力(D3)していましたが、その信号は使わず、SerialHsswが出力するRTS(D13)を接続します。D3の方のRTSは、そのまま浮かせておいても実害ありませんが、TC1タイマ割り込みだけは止めておく必要があります。割り込みの競合で受信を誤りますから。
#ifndef HsswSerial_h
// TC1 timer interrupt setting
:
:
noInterrupts ();
TCCR1A=0x00;
TCCR1B=0x08 | clk_sel;
OCR1A=ocr;
TCNT1=0x0000;
TIFR1=0x02;
TIMSK1=0x02;
interrupts ();
#endif // HsswSerial_h
使わないのにTC1の設定を変更する必要は無いので、TC1の設定変更する部分を丸ごと実行から外しました。これで、TC1を別な用途(PWM出力など)に使用することが可能です。
UNO対応版プログラム:telnet_server_esp.ino
※オリジナルのプログラムを更新したので、「#4-10 ArduinoとESP32の非同期シリアル通信」からダウンロードしても、同じプログラムを入手できます。
通信ソフトウェアTera Termを使って、192.168.0.xxx:23に対してTELNET接続してみてください。IPアドレスは環境によって異なります。確認方法は「#4-10 ArduinoとESP32の非同期シリアル通信」を参照してください。
特にプロンプトを表示していないので、「無言」ですが、以下の様なコマンドを打って動作を確認してみましょう。
実験サンプル:【 】内はTELNET端末からの送信データ
【who】
OK: telnet_server_esp32 ver.1.02
【ping,192.168.0.1】
+PING:1
【time】
+CIPSNTPTIME:Sat Mar 16 11:55:14 2019
一応、問題なく期待通りの動作を確認しました。
まあ、動くでしょうね……、この程度なら。TC0割り込みとの競合で、バイチャンスで受信を失敗するので、失敗した時にどうか?ってことですけど……。それは暫く使ってみないと、何とも言えません。
ともあれ、前編で立てた目標、「UNOとESP32を115,200baudでシリアル接続する」ことは、一応達成できた、と言うことにしてください……。
ソフトウェアシリアルを自分でコーディングしてみて、ボトルネックがどこにあるのか?など、色々と理解が進みました。時間を止めてその間に受信するなんて、通信速度∞の荒業(?)。一十一みたい……(1^^;)。
後日、「#5-3 高速ソフトウェアシリアル(続編)」にて、高速ソフトウェアシリアルライブラリ(HsswSerial)を改定し、Arduino UNOでの115,200baud通信がノーエラー(受信データに誤りがない)で使用可能となりました。以下にリンクを張っておきますが、上の本文中でリンクしたオリジナルプログラムを更新したので、どちらからダウンロードしても、同じ(更新版)プログラムを入手できます。
HsswSerialクラスライブラリ:HsswSerial.zip
既に旧バージョンをインストール済みの場合、更新版の.ZIPファイルを指定して、「.ZIP形式のライブラリをインストール」しても、「HsswSerial というライブラリは既に存在します。」と言われて上書きインストールしてくれません。まず、旧バージョンのインストールディレクトリを手動で削除してください。
スケッチがセーブされるディレクトリにLibrariesというディレクトリがありますので、その下のHsswSerialディレクトリを削除してから、更新バージョンのZIPファイルをインストールして使ってください。
インストール方法については、こちらを参照してください。
※本ページ文中の記述は変更していませんので、更新版プログラムと異なる記述が残っている事に、ご注意ください。