C Sharpの基礎 - シリアル通信

提供:MochiuWiki : SUSE, EC, PCB
ナビゲーションに移動 検索に移動

概要

シリアル通信は、データを1ビットずつ順番に送受信する通信方式である。
C#では、System.IO.Ports名前空間のSerialPortクラスを使用してシリアル通信を実装することができる。

まず、SerialPortクラスのインスタンスを生成して、ポート名、ボーレート、データビット、ストップビット、パリティ等の通信パラメータを設定する。
これらのパラメータは、通信相手のデバイスと一致させる必要がある。

通信を開始するには、SerialPort.Openメソッドを実行する。

データを送信する場合は、WriteメソッドやWriteLineメソッドを実行する。

データを受信する場合は、同期的な方法と非同期的な方法がある。

  • 同期的な方法
    ReadLineメソッドやReadメソッドを使用してデータを受信する。
    これらのメソッドは、データが到着するまでプログラムの実行をブロックすることに注意する。

  • 非同期的な方法
    DataReceivedイベントを使用する。
    このイベントは、データが受信された時に発生し、イベントハンドラ内でデータを処理する。
    これにより、UIの応答性を維持しながらデータを受信することができる。


通信が完了した後は、必ずCloseメソッドを実行してポートを閉じる。

エラーハンドリングでは、タイムアウトの設定、例外処理、リソースの適切な解放等を考慮する必要がある。

シリアル通信は、組み込みシステム、産業用機器、DAQ等の様々な分野で利用されている。
C#の豊富なライブラリとイベント駆動型のプログラミングモデルにより、効率的なシリアル通信アプリケーションの開発が可能である。

実務では、デバッグツールやシリアルモニターを活用して、通信の動作を確認することも重要である。


フロー制御

ソフトウェアフロー制御 (XON / XOFF) およびハードウェアフロー制御 (RTS / CTS) は、シリアル通信におけるデータの流れを管理するための重要な方法である。
これらの制御方式は、送信側と受信側のデバイス間でデータの転送速度を調整して、データの損失を防ぐために使用される。

これらの制御方式の選択は、通信の速度、信頼性の要求、ハードウェアの制約、コスト等の要因によって決まる。

高速で信頼性の高い通信が必要な場合は、ハードウェアフロー制御が適している。
一方、既存のシステムでの簡単な実装やコスト削減が重要な場合は、ソフトウェアフロー制御が選択されることがある。

応用例として、モデム通信やプリンタとコンピュータ間の通信等でこれらのフロー制御方式が使用されている。
また、産業用機器や医療機器等、データの正確さが極めて重要な分野でも、これらのフロー制御方式が重要な役割を果たしている。

フロー制御の選択と実装は、システムの要件や制約を十分に考慮して行う必要がある。
適切なフロー制御を使用することにより、データの損失を防ぎ、通信の信頼性を向上させることができる。

ソフトウェアフロー制御

ソフトウェアフロー制御 (XON / XOFF) は、特別な制御文字を使用してデータの流れを制御する。
XON (伝送再開) と XOFF (伝送停止) という2つの制御文字が使用される。

受信側のバッファがほぼ一杯になる時、XOFF信号を送信して送信側にデータの送信を一時停止するよう指示する。
バッファに余裕ができた時、XON信号を送って送信再開を伝える。

この方法は追加のハードウェアを必要としないため、コスト効率が良く、既存のシステムに容易に実装できるというメリットがある。

しかし、バイナリデータを送信する場合、制御文字と実際のデータが混同される可能性があるため注意が必要である。

ハードウェアフロー制御

ハードウェアフロー制御 (RTS / CTS) は、追加の信号線を使用してデータの流れを制御する。
RTS (送信要求) と CTS (送信可) という2つの信号が使用される。

送信側デバイスは、RTS信号を送信して、データを送信する準備ができたことを伝える。
受信側デバイスは、データを受信する準備ができている時にCTS信号を送り返す。

この方法は追加のハードウェア (信号線) を必要とするが、ソフトウェアフロー制御よりも信頼性が高く、高速な通信に適している。

また、バイナリデータの送信時にも問題が生じにくいというメリットがある。


同期通信

送信

