数理設計研究所 GIDトップページ
Win32API シリアル受信プログラム
デバッグの一例
数理設計研究所 矢澤正人 2006/1/30
Index
シリアルプログラミングは厄介者
開発環境
ターゲット通信仕様
開発するソフトの仕様
バッファオーバーランエラー
解決の糸口
DCB構造体
解決?
参考リンク


シリアルプログラミングは厄介者
 OSやアーキティクチャを問わず、RS232C周辺のプログラミングはなかなか手強いものです。
 とりわけWin32APIを使ったRS232通信アプリケーションの作成は厄介で、とりあえず動くものを作るのは簡単でも、CPU速度やOSバージョン、チップセットの違い、あるいはオンボードシリアルとUSB-シリアル変換器によるバリエーションが無限にあり、いつでも確実に動くコードを書くのは簡単なことではありません。
 RS232Cに限った話ではないのですが、物理的な装置の制御を伴う処理は相手先が安定な装置とは限らず、問題をソフトウェア部分だけに閉じ込めることができないのです。デバッグツールも、オシロや回線モニタなどが必要になる場面が多々あります。
 ここでは、数理設計が標準的に使用している社内製RS232用クラスライブラリがうまく動かなかった事例を紹介します。

開発環境

・PC Pentium4 2.4GHz Memory 1G デスクトップタイプ、オンボードシリアル×2
・OS WindowsXP SP2
・開発環境 Boland C++Builder5
・開発リソース RS232C.CPP、RS232C.H

ターゲット通信仕様

 GID-SSS/IF-232の通信仕様
・19200bps、Stop1, Parity無し、8data
・0x0dターミネートを含む全18byteのキャラクタ文字列を10ms毎に連続送信
・電源ONと同時に、PCからの指示無しでデータ送信を開始。電源OFFで停止 
・GID-SSS ファームウェアソースコード ZIP,PIC16F628用
開発するソフトの仕様
・アプリケーション起動時、COMポートをCreateFileして回線を開く
・ユーザからの指示により任意のタイミングで受信開始
・受信したデータを行単位で演算、表示、記録(たとえば、計測震度の計算や加速度galのグラフ描画など)

バッファオーバーランエラー

 上記仕様では、受信開始までの時間が長いと受信バッファがオーバーフローします。
 バッファサイズを4096byteに設定した場合、4096byte÷18byte/10ms = 約2.3秒でオーバーフローすることになります。
 オーバーフロー発生後、下記の状況に陥ることがわかりました。
  ・ReadFileを実行に失敗、バッファ内容を読み出せない。GetLastError = 995
  ・PurgeCommに失敗、バッファ内容をクリアできない
  ・デバイスのエラーフラグをクリアするClearCommError関数を実行しても回復しない
 ターゲットソフト終了後ハイパーターミナルを起動すると受信は成功します。従ってハードウェアの問題ではありません。
問題解決の糸口
 .  バッファサイズを増やすなりバッファがいっぱいになる前に適当にデータを読み捨てるなりすれば問題は回避できますが、しかしこれでは根本的な解決とは言えません。
 ReadFile関数実行後にGetLastError関数でエラーコードを確認しました。
  ・エラーコード995 ERROR_OPERATION_ABORTED
  ・「スレッドの終了またはアプリケーションの要求によって、I/O 処理は中止されました。」
 いまひとつ意味不明で的を得ません・・・。
 WinAPI32関数を調べるとClearCommErrorという関数があることがわかりました。MSDNを検索すると下記の説明があります。
 通信エラーの情報を取得して、通信デバイスの現在の状態を通知します。
 通信エラーが発生した場合に呼び出し、デバイスのエラーフラグをクリアして次の入出力(I/O)操作を可能にします。
 この関数を使えば解決すると思いきや、ClearCommErrorを実行してもやはり上記問題は解決しません。ClearCommError関数には成功しています。
 2回連続でClearCommError関数を呼べばエラーコードのクリアを確認できるはずですが、エラーはクリアされません。
 ClearCommErrorでlpErrors を確認すると、常にCE_RXOVERビットが立っています。ときどき、CE_BREAKとCE_FREAMEも立っており、稀に、CE_OVERRUN、CE_RXPARITYも立っています。
 回線の初期状態により、RXOVER、BREAK、FRAMEのフラグが立つのは納得できますが、OVERRUN(送信バッファの?)とパリティエラーの検出は納得できません。
