シリアル通信でのフロー制御

解決


おっ  2005-10-19 04:56:03  No: 18156

達人テクニックについてきた TComm を使ってデータ送信するプログラムを書いています。
送信先の機械のバッファサイズがあまり大きくないないため、総送信サイズが大きくなると
おそらくバッファオーバーフローで、正しい結果が得られません。

送信側・受信側共にフロー制御をXon/Xoffで行っています。
Windows側が勝手にフロー制御してくれるものだとばかり思っていたのですが、
どうも違うような気がしてきましたが、ネットで検索しても手がかりに鳴りそうな
情報がみつかりません。

そこで質問なのですが、
このとき、Xoffが送られてきたことを検出するには、どうしたらいいのでしょう?
勝手に止まってくれると思っていたのですが、どうも停止する様子もありませんし、
Xoffを検出して、自分で送信を止めるものなのでしょうか?
Windowsが勝手にやってくれるものだから、やっぱり不要なのでしょうか?

実際に書き込んでいる部分は、このようになっています。
丸々コピーですが、流れとしては、TCommX等の別のコンポーネントと同じようになっています。

  if not WriteFile(FHandle, Buffer, Size, dwBytesWritten, @FWriteOs) then
  begin
    if GetLastError = ERROR_IO_PENDING then // オーバーラップ処理時
    begin
      WaitForSingleObject(FWriteOs.hEvent, INFINITE);

      while not GetOverlappedResult(FHandle, FWriteOs,
      dwBytesWritten, True) do
      begin
  if GetLastError = ERROR_IO_INCOMPLETE then  // まだ完了しない
    Continue
  else
  begin
    ClearCommError(FHandle, dwError, @Stat);
    Break;
  end;
      end;
    end
    else
    begin                                   // その他のエラー発生
      ClearCommError(FHandle, dwError, @Stat);
      raise ECommReadWriteError.Create(dwError);
    end;


kkk  2005-10-19 18:39:09  No: 18157

Xon/Xoffのコードは相手の機器と合っていますか?
普通はXon(^Q)/Xoff(^S)ですがまれに異なる時があります。
またXoffを送った時に直ちに相手の送信データがとまるということではありません。
すでに送信しているデータもあるのでXoff送信後数バイトは送られることがあります。
WindowsでWriteFileでまとめて送信すると相手の機械が遅い場合はデータを取りこぼすこともあります。
COMMTIMEOUTSのWriteTotalTimeoutMultiplierで一文字あたりの送信時間を長めに設定することも効果があるかも知れません。
また通信ドライバによっては上記設定が効かない場合もあるようです。
そのような時はソフトで1文字ずつ細切れに送ってやるようにすれば対応できると思います。(Xon/Xoffの処理はドライバでやってくれます)
またXonXoffをドライバで処理させずに自分(ソフト)で処理することも出来ます。


おっ  2005-10-19 19:47:32  No: 18158

kkkさんレスありがとうございます。

> Xon/Xoffのコードは相手の機器と合っていますか?
> 普通はXon(^Q)/Xoff(^S)ですがまれに異なる時があります。
これを疑って、機械の説明書を眺めているのですが、今のところ記述がみつかっていません。

> (Xon/Xoffの処理はドライバでやってくれます)
これは、WriteFileで勝手に書き込み(?)しても、Windows側で送信停止してくれるということでしょうか?
ちなみに、モデム相手の通信処理ではないので、専用ドライバは使っていません。

> またXonXoffをドライバで処理させずに自分(ソフト)で処理することも出来ます。
自分で処理する場合、受信したデータを確認し、Xon/Xoffの判定を行うということでしょうか?
ほかの人の作ったコンポーネントに頼っていて、基本を理解しきれていないとは思いますが、
「どうやって処理する」という方針というか手がかりになるものが見えていない状況です。
もう少し踏み込んだ説明をお願いします。

> COMMTIMEOUTSのWriteTotalTimeoutMultiplierで一文字あたりの送信時間を長めに設定することも
これについては、試してみます。


kkk  2005-10-19 20:16:31  No: 18159

すいませんWriteTotalTimeoutMultiplierはタイムアウトに関しての設定なので
送信間隔には影響がないようです。^^;)
送信間隔をあけるにはソフトで1文字毎に処理してください。

