C Sharpの基礎 - シリアル通信
概要
シリアル通信は、データを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.Even
、Parity.Odd
、Parity.Mark
、Parity.Space
を指定する。
- データビット
- 各バイトのビット数を指定する。
- 一般的に8ビットが使用されるが、7ビットや5ビット等も可能である。
- ストップビット
- 各バイトの終わりを示すビットを指定する。
- 必要に応じて、
StopBits.One
、StopBits.OnePointFive
、StopBits.Two
を指定する。
- ハンドシェイク
- フロー制御の方法を指定する。
- ハードウェアフロー制御 (RTS / CTS)
- ソフトウェアフロー制御 (XON / XOFF)
- 以下の例では、
Handshake.None
に設定している。 - 必要に応じて、
Handshake.RequestToSend
、Handshake.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}");
}
}
}
}
タイマベースの送信
定期的にデータを送信する必要がある場合、タイマイベントを使用してデータを送信する。
定期的なデータ送信が必要なアプリケーション、例えば、環境モニタリングシステムやIoTデバイス等に適している。
タイマベースの送信により、DAQ等の一定間隔でのデータ収集と送信を自動化することができる。
using System;
using System.IO.Ports;
using System.Threading;
using System.Threading.Tasks;
using System.Timers;
class TimerBasedAsyncSender
{
private static SerialPort _serialPort;
private static System.Timers.Timer _timer;
private static Random _random = new Random();
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)
{
Handshake = Handshake.None, // フロー制御の設定
ReadTimeout = 500, // 読み取りタイムアウトの設定 (ミリ秒)
WriteTimeout = 500 // 書き込みタイムアウトの設定 (ミリ秒)
};
_serialPort.Open();
Console.WriteLine("シリアルポートをオープン");
Console.WriteLine("10秒ごとにセンサーデータを送信");
Console.WriteLine("終了するには 'exit' を押下");
_timer = new System.Timers.Timer(10000); // 10秒ごとに実行
_timer.Elapsed += TimerElapsed; // タイマイベントハンドラの設定
_timer.Start(); // タイマの開始
while (true)
{
string input = Console.ReadLine();
if (input?.ToLower() == "exit") break;
}
}
catch (Exception ex)
{
Console.WriteLine($"エラーが発生: {ex.Message}");
}
finally
{
_timer?.Stop();
_timer?.Dispose();
_serialPort?.Close();
Console.WriteLine("プログラムの終了");
}
}
private static async void TimerElapsed(object sender, ElapsedEventArgs e)
{
try
{
string sensorData = GenerateSensorData();
byte[] buffer = System.Text.Encoding.UTF8.GetBytes(sensorData + Environment.NewLine);
await _serialPort.BaseStream.WriteAsync(buffer, 0, buffer.Length);
Console.WriteLine($"送信したデータ: {sensorData}");
}
catch (Exception ex)
{
Console.WriteLine($"データ送信中にエラーが発生: {ex.Message}");
}
}
private static string GenerateSensorData()
{
// センサデータのシミュレーション
double temperature = Math.Round(_random.NextDouble() * 30 + 10, 2); // 10℃から40℃
double humidity = Math.Round(_random.NextDouble() * 60 + 20, 2); // 20%から80%
return $"温度: {temperature}℃, 湿度: {humidity}%";
}
}
その他のシリアル通信の機能
送信
以下の例では、バッファリング、再接続機能を使用して、非同期でデータを送信している。
これにより、大量のデータを扱う場合や不安定な接続環境での使用に適している。
- バッファリング
- ConcurrentQueue<string>を使用して、送信データをバッファリングする。
- また、バッファの最大サイズを制限して、オーバーフローを防ぐ。
- 再接続機能
- 接続が失敗した場合に複数回試行する。
- また、接続状態を監視して、切断された場合に再接続を試みる。
- 非同期処理
- バッファリングされたデータを非同期に送信する。
バッファリング機能により、一時的な接続問題や送信の遅延がある場合でもデータ損失のリスクを軽減する。
using System;
using System.Text;
using System.IO.Ports;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Concurrent;
class AdvancedAsyncSender
{
private static SerialPort _serialPort;
private static ConcurrentQueue<string> _sendBuffer = new ConcurrentQueue<string>();
private static int _maxBufferSize = 100; // 送信バッファの最大サイズ
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; // 書き込みタイムアウトの設定 (ミリ秒)
await ConnectWithRetry();
Console.WriteLine("シリアルポートをオープン");
Console.WriteLine("終了するには 'exit' と入力");
using (var cts = new CancellationTokenSource())
{
var sendTask = SendDataAsync(cts.Token);
var monitorTask = MonitorConnectionAsync(cts.Token);
while (true)
{
Console.Write("送信するメッセージを入力: ");
string message = Console.ReadLine();
if (message.ToLower() == "exit")
{
cts.Cancel();
break;
}
if (_sendBuffer.Count < _maxBufferSize)
{
_sendBuffer.Enqueue(message);
}
else
{
Console.WriteLine("警告: 送信バッファのオーバーフローが発生 (データを破棄)");
}
}
await Task.WhenAll(sendTask, monitorTask);
}
}
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 async Task SendDataAsync(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
if (_sendBuffer.TryDequeue(out string message))
{
try
{
byte[] buffer = Encoding.UTF8.GetBytes(message + Environment.NewLine);
await _serialPort.BaseStream.WriteAsync(buffer, 0, buffer.Length, cancellationToken);
await _serialPort.BaseStream.FlushAsync(cancellationToken);
Console.WriteLine("メッセージが非同期で送信完了");
}
catch (Exception ex)
{
Console.WriteLine($"送信中にエラーが発生: {ex.Message}");
_sendBuffer.Enqueue(message); // 送信失敗したメッセージを再度キューに追加
}
}
else
{
await Task.Delay(100, cancellationToken); // バッファが空の場合は100[ミリ秒]待機
}
}
}
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秒ごとに接続状態をチェック
}
}
}
受信
以下の例では、バッファリング、再接続機能、イベントベースの受信を行っている。
これにより、大量のデータを扱う場合や不安定な接続環境での使用に適している。
- バッファリング
- 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[秒]ごとに接続状態を確認
}
}
}