トップ回答者
COMポートとして認識されたUSBデバイスからの読み込み

質問
-
COMポートとして認識されているUSBデバイスがあります。COMポートに情報取得のコマンドを書き込むとUSBデバイスが状態を応答を返すようになっています。USBデバイスには動作指示のコマンドと情報取得のコマンドの2種類があり、動作指示はコマンドを書き込むことでUSBデバイスが意図した通りに動作しています。
一方の情報取得のコマンドは書き込みは成功しています。情報取得のコマンドを書き込んだ後に、読み込みを行うと応答がありません。USBデバイスが不調と思い、Teratermを利用して情報取得のコマンドを書き込むと意図した応答を得られます。応答は体感で1秒に満たない時間で得られます。
Teratermと同等のことをVC++から行いたいと考えています。
最初はWriteFileでCOMポートに書き込んだ直後にReadFileでCOMポートを読んでみると、0バイトでした。調べていくとSetCommMaskで受信イベントを扱う方法があると分かったので、WaitCommEventでイベントの待ち合わせを開始したら、別スレッドで情報取得コマンドの書き込みを行う様にしました。そうした結果、WaitForSingleObjectに指定した3秒のタイムアウト時間を超過し、タイムアウトしました。
処理タイミングが問題だとは思っていますが、どのように修正を行うのがいいのかが分かりません。情報取得を行うメソッドを抜粋したものが下のコードになります。非同期I/Oのセオリー的な情報などでも非常に助かります。
SetLastError(NO_ERROR); // COMポートのオープンに成功すると プロパティ m_hComPort にハンドルが設定されている this->Open(); // openに失敗していたら終了 if ( this->m_hComPort == INVALID_HANDLE_VALUE ) { return ; } OVERLAPPED overlapped; ZeroMemory(&overlapped, sizeof(overlapped)); overlapped.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); // COMポートの受信イベントマスク if (SetCommMask(this->m_hComPort,EV_RXCHAR) == FALSE){ OutputDebugString("SetCommMaskに失敗\n"); } // COMポートの受信待ち DWORD dwEvtMask = 0; DWORD dwTransfer = 0; if (WaitCommEvent(this->m_hComPort,&dwEvtMask,&overlapped) == FALSE) { if (GetLastError() != ERROR_IO_PENDING) { OutputDebugString("WaitCommEventがERROR_IO_PENDING以外になった \n"); goto end; } // 書き込み処理を別スレッドで起動する CWinThread* pReadThread = AfxBeginThread(ReadDeviceIdSendThread, this); DWORD waitResult; if ( (waitResult = WaitForSingleObject(overlapped.hEvent, 3000)) != WAIT_OBJECT_0 ) { switch( waitResult ) { case WAIT_TIMEOUT: // took longer than 3 second - cancel it and give up CancelIo(this->m_hComPort); OutputDebugString("EV_RXCHARイベント WAIT_TIMEOUT\n"); break; case WAIT_ABANDONED: OutputDebugString("EV_RXCHARイベント WAIT_ABANDONED\n"); break; default: break; } } } if ( dwEvtMask == 0 ) { OutputDebugString("WaitCommEventで発生したイベントでエラーが起きた\n"); goto end; } // ------------------------------------- // 読み込んだデバイスID BYTE deviceId = 0x00; // 読み込んだサイズ DWORD dwReadSize; // COMポートの受信イベントチェック if ((dwEvtMask & EV_RXCHAR) == EV_RXCHAR) { // COMポートの受信データチェック COMSTAT comst; DWORD dwErr; OutputDebugString("COMポートから受信した\n"); ClearCommError(this->m_hComPort,&dwErr,&comst); // COMポートからデータ受信 if(ReadFile(this->m_hComPort,&deviceId, sizeof(deviceId), &dwReadSize, &overlapped) == FALSE) { if (GetLastError() == ERROR_IO_PENDING) { // コマンド送信が完了するまで待機します GetOverlappedResult(this->m_hComPort, &overlapped, &dwReadSize, TRUE); CString msg; msg.Format("読み込みが成功しました。readsize=[%d], device_id=[%x]\n",dwReadSize, deviceId); this->m_bCommand[0] = deviceId; } OutputDebugString("ReadFile に失敗した\n"); }else{ // 受信成功 OutputDebugString("ReadFile に成功した\n"); } } CString msg; msg.Format("デバイスIDを読み込みのが成功しました。readsize=[%d], device_id=[%x]\n",dwReadSize, deviceId); OutputDebugString(msg); end: // TODO CloseHandleを追加する this->Close();
回答
-
通信条件は本当に合致しているのでしょうか。
よくあるのは、DTRとか、CTSを常にONにする必要があるとかです。
TeraTermは、たしかフローなしを選択するとも、ONにするはずなので。あとは、シリアルアナライザをかませてみるとかでしょうか。
jzkey
- 回答としてマーク masahiro akiyama 2015年1月16日 9:06
すべての返信
-
佐祐理 様
返信ありがとうございます。
COMポートの初期化は行っています。実装は下のようになっています。ボーレートなどのCOMポートの設定値はiniファイルに定義してあり、iniファイルの定義値は プロパティ m_dcbComPortに反映されています。
Teratermで試した時は ボーレートなどの値をiniファイルと同じにしました。
void UsbDongle::Open(void) { // オープン中の場合には一度クローズします if (this->m_hComPort != INVALID_HANDLE_VALUE) { this->Close(); } CString comFileName; comFileName.Format("\\\\.\\%s",this->szPortName); // COMポートをオープンします this->m_hComPort = CreateFile(comFileName, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, NULL); if (this->m_hComPort == INVALID_HANDLE_VALUE) { this->m_pLogger->OutLog4Message("COMポート(%s)がオープンできませんでした (%d)", this->szPortName, GetLastError()); this->OutLog4MessageFromLastError("COMポートがオープンできませんでした (%s)"); return; } // ComPortに溜まっているデータを廃棄します PurgeComm(this->m_hComPort, PURGE_TXABORT | PURGE_RXABORT | PURGE_TXCLEAR | PURGE_RXCLEAR); // 現在のCOMポートの設定情報を取得します DCB dcb; ZeroMemory(&dcb, sizeof(dcb)); dcb.DCBlength = sizeof(DCB); GetCommState(this->m_hComPort, &dcb); // iniファイルの定義値をCOMポートに反映します。 if (this->m_dcbComPort.BaudRate != VALUE_COMPORT_UNKNOWN) { dcb.BaudRate = this->m_dcbComPort.BaudRate; } if (this->m_dcbComPort.Parity != VALUE_COMPORT_UNKNOWN) { dcb.Parity = this->m_dcbComPort.Parity; } if (this->m_dcbComPort.ByteSize != VALUE_COMPORT_UNKNOWN) { dcb.ByteSize = this->m_dcbComPort.ByteSize; } if (this->m_dcbComPort.StopBits != VALUE_COMPORT_UNKNOWN) { dcb.StopBits = this->m_dcbComPort.StopBits; } if (this->m_dcbComPort.fDtrControl != VALUE_COMPORT_UNKNOWN) { dcb.fDtrControl = this->m_dcbComPort.fDtrControl; } if (this->m_dcbComPort.fRtsControl != VALUE_COMPORT_UNKNOWN) { dcb.fRtsControl = this->m_dcbComPort.fRtsControl; } if (SetCommState(this->m_hComPort, &dcb) == FALSE) { CloseHandle(this->m_hComPort); this->m_hComPort = INVALID_HANDLE_VALUE; this->m_pLogger->OutLog4Message("COMポート(%s)の通信設定ができませんでした (%d)", this->m_usbDongleData.szPortName, GetLastError()); return; } }
-
>処理タイミングが問題だとは思っていますが、どのように修正を行うのがいいのかが分かりません。情報取得を行うメソッドを抜粋したものが下のコードになります。非同期I/Oのセオリー的な情報などでも非常に助かります。
自分は通信のコードは次のようにやってます。
全て「可能ならば」の前提ですが、(送信)
1.送信処理はスレッド化しない。
2.送信処理は同期(ブロッキング)モード(=完了/失敗まで関数が戻らない)で行う。(受信)
3.受信は必ずスレッド化する。
4.受信スレッドの寿命は、ハンドルの寿命と同じか、又は長い。
5.受信スレッド内で待機関数を使用して待つ。
6.受信スレッドは受信以外は行わない。
6.1 単一又は複数回の受信により、受信データの最小限の完全性は確認する。
6.2 即座に受信データキューに受信データをコピーし、その旨を親に伝える。
6.3 心配なら受信バッファをクリアする
6.4 待機関数に戻る。(親)
7.通信対象のハンドルの寿命を管理する。
8.スレッドの寿命を管理する。
9.受信キューの監視を行う。
てな感じです。 -
私が質問内容をちゃんと把握していないのかもしれませんが。。。
> WaitCommEventでイベントの待ち合わせを開始したら、
> 別スレッドで情報取得コマンドの書き込みを行う様にしました。
> そうした結果、WaitForSingleObjectに指定した3秒のタイムアウト時間を超過し、
> タイムアウトしました。個人的には、上記処理部分が問題のような気がしています。
上記で言及されている「別スレッドで情報取得コマンドの書き込みを行う」処理は、提示されたコードの「// 書き込み処理を別スレッドで起動する」でコメントされている部分に該当すると思いますが、このコードだと、WaitCommEvent() == FALSE かつ LastErrorCode == ERROR_IO_PENDING の状態になった場合にのみ、「情報取得のコマンド」が発行される。。。ということになるのでしょうか?
MSDN で公開されている WaitCommEvent() API の解説を確認する限り、提示されているコードでこのような状況が発生するのは、「1文字受信したけど、受信処理をただちに完了させられない場合」というケースに限定されると思います。
だとしたら、「情報取得のコマンド」を発行する前に、既に受信データが入力バッファにセットされているのは、おかしいのでは。。。と思うのです。
(WaitCommEvent() コールは、WriteFile() でコマンドを送信した後に行うのが一般的のような気がします。)
仲澤@失業者さんへ
先の仲澤@失業者さんの返信では、「送信処理(WriteFile)は同期 I/O で、受信処理(ReadFile)は非同期 I/O で行う」ということを提案されていると私は理解したのですが、この認識であっていますでしょうか?
もし私の認識があっているのであれば、受信スレッドの具体的な待機方法を教えていただけますでしょか?
(私の足りない脳味噌では、「2.送信処理は同期(ブロッキング)モード(=完了/失敗まで関数が戻らない)で行う。」とした場合に、「5.受信スレッド内で待機関数を使用して待つ。」というをどのような実装にすればよいのか、思い浮かびませんでした。。。)- 編集済み お馬鹿 2015年1月7日 9:15 追記
-
お馬鹿さま、コメントありがとうございます。
前提があやふやな発言でした。この点の反省を踏まえてご返答申し上げます。
>先の仲澤@失業者さんの返信では、「送信処理(WriteFile)は同期 I/O で、受信処理(ReadFile)は非同期 I/O で行う」ということを提案されていると私は理解したのですが、この認識であっていますでしょうか?
その通りですが、先の発言は、発言者さんの発言の引用部分である、
>非同期I/Oのセオリー的な情報などでも非常に助かります。
の部分への発言です。
1.まず、「可能ならば」の話なのは先の自分の発言にある通りです。
2.次に、通信対象のハンドル自体が同期か非同期かについて
何もコメントしていない点をご確認ください。
3.Comポートを使うことを前提としていません
WriteFile()、ReadFile()、またそれらのハンドル構築時の
オプションも無関係です。
で、手元にはComポートのコードが見つからなかったので
TCPサーバーのコードで似たようなのを探すと、
(受信スレッド内)
while( 1){
複数イベント待機関数();
switch(イベント種別判定と状態判定()){
case 切断: 切断処理関数(); return;
case 受信: 受信処理関数(); break;
}
イベントのリセット();
}
送信は、単にsend()してるだけでした。
受信処理は一度のrecv()で満了できない場合の処理がやや複雑ですが、
特に難しいことはやってないようでした。
同期で受信しても良いようなコードですが、
なるべく、一か所での分岐にしたかったのかもしれません。
大昔のコードなので忘れました(vv;)。
さて、Comの場合なのですが、WaitCommEvent()等を工夫すれば
似たような処理ができるかもしれません。
もちろん1文字受信を同期で待つ方法もあります。
さて、本件では、確実に受信することがポイントであると考えられます。
従って、受信コードの独立性と、確実性が重要なポイントだと思います。
ゆえ、スレッド化をお勧めしました。- 編集済み 仲澤@失業者 2015年1月7日 9:45
-
仲澤@失業者さん、ご回答いただきありがとうございます。
先に投稿された内容の趣旨は、大筋で理解しましたが、まだ理解できない部分があるので、申し訳ありませんが質問させてください。> 2.次に、通信対象のハンドル自体が同期か非同期かについて
> 何もコメントしていない点をご確認ください。
> 3.Comポートを使うことを前提としていません
> WriteFile()、ReadFile()、またそれらのハンドル構築時の
> オプションも無関係です。WinSock の場合、ioctlsocket() API が用意されているので、任意のタイミングで同期/非同期のモード切り替えが可能になっている。。というのが、私の理解です。
この場合、仲澤@失業者さんが提案された方法での実現が可能であろうことは、私にも理解できます。
ですが、既に上記で指摘されているように、COM ポート デバイスの場合、デバイス ハンドルの生成時に同期/非同期のモードが決定されてしまい、以降モードの切り替えは不可能なはずです。
「ハンドル構築時のオプションも無関係」とし、かつ送信処理を同期モードとした場合、受信処理ではどのような同期オブジェクトを使って、どのような API でデータを読みにいけばいいのでしょうか?
(先の投稿で明記せずに申し訳ありませんでしたが、私が教えていただきたかったのは、この部分です。)度々の質問で申し訳ありませんが、ご教授いただければ幸いです。
(masahiro akiyama さん、このスレッドで勝手に質問してしまってごめんなさい。。。)- 編集済み お馬鹿 2015年1月8日 1:10 追記
-
実行済みとは失礼しました。
更に明後日な方向のコメントになってしまいますが、Windows Vista以降を仮定できるのであれば、AfxBeginThreadではなくThread Pool APIを使用するのはどうでしょうか?
WaitCommEvent()、WriteFile()、ReadFile()など完了待ちがややこしくなっていると思いますが、
- CreateThreadpoolIo()
- StartThreadpoolIo()
- WaitCommEvent()、WriteFile()、ReadFile()などOVERLAPPEDを要求する関数
の順に呼び出すと、3.の処理完了時に1.で指定したコールバック関数が新たなスレッド上で呼び出されます。
-
-
仲澤@失業者 さん、ご回答ありがとうございます。
>自分は通信のコードは次のようにやってます。
> 全て「可能ならば」の前提ですが、
を参考して、コードを組みなおしてみます。佐祐理さん
Thread Pool APIの具体的なコードは下のブログの最後にあるようなものでしょうか?https://earlgreyx.wordpress.com/2013/06/09/vista%e4%bb%a5%e9%99%8d%e3%81%a7%e3%81%af%e5%ae%89%e6%98%93%e3%81%ab%e3%82%b9%e3%83%ac%e3%83%83%e3%83%89%e3%82%92%e4%bd%9c%e3%82%8b%e3%81%aa%ef%bc%81%ef%bc%9f/
仲澤@失業者さんのコメントを含めて考えると、下の流れになりますか?
- コールバックには COMポートから読み込む処理を書き、
SubmitThreadpoolWork
でスレッドプールに処理を投げる COMポートに書き込みを行う
- WaitForSingleObject でスレッドプールの処理完了を待ち合わせる。
まだ、試せてないのですが、動きを想像したときに、
StartThreadpoolIo
から処理が戻ってくるタイミングをつかめませんでした。今回は、WriteFileで書き込みを行った直後にReadFileで読み込みを行いたいと考えています。この場合に、StartThreadpoolIo
から戻ってくるのはWriteFileが終わり、ReadFileで読み込める状態になっているのでしょうか。お馬鹿さん
勉強になるので、気になさらないでください。
- 編集済み masahiro akiyama 2015年1月8日 11:49 誤記
- コールバックには COMポートから読み込む処理を書き、
-
通信条件は本当に合致しているのでしょうか。
よくあるのは、DTRとか、CTSを常にONにする必要があるとかです。
TeraTermは、たしかフローなしを選択するとも、ONにするはずなので。あとは、シリアルアナライザをかませてみるとかでしょうか。
jzkey
- 回答としてマーク masahiro akiyama 2015年1月16日 9:06
-
原因の切り分けのためにも、最初から対象機器と通信して動作確認をするのではなく、パソコンのCOMポート同士を接続してターミナルソフト等を通信相手として、動作をエミュレーションする事をお勧めします。ターミナルなどの通信相手とした通信で問題が無ければ、後は制御線の制御やCOMポートの設定の問題と判断出来ます。通信障害のリカバリや異常値の受信時のテストなども行おうとすると、必ず相手機器をエミュレーションするプログラムなどを作る必要がありますので、相手機器をエミュレーションするプログラムを先に作るのも良いと思います。
私見ですが送信と受信を別スレッドにする事はお勧めしません。殆どの場合において送信が正常に行われたか受信データを元に判断する必要があるはずです。送信と受信は同期的に密結合した関係にあるので、これを別々のスレッドに別けてしまうと状態フラグや排他制御が増えてしまい実装が困難になります。例えば障害時にポートのオープンからやり直すとして、スレッドを跨いだ状態遷移とか考えたらうんざりしませんか?送信と受信を非同期に実行することを前提にプロトコルが設計されているとか、送信と受信を別けることで大幅なパフォーマンス向上を期待できるとかなら、別ですけどね。
PIPE等で要求を受け付けて「受信バッファのクリア→要求送信→応答受信→再送などのエラー処理→PIPEなどで結果を返す」と言ったスレッドを作ることをお勧めします。
甕星
-
みなさんの回答を頼りに、RS232Cテストツールhttp://homepage2.nifty.com/nonnon/Chinamini/20090829.htmlで応答を得られることを目指しました。その結果 DTRと RTSをオンにすることで 解決しました。
そう思ってソースを見返すと、this->m_dcbComPort.fDtrControl と this->m_dcbComPort.fRtsControl にDISABLEが設定されていましたでの ENABLEするとプログラムの中でも応答を得ることができました。
今回は慣れないCOMポートとの通信であったので、皆さんの回答が勉強になりました。