>これは、WriteFileで勝手に書き込み(?)しても、Windows側で送信停止してくれるということでしょうか?
Xon/Xoffをして指定してあればドライバ側(Windows側)で自動で送信停止・再開します。
モデムではなくても通信ドライバは存在します。(標準ポートの場合は標準ドライバが使用されます)
なお通信レートが速くて相手の機器が遅い場合は複数バイトを連続で送ると相手の処理が追いつかない場合があります。
その様な時は前記のように1文字単位で分割して送ると効果があります。
ただし通信にUSBシリアル変換を使用している場合などは1文字毎に分割してもドライバ側である程度まとまるまで送信が開始されないのでFlushfilebuffers()を使用して強制書き込みする必要があります。

>自分で処理する場合
はWindowsのオープン処理でXon/Xoffのフロー制御なしと指定します
ソフトでは1文字毎に区切りながら送信してその間に受信データの処理も行います。
受信データのXoff(^S)/Xon(^Q)に応じて送信を制御します。
ただ簡単なようで結構手間のかかる処理です。
Xoff受信後Xonを取りこぼすと通信がハングしてしまいますのでタイムアウト等の処理を入れる必要があります。


おっ  2005-10-19 21:18:27  No: 18160

> >これは、WriteFileで勝手に書き込み(?)しても、Windows側で送信停止してくれるということでしょうか?
> Xon/Xoffをして指定してあればドライバ側(Windows側)で自動で送信停止・再開します。
> モデムではなくても通信ドライバは存在します。(標準ポートの場合は標準ドライバが使用されます)
うーむ・・・。
最初そうだと思って、WriteFileしまくっていたんですが(もちろんポートの設定も
機械の設定もフロー制御はXon/Xoffです)、機械側の動作を停止し、受信のみにの状態に
設定しても止まらずに送信し続けるんです。

送信前に、このように送信キューに空きがでるまで待つようにしています。
  repeat
    ClearCommError(FHandle, dwError, @Stat);
  until (FOutQueueSize - Stat.cbOutQue) >= Size;
Windows側で送信がストップしているなら、空きができないはずなので、
無限ループに陥るはずです。

そのため、送信キューの状態チェック前に、受信キューを調べて
Xoff/Xonの状態をチェックするようにしました。
これで機械側を待機状態にし送信すると、確かに止まるようになりました。

ん〜・・・矛盾してる・・・。
ますますわからないです。


kkk  2005-10-19 21:41:53  No: 18161

うーん
状況を考えるとPC側でXon/Xoff制御が有効になっていないようですね・・・
TCommをもっていないのであまり判断できませんが
SysinternalsのPortmonを使用してWindowsでのオープンをモニタしてみてください。
http://www.sysinternals.com/Utilities/Portmon.html
Xon/Xoffフロー制御付でオープンされているか確認できます。


おっ  2005-10-20 01:02:19  No: 18162

> SysinternalsのPortmonを使用してWindowsでのオープンをモニタしてみてください。
http://www.sysinternals.com/Utilities/Portmon.html
> Xon/Xoffフロー制御付でオープンされているか確認できます。
使ってみましたが、どれがXon/Xoffの設定なのかわからないような感じですけど。

行頭省略ですが、こんな感じでログを拾っています。

RP_MJ_CREATE                    Serial0 SUCCESS Options: Open   
OCTL_SERIAL_SET_QUEUE_SIZE      Serial0 SUCCESS InSize: 4096 OutSize: 4096      
OCTL_SERIAL_PURGE               Serial0 SUCCESS Purge: TXABORT RXABORT TXCLEAR RXCLEAR  
OCTL_SERIAL_SET_TIMEOUTS        Serial0 SUCCESS RI:-1 RM:0 RC:1000 WM:0 WC:1000 
OCTL_SERIAL_GET_BAUD_RATE       Serial0 SUCCESS         
OCTL_SERIAL_GET_LINE_CONTROL    Serial0 SUCCESS         
OCTL_SERIAL_GET_CHARS           Serial0 SUCCESS         
OCTL_SERIAL_GET_HANDFLOW        Serial0 SUCCESS         
OCTL_SERIAL_GET_BAUD_RATE       Serial0 SUCCESS         
OCTL_SERIAL_GET_LINE_CONTROL    Serial0 SUCCESS         
IOCTL_SERIAL_GET_CHARS          Serial0 SUCCESS         
IOCTL_SERIAL_GET_HANDFLOW       Serial0 SUCCESS         
IOCTL_SERIAL_SET_BAUD_RATE      Serial0 SUCCESS Rate: 9600      
IOCTL_SERIAL_SET_RTS            Serial0 SUCCESS         
IOCTL_SERIAL_SET_LINE_CONTROL   Serial0 SUCCESS StopBits: 1 Parity: NONE WordLength: 8  
IOCTL_SERIAL_SET_CHAR           Serial0 SUCCESS EOF:0 ERR:0 BRK:0 EVT:0 XON:11 XOFF:13  
IOCTL_SERIAL_SET_HANDFLOW       Serial0 SUCCESS Shake:12 Replace:40 XonLimit:2048 XoffLimit:512 
IOCTL_SERIAL_GET_BAUD_RATE      Serial0 SUCCESS         
IOCTL_SERIAL_GET_LINE_CONTROL   Serial0 SUCCESS         

    GetCommState(FHandle, FDCB);
    FDCB.BaudRate := 9600;
    SetCommState(FHandle, FDCB);
