Qikuzou
@Qikuzou

Are you sure you want to delete the question?

Leaving a resolved question undeleted may help others!

System.IO.Ports.SerialPortのRead()とWrite()に時間がかかることがある

解決したいこと

PCの仮想COMポートを使用して外部機器とバイナリ通信を行っているが、ランダムで仮想COMポートへの読み書きに時間がかかることがある。

PC環境
・OS:Windows 10 64bit
・開発環境:Visual Studio Community 2022
・言語:C#
・プロジェクトの種類:Windowsフォームアプリ
・ターゲットフレームワーク:.NET 7.0
・通信パッケージ:System.IO.Ports (7.0.0)
・通信フロー制御:なし

外部機器
・USB UART変換IC:Silicon Labs CP2104

発生している問題・エラー

・System.IO.Ports.SerialPortのRead()とWrite()に時間がかかることがある。
・通常は10msもかからないが、時間がかかるときは必ず約500ms、もしくは約1000msのどちらか。
・System.IO.Ports.SerialPortのReadTimeoutとWriteTimeoutには100msを設定している。
・メインPCより性能の劣るサブPC(Windows 11)だと発生頻度が増える。

該当するソースコード(抜粋)

void Send(SerialPort serialPort, byte[] bytes)
{
  var stopwatch = Stopwatch.StartNew();
  serialPort.Write(bytes, 0, bytes.Length);
  stopwatch.Stop();
  if (stopwatch.ElapsedMilliseconds > 10)
  {
    Debug.WriteLine($"★☆★☆ WriteTime = {stopwatch.ElapsedMilliseconds} ms");
  }
}

private void DataReceivedEvent(object sender, SerialDataReceivedEventArgs e)
{
  SerialPort serialPort = (SerialPort)sender;
  var length = serialPort.BytesToRead;
  var bytes = new byte[length];

  var stopwatch = Stopwatch.StartNew();
  serialPort.Read(bytes, 0, bytes.Length);
  stopwatch.Stop();
  if (stopwatch.ElapsedMilliseconds > 10)
  {
    Debug.WriteLine($"★☆★☆ ReadTime = {stopwatch.ElapsedMilliseconds} ms");
  }
}

自分で試したこと

・Silicon Labs CP2104のドライバを最新に。
・デバイスマネージャのCOMポートの詳細設定で「FIFOバッファーを使用する」の設定をいろいろ変更。
・System.IO.Ports.SerialPortのRead()とWrite()をlockで囲ってみる。
・System.IO.Ports.SerialPortのRead()とWrite()の前後にTask.Delay()を入れてみる。
・System.IO.Ports.SerialPortのReadTimeoutとWriteTimeoutの設定値を大きくしたり小さくしたり。

追記

PC環境を以下のように変更し、ソースコードもエラーが出る部分は修正して試したところ、Read()とWrite()の処理時間は通常0~20ms、長くても93msに収まっていました。

【変更前】
・プロジェクトの種類:Windowsフォームアプリ
・ターゲットフレームワーク:.NET 7.0

【変更後】
プロジェクトの種類:Windowsフォームアプリケーション(.NET Framework)
ターゲットフレームワーク:.NET Framework 4.8

追記2

ツールを使ってCPUに高負荷をかけると高性能のメインPCでも発生頻度が激増する。ただしサブPCと違って時間は500ms or 1000msではなくランダム。

13:00:55.719 ReadTime = 190 ms
13:00:56.298 ReadTime = 171 ms
13:00:57.287 WriteTime = 484 ms
13:00:58.224 ReadTime = 585 ms
13:00:59.183 WriteTime = 384 ms
13:01:00.413 ReadTime = 724 ms
13:01:01.443 WriteTime = 708 ms
13:01:02.119 ReadTime = 475 ms
13:01:03.302 WriteTime = 965 ms
13:01:04.811 ReadTime = 1116 ms
13:01:07.519 WriteTime = 1523 ms
13:01:10.013 ReadTime = 781 ms
13:01:10.889 WriteTime = 386 ms