以下に示すパラメータは、通信相手のデバイスの設定と一致している必要がある。
使用時では、接続するデバイスの仕様に合わせてこれらの値を適切に調整すること。

  • パリティ
    データの整合性チェックのために使用する。
    以下の例では、Parity.Noneに設定している。
    必要に応じて、Parity.EvenParity.OddParity.MarkParity.Spaceを指定する。
  • データビット
    各バイトのビット数を指定する。
    一般的に8ビットが使用されるが、7ビットや5ビット等も可能である。
  • ストップビット
    各バイトの終わりを示すビットを指定する。
    必要に応じて、StopBits.OneStopBits.OnePointFiveStopBits.Twoを指定する。
  • ハンドシェイク
    フロー制御の方法を指定する。
    ハードウェアフロー制御 (RTS / CTS)
    ソフトウェアフロー制御 (XON / XOFF)

    以下の例では、Handshake.Noneに設定している。
    必要に応じて、Handshake.RequestToSendHandshake.XOnXOff等を指定する。
  • タイムアウト
    読み取りと書き込みのタイムアウトを設定している。
    これにより、操作が無限に待機することを防ぐことができる。


 using System;
 using System.IO.Ports;
 
 class Sender
 {
    static void Main(string[] args)
    {
       string portName = "/dev/ttyS0";    // ポート名を適切に設定
       int baudRate    = 9600;            // ボーレート 9600[bps]
       Parity parity   = Parity.None;     // パリティ無し
       int dataBits    = 8;               // データ長は8ビット
       StopBits stopBits = StopBits.One;  // ストップビットは1ビット
 
       try
       {
          using (SerialPort serialPort = new SerialPort(portName, baudRate, parity, dataBits, stopBits))
          {
             serialPort.Handshake    = Handshake.None;  // フロー制御の設定
             serialPort.ReadTimeout  = 500;             // 読み取りタイムアウトの設定 (ミリ秒)
             serialPort.WriteTimeout = 500;             // 書き込みタイムアウトの設定 (ミリ秒)
 
             serialPort.Open();
             Console.WriteLine("シリアルポートをオープン")
             Console.WriteLine("終了するには 'exit' と入力");
 
             while (true)
             {
                Console.Write("送信するメッセージを入力: ");
                string message = Console.ReadLine();
 
                if (message.ToLower() == "exit") break;
 
                serialPort.WriteLine(message);
                Console.WriteLine("メッセージが送信された");
             }
 
             serialPort.Close();
          }
       }
       catch (TimeoutException)
       {
          Console.WriteLine("送信操作がタイムアウト");
       }
       catch (Exception ex)
       {
          Console.WriteLine($"エラーが発生: {ex.Message}");
       }
    }
 }


受信

以下の例では、シリアル通信を同期処理で受信している。

 using System;
 using System.IO.Ports;
 
 class Receiver
 {
    static void Main(string[] args)
    {
       string portName = "/dev/ttyS0";    // ポート名を適切に設定
       int baudRate    = 9600;            // ボーレート 9600[bps]
       Parity parity   = Parity.None;     // パリティ無し
       int dataBits    = 8;               // データ長は8ビット
       StopBits stopBits = StopBits.One;  // ストップビットは1ビット
 
       try
       {
          using (SerialPort serialPort = new SerialPort(portName, baudRate, parity, dataBits, stopBits))
          {
             serialPort.Handshake    = Handshake.None;  // フロー制御の設定
             serialPort.ReadTimeout  = 500;             // 読み取りタイムアウトの設定 (ミリ秒)
             serialPort.WriteTimeout = 500;             // 書き込みタイムアウトの設定 (ミリ秒)
 
             serialPort.Open();
             Console.WriteLine("シリアルポートをオープン")
             Console.WriteLine("終了するには [Ctrl] + [C]キーを押下");
 
             serialPort.DataReceived += new SerialDataReceivedEventHandler(DataReceivedHandler);
 
             // プログラムを実行し続けるためのループ
             while (true)
             {
                System.Threading.Thread.Sleep(100);
             }
          }
       }
       catch (TimeoutException)
       {
          Console.WriteLine("受信操作がタイムアウト");
       }
       catch (Exception ex)
       {
          Console.WriteLine($"エラーが発生: {ex.Message}");
       }
    }
 
    private static void DataReceivedHandler(object sender, SerialDataReceivedEventArgs e)
    {
       SerialPort sp = (SerialPort)sender;
 
       try
       {
          string indata = sp.ReadExisting();
          Console.WriteLine("受信したデータ: " + indata);
       }
       catch (TimeoutException)
       {
          Console.WriteLine("データの読み取り中にタイムアウトが発生");
       }
    }
 }



非同期通信

送信