こんな感じで、設定を次々変更した分だけIOCTL_SERIAL_SET_〜が発生しているような感じです。

ソフト側で、Xon/Xoff対応に書き換えてみたのですが、希望の結果が得られません。
ちょっと思ったのが、送信タイミングが早すぎて、Xoffが返ってきたときにはすでに
バッファあふれ確定データを送信しちゃっているのかもしれません。
となると、1バイト毎に、Xoffが返ってくるかどうか待機しないといけないということに?


おっ  2005-10-20 01:24:24  No: 18163

> ソフト側で、Xon/Xoff対応に書き換えてみたのですが、希望の結果が得られません。
いろいろごちゃごちゃと書きすぎていたのが原因なのか、問題発生前のソースに戻し
必要な部分だけ修正し直ししたら、うまくいきました。

ん〜〜・・・納得いかない。
もしかして、偶然なのか???


kkk  2005-10-20 02:21:54  No: 18164

>IOCTL_SERIAL_SET_HANDFLOW       Serial0 SUCCESS Shake:12 Replace:40 XonLimit:2048 XoffLimit:512
ここあやしいですね
---
ntddrser.h の一部です
// Shakeのビット定義
#define SERIAL_DTR_MASK                   0x00000003
#define SERIAL_DTR_CONTROL                0x00000001
#define SERIAL_DTR_HANDSHAKE              0x00000002
#define SERIAL_CTS_HANDSHAKE              0x00000008
#define SERIAL_DSR_HANDSHAKE              0x00000010
#define SERIAL_DCD_HANDSHAKE              0x00000020
#define SERIAL_OUT_HANDSHAKEMASK          0x00000038
#define SERIAL_DSR_SENSITIVITY            0x00000040
#define SERIAL_ERROR_ABORT                0x80000000
#define SERIAL_CONTROL_INVALID            0x7fffff84
// Replaceのビット定義
#define SERIAL_AUTO_TRANSMIT              0x00000001
#define SERIAL_AUTO_RECEIVE               0x00000002
#define SERIAL_ERROR_CHAR                 0x00000004
#define SERIAL_NULL_STRIPPING             0x00000008
#define SERIAL_BREAK_CHAR                 0x00000010
#define SERIAL_RTS_MASK                   0x000000c0
#define SERIAL_RTS_CONTROL                0x00000040
#define SERIAL_RTS_HANDSHAKE              0x00000080
#define SERIAL_TRANSMIT_TOGGLE            0x000000c0
#define SERIAL_XOFF_CONTINUE              0x80000000
#define SERIAL_FLOW_INVALID               0x7fffff20
---
Shake:12はDTR/DSR制御
Replace:40は RTS Controlとなています
すなわちXon/Xoffの設定になっていません。
Xon/Xoffの時は Shake:xx1, Replace:xx03 かな?
DCBの fTXContinueOnXoff(0), fOutX(1),fInX(1) がちゃんとセットされていないかも


おっ  2005-10-20 18:24:38  No: 18165

kkkさん、何度もすみません。

ちなみに、ログは最初の一部しか掲載していませんでした。

(たぶん)最後の設定が終わったときの結果がこれです
IOCTL_SERIAL_SET_LINE_CONTROL   Serial0 SUCCESS StopBits: 1 Parity: NONE WordLength: 8  
IOCTL_SERIAL_SET_CHAR           Serial0 SUCCESS EOF:0 ERR:0 BRK:0 EVT:0 XON:11 XOFF:13  
IOCTL_SERIAL_SET_HANDFLOW       Serial0 SUCCESS Shake:12 Replace:43 XonLimit:1024 XoffLimit:1024        