「追記」で作成した.NET Framework版に高負荷をかけると以下のようになる。

12:58:31.162 ReadTime = 45 ms
12:58:31.688 ReadTime = 27 ms
12:58:32.214 WriteTime = 47 ms
12:58:32.740 ReadTime = 17 ms
12:58:33.324 ReadTime = 73 ms
12:58:33.845 ReadTime = 22 ms
12:58:34.421 ReadTime = 11 ms
12:58:34.970 ReadTime = 26 ms
12:58:35.798 WriteTime = 56 ms
12:58:37.658 ReadTime = 118 ms
12:58:38.474 WriteTime = 15 ms
12:58:39.678 ReadTime = 96 ms
12:58:40.864 WriteTime = 95 ms

Microsoft製ツール(CpuStres v2.0)
https://learn.microsoft.com/ja-jp/sysinternals/downloads/cpustres

1

5Answer

呼び出し頻度や送受信するデータの量が判りませんが、少なくても発生しますか?
別段プログラム側で特殊な処理をやってる訳じゃないなら、
接続条件を変える、接続ポートを変える、最悪ハードを変えてみるしかなさそうな気がします。

0Like

Comments

  1. @Qikuzou

    Questioner

    返信ありがとうございます。

    呼び出し頻度や送受信するデータの量

    外部機器から1秒毎に25バイトのデータが送られてきて、それに3バイトのレスポンスを返しているだけです。

    接続条件を変える

    通信速度などのことでしょうか?
    外部機器は既存製品なので変更することは難しいです。

    接続ポートを変える

    接続ポートというのはPCのUSBポートのことでしょうか?
    別のUSBポートに挿し替えても現象はかわりませんでした。

    最悪ハードを変えてみる

    PC側はデスクトップPCとノートPCで試しましたが、発生頻度の違いはあれ両方ともに発生します。
    外部機器側はすぐに別のものを手配するのは厳しいです。不可能ではありませんが…

  2. いずれにせよ、ライブラリの内部的な動作としては、
    Open→WindowsAPIのCreateFile
    Read,Write→WindowsAPIのReadFile,WriteFile
    を呼び出しているだけですので、ほぼC#の外の話になってしまうんですよね。
    変えれるのは、せいぜいタイムアウトやBaudRate・Parity・DataBits諸々の設定程度です。

  3. 追記を見ましたが、フレームワークのバージョンの挙動で差が出ているなら、
    ・プロジェクトを.NET Framework 4.8に変更する
    ・シリアルポート処理の部分だけ.NET Framework 4.8で作ってそれを呼び出す
    ・自分で直接WinAPIのCreateFile、ReadFile、WriteFileを呼ぶ
    のいずれかしかなさそうな気はします。

    ただ、公式のソースを見てもWinAPIを呼ぶ以外の処理は殆ど大した事はしていないので、挙動に差が出ている事には疑問を感じますが。

    こちらは、.NET Frameworkのソース

    こちらは、現在の.NET Runtimeのソース

    そこまで差はありません。

    APIで自力で頑張るなら、このへん参考にするといいかも。
    それで同じ現象が発生するなら、フレームワークのバージョンの線もなんか怪しい気がします。

先の回答者様と同様に、私も切り分けから入ると思います(それで解決するとは限りませんが・・・)。

思い付きでは:

  • 100msecのタイムアウト値を極端に長くしてみる。例えば30000msecとか。
  • 別の仮想COMデバイスを使ってみる。
0Like