以下の例では、シリアル通信を非同期処理で送信している。

 using System;
 using System.IO.Ports;
 using System.Threading.Tasks;
 
 class AsyncSender
 {
    static async Task Main(string[] args)
    {
       string portName = "/dev/ttyS0";    // ポート名を適切に設定
       int baudRate    = 9600;            // ボーレート 9600[bps]
       Parity parity   = Parity.None;     // パリティ無し
       int dataBits    = 8;               // データ長は8ビット
       StopBits stopBits = StopBits.One;  // ストップビットは1ビット
 
       try
       {
          using (SerialPort serialPort = new SerialPort(portName, baudRate, parity, dataBits, stopBits))
          {
             serialPort.Handshake    = Handshake.None;  // フロー制御の設定
             serialPort.ReadTimeout  = 500;             // 読み取りタイムアウトの設定 (ミリ秒)
             serialPort.WriteTimeout = 500;             // 書き込みタイムアウトの設定 (ミリ秒)
 
             serialPort.Open();
             Console.WriteLine("シリアルポートをオープン");
             Console.WriteLine("終了するには 'exit' と入力");
 
             while (true)
             {
                Console.Write("送信するメッセージを入力: ");
                string message = Console.ReadLine();
 
                if (message.ToLower() == "exit") break;
 
                byte[] buffer = System.Text.Encoding.UTF8.GetBytes(message + Environment.NewLine);
                await serialPort.BaseStream.WriteAsync(buffer, 0, buffer.Length);
 
                Console.WriteLine("メッセージが非同期で送信完了");
             }
 
             serialPort.Close();
          }
       }
       catch (Exception ex)
       {
          Console.WriteLine($"エラーが発生: {ex.Message}");
       }
    }
 }


受信

以下の例では、シリアル通信を非同期処理で受信している。

 using System;
 using System.IO.Ports;
 using System.Threading;
 using System.Threading.Tasks;
 
 class AsyncReceiver
 {
    static async Task Main(string[] args)
    {
       string portName = "/dev/ttyS0";    // ポート名を適切に設定
       int baudRate    = 9600;            // ボーレート 9600[bps]
       Parity parity   = Parity.None;     // パリティ無し
       int dataBits    = 8;               // データ長は8ビット
       StopBits stopBits = StopBits.One;  // ストップビットは1ビット
 
       try
       {
          using (SerialPort serialPort = new SerialPort(portName, baudRate, parity, dataBits, stopBits))
          {
             serialPort.Handshake    = Handshake.None;  // フロー制御の設定
             serialPort.ReadTimeout  = 500;             // 読み取りタイムアウトの設定 (ミリ秒)
             serialPort.WriteTimeout = 500;             // 書き込みタイムアウトの設定 (ミリ秒)
 
             serialPort.Open();
             Console.WriteLine("シリアルポートをオープン");
             Console.WriteLine("終了するには [Ctrl] + [C]キーを押下");
 
             using (var cts = new CancellationTokenSource())
             {
                Console.CancelKeyPress += (s, e) =>
                {
                   e.Cancel = true;
                   cts.Cancel();
                };
 
                await ReceiveDataAsync(serialPort, cts.Token);
             }
 
             serialPort.Close();
          }
       }
       catch (OperationCanceledException)
       {
          Console.WriteLine("エラー: 受信が中断");
       }
       catch (Exception ex)
       {
          Console.WriteLine($"エラーが発生: {ex.Message}");
       }
    }
 
    private static async Task ReceiveDataAsync(SerialPort serialPort, CancellationToken cancellationToken)
    {
       byte[] buffer = new byte[1024];
       while (!cancellationToken.IsCancellationRequested)
       {
          try
          {
             int bytesRead = await serialPort.BaseStream.ReadAsync(buffer, 0, buffer.Length, cancellationToken);
             if (bytesRead > 0)
             {
                string receivedData = System.Text.Encoding.UTF8.GetString(buffer, 0, bytesRead);
                Console.WriteLine($"受信したデータ: {receivedData.Trim()}");
             }
          }
          catch (OperationCanceledException)
          {
             throw;
          }
          catch (Exception ex)
          {
             Console.WriteLine($"受信中にエラーが発生: {ex.Message}");
          }
       }
    }
 }



その他のシリアル通信の機能

受信