> Replace:40は RTS Controlとなています
> すなわちXon/Xoffの設定になっていません。
> Xon/Xoffの時は Shake:xx1, Replace:xx03 かな?
この通り Replace:43 になっていますが、Shake:12 ですね。

もしかすると、あやしいのがフロー制御を設定している部分かもしれません。
実行部分を一部抜粋すると、以下のようになっています。
--------------------------------
GetCommState(FHandle, FDCB);
// fOutxCtsFlow, fOutxDsrFlow, fDtrControl, fOutX, fInX, fRtsControl をオフにする
FDCB.Flags := FDCB.Flags and $FEC0C003; 

FDCB.Flags := FDCB.Flags or dcb_Rts_Enable or dcb_Dtr_Enable
  dcb_OutX or dcb_InX;

FDCB.Flags := FDCB.Flags or $00000003;   // fBinary & fParity;
SetCommState(FHandle, FDCB);
----------------------------------

「〜をオフにする」がくさい感じです。
オフではなく、0にしてからフラグ更新するように代えると
Shake:12 Replace:43 → Shake:9 Replace:83 になりました。
どうもXon/Xoffが有効らしい設定ですが、これでいいんでしょうか?

これでテストしてみます。


おっ  2005-10-20 19:05:38  No: 18166

> Shake:12 Replace:43 → Shake:9 Replace:83 になりました。
これ、よくよく考えると、SERIAL_RTS_HANDSHAKE ですね。
Shake:9 はともかく、Replace:3 にならないといけないですね。
何がおかしいのでしょうか・・・。


おっ  2005-10-20 19:20:21  No: 18167

全然わかっていなかったようです。

> Xon/Xoffの時は Shake:xx1, Replace:xx03 かな?
これは、16進のANDではなく、文字通りShakeの一桁目が1で、
Replaceの下位2桁が03ということですね。


kkk  2005-10-20 19:27:43  No: 18168

>これは、16進のANDではなく、文字通りShakeの一桁目が1で、
>Replaceの下位2桁が03ということですね。
そうです。分かりづらくてすいません。
これはReplace:43でもOKです

>> Shake:12 Replace:43 → Shake:9 Replace:83 になりました。
>Shake:9 はともかく、Replace:3 にならないといけないですね。
Replaceは 83はまずそうですね  Replace:3か43(RTS=Enable)になる必要がありますよね。
それとShake:9もまずそうですよね。(CTS Handshake)
期待値は Shake:80000001かShake:1ですね(DTR=Enable)

ついでに・・・
DCB設定する前にGetCommState(FHandle, FDCB);
してますがこれはWindowsの設定を反映する時は必要ですがソフトですべて設定するのであれば特に必要はありません。


kkk  2005-10-20 19:39:04  No: 18169

訂正GetCommStateはフラグ以外の情報を取り込むのにも使いますので
やはりGetCommStateした後でフラグだけ再設定するのが良いのかな?


おっ  2005-10-20 22:20:41  No: 18170

結論(?)
まず一つ勘違いしていましたが、SERIAL_RTS_HANDSHAKE が入ってきていたのは、元々の設定のためでした。
個人的には、Xon/Xoffのみだと思いこんでいたのですが、よくよくデバックしてみると違っていたようです。

それで、Shake:1, Replace:43 になるようにプログラム変更しましたが、
やっぱり停止してくれませんでした。
Xon/XoffをWindowsの標準ポートでは処理してくれないとしか思えないです。
テストしていませんが、他のコンポーネントでもXon/Xoffに関する記述がなかったようなので、
標準ポートの標準ドライバ相手では、同じような結果になりそうな気がします。
(さすがに、いろいろなコンポーネントで試す気にはなれません)

モデム通信とかの用途だと専用ドライバがあるので、ドライバ側で
うまく処理してくれるため、問題がでなかったんでしょう・・・(推測)。
なんか、とってもがっかりです。


おっ  2005-10-21 00:49:32  No: 18171

いちおう解決
納得できませんが。。。


kkk  2005-10-21 01:26:28  No: 18172

>納得できませんが。。。
そうですね
Shake:1, Replace:43になれば問題ないと思いましたが・・・
Portmonでは送受信のデータそのものも確認できますので
気が向いたら相手機器からXoffが送られてきているかも確認してみてください。


おっ  2005-10-21 05:11:39  No: 18173

解決チェック入れましたが、ますますわけがわからなくなってきました。

一昨日から今日の昼間でのテストと今回のテストで、動作が違います。
なぜかはわからないのですが、WindowsがXon/Xoffを吸収して?送信が止まりました。