Comments

  1. @Qikuzou

    Questioner

    返信ありがとうございます。

    100msecのタイムアウト値を極端に長くしてみる。例えば30000msecとか。

    Read/Write両方とも30000msに変更しても現象はかわりませんでした。

    別の仮想COMデバイスを使ってみる。

    外部機器のUSB UART変換ICのことでしょうか?
    外部機器は既存製品なので変更することは難しいです。

  2. ①対抗機器 <---> ②USBシリアル変換アダプタ(Silicon Labs CP2104内蔵) <---> ③Windows PC

    って構成だと思っていたのですが、勘違いですかね?そもそも、①と②が合体しちゃっている?

    (いずれにせよ、不具合がどこに付いて回るのか、カット&トライしていくしかないと思いますが)

  3. @Qikuzou

    Questioner

    ①と②が合体しちゃっている?

    はい。Silicon Labs CP2104は対抗機器に内蔵されており、対抗機器とWindowsPCはUSBケーブルで直接接続されています。

  4. なるほど、外部機器のベンダに問い合わせってできないんです?

  5. @Qikuzou

    Questioner

    一番上の文章に追記しましたが、以下のように変更すると現象が発生しなくなるので外部機器側が原因の可能性は非常に低いと考えております。

    【変更前】
    ・プロジェクトの種類:Windowsフォームアプリ
    ・ターゲットフレームワーク:.NET 7.0

    【変更後】
    プロジェクトの種類:Windowsフォームアプリケーション(.NET Framework)
    ターゲットフレームワーク:.NET Framework 4.8

  6. なるほど、フレームワークのバージョンで挙動に差が出るのですね。半歩前進といったところでしょうか。。。

    真因を追うかによって取るべき手段も変わってきましょうが、私であれば、短期的には @radian-jp さんが書かれているような対応を取ると思います。

    いずれにせよ、有益な情報(人柱?)をありがとうございます。

1秒毎に25バイトのデータが送られてきて、それに3バイト

最大バイトの25*1.1ならバッファー値はデフォルトで十分です。
 flushするタイミングはバッファーが満タンか?無通信のタイムアウトです。

serialPort.Write
System.IO.Ports.SerialPort.Flush() System.IO.Ports.SerialPort.Finalize()

serialPort.Write
serialPort.Finalize() 

仮説:
 バッファーサイズが大きいと受信バッファーが満タンになるまで、プログラムへはデータを渡さない条件に陥ったと考えられます。

対処:
  機器の送信タイミングで適切な
  Flush()を行う。

0Like

Comments

  1. @Qikuzou

    Questioner

    返信ありがとうございます。

    SerialPortのメソッドにFlushが無かったのでググったところSerialPort.BaseStream.Flushがあったのでこちらを追加してみましたが現象はかわりませんでした。

    serialPort.Write(bytes, 0, bytes.Length);
    serialPort.BaseStream.Flush();
    
  2. write,flushの後、readの前に100~150ms位スリープをいれて機器側の応答時間分待ってはどうでしょうか?

    臼の餅つきと同じで尽き手と返し手の息を合わせる必要があります。特に低レベルな通信プロトコルは対向機器のタイミングに合わせるのが基本です。

全く知見のない分野なので的外れだったら大変申し訳ありませんが、Windows Formプロジェクトで発生しているとのことなのでコンソールアプリケーションだとどうなるか試すことは可能でしょうか?
メインスレッドとのデッドロックなどが発生していないか?を気にしています。
あるいは当該処理をTask.Runで囲ってawaitしてみるとか…

0Like

Comments

  1. @Qikuzou

    Questioner

    スパムの疑いがありますと表示されてしまってうまく詳細が返信できないのですが、プロジェクトに関しては「Windowsフォームアプリ」から「Windowsフォームアプリケーション(.NET Framework)」に変更すると現象が発生しなくなります。
    Task.RunはRead/Writeどちらに(両方に)追加しても現象は変わらずでした。

何の解決にもならないですがReadメソッドを実行しているのはDataReceivedイベントハンドラ内なので取り出そうとしているデータは受信を完了しているものだけのはずで、少なくとも受信については外部機器が原因ではなさそうです。
内部の受信バッファから内容をコピーするくらいしかしてなさそうなReadメソッドなのに、なぜそんなに待たされるのかがわからないですが…

0Like

Your answer might help someone💌