エラーコード 内容
CE_RXOVER 0x01 入力バッファのオーバーフローが発生しました。
入力バッファに空きがないか、EOF の後に文字を受信しました。
CE_BREAK 0x10 ハードウェアがブレーク条件を検出しました
CE_FRAME 0x08 ハードウェアがフレーミングエラーを検出しました。
CE_OVERRUN 0x02 文字バッファがいっぱいになりました。次の文字は失われます。
CE_RXPARITY 0x04 ハードウェアがパリティエラーを検出しました。
 エラー検出後にClearCommEvent関数を実行するとCE_RXOVER以外のビットをクリアすることができました。CE_RXOVERビットをクリアすることはできませんでした。

DCB構造体を変更
 シリアル通信デバイスの制御設定を定義するDCB構造体に、fAbortOnErrorというパラメータがあります。
 念のため、MDSNより日英両方を転載します。
fAbortOnError  エラーの発生時に読み取りおよび書き込みの操作を終了するかどうかを指定します。このメンバがTRUEの場合、 ドライバはエラーが発生したときに読み取りおよび書き込みの操作をすべて終了させ、 エラー状態を返します。ドライバは、 アプリケーションがClearCommError関数を呼び出してエラーに対する受領通知を完了するまで、 これ以降の通信操作を受け付けません。
If this member is TRUE, the driver terminates all read and write operations with an error status if an error occurs. The driver will not accept any further communications operations until the application has acknowledged the error by calling the ClearCommError function.
 さらに、ClearCommError関数の解説に、備考として下記の記載があります。これも日英両方をMSDNより転載します。
備考  通信ポートの設定時にDCB構造体のfAbortOnErrorメンバにTRUE値がセットされている場合、 通信ソフトウェアは、 通信エラーが発生したときに、 ポート上でのすべての読み取りおよび書き込みの操作を終了します。この場合、 アプリケーションがClearCommError関数を使って通信エラーに対する受領通知を完了するまで、 新しい読み取りおよび書き込みの操作は受け付けられません。
Remarks If a communications port has been set up with a TRUE value for the fAbortOnError member of the setup DCB structure, the communications software will terminate all read and write operations on the communications port when a communications error occurs. No new read or write operations will be accepted until the application acknowledges the communications error by calling the ClearCommError function.

 これらを読む限り、fAbortOnErrorビットの意味は下記のように読み取れることができます。このビットがTrueのときに発生したエラーはClearCommError関数によりクリアすることができるはずですが、実際にはそうはなりませんでした。
fAbortOnError=True  エラー発生時にデバイス操作を強制終了。その後ClearCommError関数の実行によりエラーは解除され操作を再開できる
fAbortOnError=False エラー発生時もデバイス操作は続行する。


解決?
 試しにfAbortOnErrorビットをFalseに設定してコードを走らせて見ると、バッファオーバーランから回復することができました。
 ソースコード中、ClearCommErrorを必ず2回連続で記述してあるのは、1回目でエラーコードを読み、2回目でエラーコードがクリアされたことを確認するためです。
//初期設定(RS232Cクラスのコンストラクタで実行)
 ComHandle = CreateFile(buf, GENERIC_READ | GENERIC_WRITE, 0, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);  
 SetupComm(ComHandle, RX_BUF_SIZE, TX_BUF_SIZE);
 GetCommState(ComHandle, &dcb); //DCB構造体の内容を取得する
 BuildCommDCB(param, &dcb);
 SetDCB();
 SetTimeOut( 0 ); // MAXDWORD