今現在の状況では・・・
http://www2.muroran-it.ac.jp/circle/yume/maincontents/serialport/#dcb
ここでいう
fOutX, fInX がTrueのとき、Windowsのポートで吸収されてしまい、ソフト側に届きません。
いわゆるXon/Xoffフロー制御を有効にする設定です。
また、PortmonにXon/Xoffの制御コードは届いた情報は表示されません。

逆に、fOutX, fInX がFalseのときは、アプリ側に届き、自らフロー制御を書かないといけません。
このときは、PortmonにXon/Xoffの制御コードが検出されました。

いろいろテストしているうちに、自ら怪しいコードを入れてしまったのかもしれません。
解決したと思いましたが、明日再テストの必要ありです。

fOutX, fInX がTrueのときにも、アプリまで届いたことがあったのですが、
プロパティのさわりかたが悪かったのか、勘違いだったのか。

Windowsの標準ポートも、設定次第では正しく動作するということかもしれません。
もう、うんざりです。


おっ  2005-10-21 22:59:04  No: 18174

[[結果報告]]

<< 結論 >>
「Windowsの標準ポートでもXon/Xoffのフロー制御は有効である」

<< 今回の謎の発生原因 >>
WriteFileで2バイト以上のデータをまとめて送信していたた。
送信エラーが発生していたが、(Portmon.exe では TIMEOUT 扱いになっている)
コンポーネントの考慮不足により、エラーが無視されデータを次々送信していった。

[推測]
受付側の機器が、許容範囲以上のデータを受信したが、処理できなかったためエラーになる。
このとき、受け取った全てのデータが切り捨てられるため、受付機器のバッファには
空きが残ったままになり、Xoff信号が送信されない。
エラーを検出せず、アプリ側が次々データを送信しするため、フロー制御が
有効ではないように見えた。

<<対応>>
以下のいずれかの対応で、回避可能と推測される。

(1)データの送信は、1バイトずつ行う

  1バイト送信することで、受付機器のバッファに空きがなくなると、
  即Xoffが発行され、Windowsのポートが送信を停止する。

  また、実際にWriteFileする前に、Windowsの送信バッファに空きができるまで
  待機することで、Xoffで送信停止しているバッファの溢れを防ぐことができる
  (TCommでは実装済み)。
  
  未検証ではあるが、他のCommコンポーネントの中には、必ず1バイトずつの
  送信を行うものもあるようだ。
  この対応をしているコンポーネントでは、発生しない。

(2)まとめて送った後に、実際に書き込みを行ったサイズをチェックする。

  WriteFileの引数の4番目で、実際の書き込み数を得ることができる。
  WriteFile(FHandle, Buffer, Size, dwBytesWritten, 〜

  ただし、TCommコンポーネントでは、オーバーラップ指定で開いているため、
  WriteFileでの常に0になるため、GetOverlappedResultの第3引数で取得することになる。
  GetOverlappedResult(FHandle, 〜, dwBytesWritten) do

  受付機器のバッファ溢れ等でエラーになった場合は、WriteFileで指定した
  データサイズと、書き込まれたサイズは一致しない。
  また、GetOverlappedResultの結果が True で返ってくるため、エラーと
  認識しない。GetOverlappedResultの結果が True であっても、書き込みサイズと
  書き込まれたサイズをチェックする必要がある。(TCommでは未対応)
  

紆余曲折を経て導き出された回答は、コンポーネントの不具合(考慮不足)及び、
使用者(私)の知識不足にありました。

kkkさん、いろいろお手数おかけしまして、すいませんでした。


kkk  2005-10-21 23:44:23  No: 18175

通信関連はノウハウものが多いですので今後他の方の参考にもなる思います。
お疲れ様でした。


おっ  2005-10-24 18:52:02  No: 18176

[追加報告]

送信途中のキャンセルができるようにということで、送信処理をThread化したところ、
送信データの一部に、再び誤りが生じました。

ということで、先日の対応策は、やはり不十分のようです。
全エラーの可能性を確実につぶしておくのが、安全のようですね。

・送信(WriteFile)は1バイトずつ行う(相手機器によっては不要かも)
・WriteFile前に、送信バッファに空きができるまで待機する
・WriteFile後、WriteFile,GetOverlappedResult等で書き込みできたサイズを確認し
  未送信データがあれば、再送する


※返信する前に利用規約をご確認ください。

※Google reCAPTCHA認証からCloudflare Turnstile認証へ変更しました。






  このエントリーをはてなブックマークに追加