以下の例では、バッファリング、再接続機能、イベントベースの受信を行っている。
これにより、大量のデータを扱う場合や不安定な接続環境での使用に適している。

  • バッファリング
    ConcurrentQueue<byte[]>を使用して、受信データをバッファリングする。
    また、バッファの最大サイズを制限して、オーバーフローを防ぐ。
  • 再接続機能
    接続が失敗した場合に複数回試行する。
    また、接続状態を監視して、切断された場合に再接続を試みる。
  • イベントベースの受信
    SerialPort_DataReceivedイベントハンドラを使用して、データ受信時の処理を行う。
  • 非同期処理
    バッファリングされたデータを非同期に処理する。


 using System;
 using System.Text;
 using System.IO.Ports;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Collections.Concurrent;
 
 class AdvancedAsyncReceiver
 {
    private static SerialPort _serialPort;
    private static ConcurrentQueue<byte[]> _dataBuffer = new ConcurrentQueue<byte[]>();
    private static int _maxBufferSize     = 10;    // 受信バッファの最大サイズ
    private static int _reconnectAttempts = 5;     // 再接続の試行回数
    private static int _reconnectDelay    = 5000;  // 再接続の待機時間 (ミリ秒)
 
    static async Task Main(string[] args)
    {
       string portName   = "/dev/ttyS0";  // ポート名を適切に設定
       int baudRate      = 9600;          // ボーレート 9600[bps]
       Parity parity     = Parity.None;   // パリティ無し
       int dataBits      = 8;             // データ長は8ビット
       StopBits stopBits = StopBits.One;  // ストップビットは1ビット
 
       try
       {
          _serialPort = new SerialPort(portName, baudRate, parity, dataBits, stopBits);
          _serialPort.Handshake = Handshake.None;  // フロー制御の設定
          _serialPort.ReadTimeout = 500;           // 読み取りタイムアウトの設定 (ミリ秒)
          _serialPort.WriteTimeout = 500;          // 書き込みタイムアウトの設定 (ミリ秒)
 
          _serialPort.DataReceived += SerialPort_DataReceived; // イベントハンドラの登録
 
          await ConnectWithRetry();
 
          Console.WriteLine("シリアルポートをオープン");
          Console.WriteLine("終了するには [Ctrl] + [C]キーを押下");
 
          using (var cts = new CancellationTokenSource())
          {
             Console.CancelKeyPress += (s, e) =>
             {
                e.Cancel = true;
                cts.Cancel();
             };
 
             await Task.WhenAll(
                      ProcessBufferedDataAsync(cts.Token),
                      MonitorConnectionAsync(cts.Token)
             );
          }
       }
       catch (OperationCanceledException)
       {
          Console.WriteLine("エラー: 受信が中断");
       }
       catch (Exception ex)
       {
          Console.WriteLine($"エラーが発生: {ex.Message}");
       }
       finally
       {
          _serialPort?.Close();
       }
    }
 
    private static async Task ConnectWithRetry()
    {
       for (int i = 0; i < _reconnectAttempts; i++)
       {
          try
          {
             _serialPort.Open();
             return;
          }
          catch (Exception ex)
          {
             Console.WriteLine($"接続試行 {i + 1} 失敗: {ex.Message}");
             if (i < _reconnectAttempts - 1)
             {
                await Task.Delay(_reconnectDelay);
             }
          }
       }
       throw new Exception("接続に失敗");
    }
 
    private static void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
    {
       int bytesToRead = _serialPort.BytesToRead;
       byte[] buffer = new byte[bytesToRead];
       _serialPort.Read(buffer, 0, bytesToRead);
 
       if (_dataBuffer.Count < _maxBufferSize)
       {
          _dataBuffer.Enqueue(buffer);
       }
       else
       {
          Console.WriteLine("警告: バッファオーバーフロー");
          Console.WriteLine("データが破棄されました");
       }
    }
 
    private static async Task ProcessBufferedDataAsync(CancellationToken cancellationToken)
    {
       while (!cancellationToken.IsCancellationRequested)
       {
          if (_dataBuffer.TryDequeue(out byte[] data))
          {
             string receivedData = Encoding.UTF8.GetString(data);
             Console.WriteLine($"受信したデータ: {receivedData.Trim()}");
          }
          else
          {
             await Task.Delay(100, cancellationToken);  // バッファが空の場合は、100[mS]待機
          }
       }
    }
 
    private static async Task MonitorConnectionAsync(CancellationToken cancellationToken)
    {
       while (!cancellationToken.IsCancellationRequested)
       {
          if (!_serialPort.IsOpen)
          {
             Console.WriteLine("接続が切断されたため再接続を試行...");
             await ConnectWithRetry();
             Console.WriteLine("再接続に成功");
          }
 
          await Task.Delay(1000, cancellationToken);  // 1[秒]ごとに接続状態を確認
       }
    }
 }


送信