//受信開始処理
 Sleep(1000);//意図的にバッファオーバーフローを起こす

 dcb.fAbortOnError = false; //ここを=Trueにすると、以降受信不能になる
 bool error = rs->SetDCB();

 error = rs->PurgeComm();//バッファクリアのはずが、AbortOnError=Falseの時もクリアできていない
 int err = GetLastError();
 error = ClearCommError(s->ComHandle,&lpErrors,&lpStat);//エラークリア //AbortOnError=Falseの時、
 error = ClearCommError(rs->ComHandle,&lpErrors,&lpStat);//エラークリア //←ここでlpError=0が確認できる

 int rxsize = rs->Read(rx_new,16383); //バッファ内全部を読み捨ててバッファをクリア
 err = GetLastError();
 error = ClearCommError(rs->ComHandle,&lpErrors,&lpStat);//エラークリア
 error = ClearCommError(rs->ComHandle,&lpErrors,&lpStat);//エラークリア

 Sleep(200);
 rxsize = rs->ReadLine(rx_new,0x0d,16383); //中途半端かもしれない先頭行を読み捨てる
 error = ClearCommError(rs->ComHandle,&lpErrors,&lpStat);//エラークリア
 error = ClearCommError(rs->ComHandle,&lpErrors,&lpStat);//エラークリア

 Sleep(200);
 rxsize = rs->ReadLine(rx_new,0x0d,16383); //18byteのデータを行単位で読み込む
 error = ClearCommError(rs->ComHandle,&lpErrors,&lpStat);//エラークリア
 error = ClearCommError(rs->ComHandle,&lpErrors,&lpStat);//エラークリア


 上記ソースは受信開始処理部までですので、このあとに通常の受信処理を記述します。
 バッファサイズを4096byteにしたときに約2.3秒まではオーバーフローしないので、余裕をみてバッファサイズをこの4倍、16384byteとすると、なんらかの理由で9.2秒もの長時間受信処理を実行できなかったとしてもデータを取りこぼす心配はありません。
 100msに設定したシステムタイマのイベントで受信処理を行いながら、同時にIlustlater,AutoCad,Wordなどの重いソフトを起動しても、バッファオーバーランが発生することはありませんでした。
 逆に、システムタイマを50msに設定してポーリングさせていると、まれに1バイトも受信できていないことがありました。
 データは確実に一定間隔で送信できているので、なんらかの理由でタイマが連続して起動しているものと思われます。

 PurgeComm関数を呼んでもバッファがクリアされない問題が解決していませんが、ReadFileで読める限りのデータを読み出すことで回避しています。
 fAbortOnErrorフラグをTrueにしたときのClearCommError関数の処理結果がおかしい問題も解決していません。
 シリアルのデバイスドライバのバグを疑い、試しに秋月のUSBシリアル変換ケーブル(通販コードM-720)を使用して実験してみたところ、オンボードシリアルと同様の結果が得られました。
 PC界の大先輩に聞くところによると、「常時送信しっぱなしの相手なんて想定していないんじゃないの?」とのことです。
 なんでも、マイクロソフトはIBMのコンソールとして使用可能かどうかをシリアルポート動作確認の基準にしているらしく、そこから外れたことはあまり考えていないそうです。
 真偽の程は判りませんが、ハイパーターミナルが600bpsをサポートしないのは大昔のPCのハード設計の都合(クロック設計がうまくいかなかった)、110bpsをサポートするのは骨董品メインフレーム(VAX11とか?)との通信をサポートするためなんだとか。
 巷はUSBや高速LANが花盛りですが、RS232Cはまだまだ健在です。
参考リンク
MSDNライブラリ 日本語、マイクロソフト。Win32APIの関数や変数を参照することができる
MSDN Library 上記の英語版。日本語訳が怪しいとき以外でも参照するのがお勧め
MSDN コミュニケーション関連関数 

VC++ML VC++プログラミングのメーリングリスト
Belution.com プログラマのための掲示板

How to collect data from monitors 大阪大学医学部? リアルタイムデータ処理の詳細解説。マルチメディアタイマの利用など。
Win32エラーコード一覧 シーラ家。GetLastErrorの返り値一覧。winerror.hより抽出し作成