C#のシリアル通信入門|SerialPortでCOMポート接続・送受信・エラー対策を解説

はじめに

C#で機器制御や計測システムを開発する場合、よく使われる通信方法のひとつがシリアル通信です。シリアル通信は、COMポートを介してArduino、PLC、バーコードリーダー、測定器、電子天秤、マイコン、USBシリアル変換器などとデータを送受信する仕組みです。

C#では、SerialPortクラスを使うことで、比較的少ないコードでCOMポートの接続、データ送信、データ受信、切断処理を実装できます。一方で、実際の開発では「ポートが開けない」「受信データが途中で切れる」「ReadLineで止まる」「DataReceivedイベントが発生しない」「文字化けする」といったトラブルも起こりやすいです。

この記事では、C#のシリアル通信入門として、SerialPortによるCOMポート接続、送信、受信、非同期受信、タイムアウト処理、Windows FormsでのUI反映、よくあるエラーと対策までを実用的なサンプルコード付きで解説します。

1. C#のシリアル通信とは?SerialPortでできること

C#のシリアル通信とは、PCのCOMポートを通じて外部機器とデータを1ビットずつ順番に送受信する通信方法です。Windowsでは、USB接続の機器であっても、ドライバによって仮想COMポートとして認識されることがあります。

C#では、System.IO.Ports.SerialPortクラスを使うことで、COMポートを開く、文字列やバイト配列を送信する、受信データを読み取る、イベントで非同期に受信する、といった処理を実装できます。

1-1. シリアル通信の基本:COMポート・RS-232C・USBシリアルの違い

シリアル通信を理解するうえで、まず押さえておきたいのがCOMポート、RS-232C、USBシリアルの違いです。

COMポートは、Windows上でシリアル通信ポートを表す名前です。たとえば、COM1COM3COM10のような名前で表示されます。C#のSerialPortでは、このCOMポート名を指定して通信を行います。

RS-232Cは、シリアル通信の物理的・電気的な規格のひとつです。古い測定器、制御装置、産業機器では現在でも使われています。D-sub 9ピンコネクタを使う機器も多く、PC側にRS-232Cポートがない場合はUSBシリアル変換器を利用します。

USBシリアルは、USB接続の機器を仮想COMポートとして扱う方式です。Arduino、USBバーコードリーダー、USB接続の測定器、USB-RS232C変換アダプタなどでよく利用されます。見た目はUSB接続でも、Windows上ではCOM5などのCOMポートとして認識されるため、C#からはSerialPortで通信できます。

1-2. C#でシリアル通信を行う主な用途

C#のシリアル通信は、次のような用途でよく使われます。

製造業や検査装置では、測定器から数値データを取得したり、PLCやマイコンへ制御コマンドを送信したりする場面で使われます。たとえば、電子天秤から重量を読み取る、温度計から測定値を取得する、電源装置へ電圧設定コマンドを送る、といった処理です。

また、ArduinoやRaspberry Pi PicoなどのマイコンとPCアプリを連携させる場合にも使われます。C#側でWindowsアプリを作成し、マイコン側からセンサーデータを受信したり、C#側からLED制御コマンドを送信したりできます。

バーコードリーダー、QRコードリーダー、ラベルプリンタ、POS機器、GPS受信機なども、シリアル通信でデータをやり取りする代表的な機器です。

1-3. SerialPortクラスで実現できる送受信処理

SerialPortクラスでは、主に次のような処理を実現できます。

COMポートを指定して接続する処理、ボーレートやパリティなどの通信条件を設定する処理、文字列データを送信する処理、バイト配列を送信する処理、受信データを同期的に読み取る処理、DataReceivedイベントで非同期に受信する処理、タイムアウトを設定する処理、通信終了時にCOMポートを閉じる処理などです。

シリアル通信では、機器ごとに送信コマンドや応答形式が異なります。たとえば、コマンドの末尾に\rを付ける機器、\nを付ける機器、\r\nを付ける機器があります。そのため、C#側では機器仕様書に合わせて送信文字列や改行コードを正しく設定することが重要です。

1-4. Windowsアプリ・コンソールアプリでの利用シーン

C#のシリアル通信は、Windows Forms、WPF、コンソールアプリ、Windowsサービスなど、さまざまなアプリケーションで利用できます。

Windows FormsやWPFでは、送信ボタン、受信ログ表示欄、COMポート選択コンボボックスなどを用意して、ユーザーが操作しやすい通信アプリを作成できます。ただし、DataReceivedイベントはUIスレッドとは別のスレッドで発生するため、画面に受信データを表示する場合はInvokeDispatcherを使う必要があります。

コンソールアプリでは、シンプルな通信確認ツールや自動処理プログラムを作成するのに向いています。機器から一定周期でデータを取得するバッチ処理や、コマンド送信後に応答を受け取る簡易テストにも便利です。

2. C#でシリアル通信を始める前に必要な準備

C#でシリアル通信を始める前に、開発環境、必要なライブラリ、接続する機器、COMポート番号、通信条件を確認しておきます。特に、通信条件が機器側と一致していないと、ポートは開けても正しいデータを送受信できません。

2-1. 開発環境:Visual Studioと.NETの確認

C#でシリアル通信を行う場合、Visual Studioを使うと開発しやすいです。プロジェクトの種類は、用途に応じてコンソールアプリ、Windows Formsアプリ、WPFアプリなどを選びます。

.NET FrameworkのWindowsアプリでは、System.IO.Ports名前空間をそのまま利用できることが多いです。一方、.NET 6、.NET 7、.NET 8などの新しい.NETプロジェクトでは、必要に応じてSystem.IO.Portsパッケージを追加します。

Visual Studioでプロジェクトを作成したら、まずは次の名前空間を使用します。

C#
using System;
using System.IO.Ports;

文字コードを指定する場合は、次の名前空間も使います。

C#
using System.Text;

2-2. System.IO.Portsの追加方法

SerialPortクラスが見つからない場合は、System.IO.Portsパッケージを追加します。

Visual Studioでは、ソリューションエクスプローラーでプロジェクトを右クリックし、「NuGetパッケージの管理」を開きます。検索欄でSystem.IO.Portsを検索し、パッケージをインストールします。

コマンドで追加する場合は、プロジェクトフォルダで次のように実行します。

Bash
dotnet add package System.IO.Ports

追加後、C#ファイルの先頭に次のusingを記述します。

C#
using System.IO.Ports;

これで、SerialPortクラスを使ってCOMポート通信を実装できるようになります。

2-3. 接続機器とCOMポート番号の確認方法

C#でCOMポートに接続するには、対象機器がWindows上でどのCOMポート番号として認識されているかを確認する必要があります。

Windowsでは、デバイスマネージャーを開き、「ポート(COMとLPT)」を確認します。USBシリアル変換器やArduinoを接続している場合、たとえば「USB-SERIAL CH340 (COM5)」や「USB Serial Port (COM3)」のように表示されます。

C#側では、このCOM5COM3SerialPort.PortNameに指定します。

C#
serialPort.PortName = "COM5";

COMポート番号は、USBの差し込み口を変えたり、別の機器を接続したりすると変わることがあります。そのため、固定値で指定するだけでなく、利用可能なCOMポート一覧を取得してユーザーに選択させる設計にすると実用的です。

2-4. ボーレート・パリティ・データビット・ストップビットとは

シリアル通信では、通信条件を正しく設定する必要があります。代表的な設定項目は、ボーレート、パリティ、データビット、ストップビットです。

ボーレートは通信速度を表します。よく使われる値は9600192003840057600115200などです。Arduinoでは9600115200がよく使われます。

パリティは、データの誤り検出に使う設定です。一般的にはNoneが多く、機器によってはEvenOddを指定する場合もあります。

データビットは、1文字分のデータ長です。多くの場合は8を指定します。

ストップビットは、1つのデータ送信が終わったことを示すビットです。一般的にはOneを指定します。

C#では次のように設定します。

C#
serialPort.BaudRate = 9600;
serialPort.Parity = Parity.None;
serialPort.DataBits = 8;
serialPort.StopBits = StopBits.One;

2-5. 通信条件を機器側の設定と合わせる重要性

シリアル通信で最も重要なのは、C#側と機器側の通信条件を一致させることです。

たとえば、機器側が9600bps, 8bit, パリティなし, ストップビット1で動作しているのに、C#側を115200bpsに設定すると、受信データが文字化けしたり、まったく応答が返ってこなかったりします。

また、改行コードも重要です。機器によって、コマンド末尾に必要な改行コードが異なります。\r\n\r\nのどれを使うかは、機器の通信仕様書を確認する必要があります。

通信できない場合は、コードを疑う前に、COMポート番号、ボーレート、パリティ、データビット、ストップビット、改行コード、ケーブル、ドライバを順番に確認するのが基本です。

3. SerialPortクラスの基本的な使い方

C#でシリアル通信を行う基本的な流れは、SerialPortオブジェクトを作成し、通信条件を設定し、OpenメソッドでCOMポートを開き、送受信を行い、最後にCloseまたはDisposeで閉じる、という流れです。

3-1. SerialPortオブジェクトの作成

まず、SerialPortオブジェクトを作成します。

C#
SerialPort serialPort = new SerialPort();

コンストラクタでCOMポート名や通信条件を指定することもできます。

C#
SerialPort serialPort = new SerialPort("COM3", 9600, Parity.None, 8, StopBits.One);

後からプロパティで設定する方法のほうが、設定内容が分かりやすく、変更もしやすいため、実務ではプロパティ設定を使うことが多いです。

3-2. PortName・BaudRate・Parity・DataBits・StopBitsの設定

SerialPortで最低限設定する代表的なプロパティは次のとおりです。

C#
serialPort.PortName = "COM3";
serialPort.BaudRate = 9600;
serialPort.Parity = Parity.None;
serialPort.DataBits = 8;
serialPort.StopBits = StopBits.One;

さらに、読み取りタイムアウトや書き込みタイムアウトも設定しておくと、通信トラブル時に処理が止まりにくくなります。

C#
serialPort.ReadTimeout = 3000;
serialPort.WriteTimeout = 3000;

ReadTimeoutは受信待ちの最大時間、WriteTimeoutは送信処理の最大時間です。タイムアウトを設定しておくことで、機器から応答がない場合でもアプリが永久に待ち続けることを防げます。

3-3. OpenメソッドでCOMポートを開く

設定が完了したら、OpenメソッドでCOMポートを開きます。

C#
serialPort.Open();

Openを呼び出すと、指定したCOMポートが使用可能な状態になります。ただし、指定したCOMポートが存在しない場合、別のアプリが使用中の場合、デバイスが未接続の場合などは例外が発生します。

そのため、実際のアプリではtry-catchで例外処理を行います。

C#
try
{
serialPort.Open();
Console.WriteLine("COMポートを開きました。");
}
catch (Exception ex)
{
Console.WriteLine($"接続に失敗しました: {ex.Message}");
}

3-4. IsOpenで接続状態を確認する

COMポートが開いているかどうかは、IsOpenプロパティで確認できます。

C#
if (serialPort.IsOpen)
{
Console.WriteLine("接続中です。");
}
else
{
Console.WriteLine("未接続です。");
}

送信や受信を行う前にIsOpenを確認しておくと、未接続状態でWriteReadを呼び出して例外が発生することを防ぎやすくなります。

C#
if (serialPort.IsOpen)
{
serialPort.WriteLine("TEST");
}

3-5. Close・Disposeで安全にCOMポートを閉じる

シリアル通信が終わったら、必ずCOMポートを閉じます。

C#
serialPort.Close();

また、SerialPortはリソースを使用するため、不要になったらDisposeも行います。

C#
serialPort.Dispose();

アプリ終了時にCOMポートを閉じ忘れると、次回起動時に「COMポートが使用中」となり、接続できない場合があります。安全に閉じるには、次のように確認してから閉じます。

C#
if (serialPort != null)
{
if (serialPort.IsOpen)
{
serialPort.Close();
}

serialPort.Dispose();
}

4. C#でCOMポートに接続するサンプルコード

ここでは、C#でCOMポートに接続する基本的なサンプルを紹介します。まずは最小構成で接続し、次に利用可能なCOMポート一覧を取得する方法や、指定したCOMポートに接続する実装を確認します。

4-1. 最小構成の接続サンプル

次のコードは、COM39600bpsで接続する最小構成のサンプルです。

C#
using System;
using System.IO.Ports;

class Program
{
static void Main()
{
SerialPort serialPort = new SerialPort();

serialPort.PortName = "COM3";
serialPort.BaudRate = 9600;
serialPort.Parity = Parity.None;
serialPort.DataBits = 8;
serialPort.StopBits = StopBits.One;
serialPort.ReadTimeout = 3000;
serialPort.WriteTimeout = 3000;

try
{
serialPort.Open();
Console.WriteLine("COMポートを開きました。");
}
catch (Exception ex)
{
Console.WriteLine($"接続エラー: {ex.Message}");
}
finally
{
if (serialPort.IsOpen)
{
serialPort.Close();
Console.WriteLine("COMポートを閉じました。");
}

serialPort.Dispose();
}
}
}

このコードでは、Openに成功するとCOMポートが開かれ、最後にfinallyで確実に閉じるようにしています。

4-2. 利用可能なCOMポート一覧を取得する方法

利用可能なCOMポート一覧は、SerialPort.GetPortNames()で取得できます。

C#
string[] ports = SerialPort.GetPortNames();

foreach (string port in ports)
{
Console.WriteLine(port);
}

実行すると、次のように現在利用可能なCOMポート名が表示されます。

COM3
COM5
COM8

Windows Formsアプリでは、この一覧をコンボボックスに表示して、ユーザーに接続先を選ばせることができます。

C#
comboBoxPorts.Items.Clear();
comboBoxPorts.Items.AddRange(SerialPort.GetPortNames());

COMポート番号は環境によって変わるため、固定でCOM3などを指定するよりも、一覧から選択できるようにしたほうが実用的です。

4-3. 指定したCOMポートに接続する処理

次のコードは、指定したCOMポート名とボーレートで接続するメソッドの例です。

C#
using System;
using System.IO.Ports;

public class SerialConnector
{
private SerialPort _serialPort;

public bool Connect(string portName, int baudRate)
{
try
{
_serialPort = new SerialPort
{
PortName = portName,
BaudRate = baudRate,
Parity = Parity.None,
DataBits = 8,
StopBits = StopBits.One,
ReadTimeout = 3000,
WriteTimeout = 3000
};

_serialPort.Open();
return true;
}
catch (Exception ex)
{
Console.WriteLine($"接続失敗: {ex.Message}");
return false;
}
}

public void Disconnect()
{
if (_serialPort != null)
{
if (_serialPort.IsOpen)
{
_serialPort.Close();
}

_serialPort.Dispose();
}
}
}

このように接続処理をメソッド化しておくと、Windows FormsやWPFのボタン処理から呼び出しやすくなります。

4-4. 接続失敗時に確認すべきポイント

SerialPort.Open()で接続に失敗する場合は、次のポイントを確認します。

指定したCOMポートが存在しているかを確認します。デバイスマネージャーで表示されているCOMポート番号と、C#で指定しているPortNameが一致している必要があります。

別のアプリが同じCOMポートを使用していないかも確認します。ターミナルソフト、Arduino IDEのシリアルモニタ、別の自作アプリなどがCOMポートを開いていると、C#側では開けません。

USBシリアル変換器を使っている場合は、ドライバが正しくインストールされているか確認します。ドライバが不正な場合、デバイスマネージャー上で警告アイコンが表示されることがあります。

また、USBケーブルが充電専用ケーブルではないかも確認します。充電専用ケーブルでは通信できないため、データ通信対応のUSBケーブルを使用する必要があります。

4-5. using文を使った安全なリソース解放

短時間だけCOMポートを開いて処理する場合は、using文を使うと安全です。

C#
using System;
using System.IO.Ports;

class Program
{
static void Main()
{
using (SerialPort serialPort = new SerialPort("COM3", 9600, Parity.None, 8, StopBits.One))
{
serialPort.ReadTimeout = 3000;
serialPort.WriteTimeout = 3000;

try
{
serialPort.Open();
Console.WriteLine("接続しました。");

serialPort.WriteLine("TEST");
}
catch (Exception ex)
{
Console.WriteLine($"エラー: {ex.Message}");
}
}

Console.WriteLine("COMポートを解放しました。");
}
}

usingを使うと、処理を抜けるときに自動的にDisposeが呼ばれます。接続と送信を短い範囲で完結させる場合に便利です。

5. SerialPortでデータを送信する方法

C#のSerialPortでは、WriteメソッドやWriteLineメソッドを使ってデータを送信できます。送信するデータが文字列なのか、バイナリデータなのか、改行コードが必要なのかによって使い分けます。

5-1. WriteメソッドとWriteLineメソッドの違い

Writeメソッドは、指定した文字列やバイト配列をそのまま送信します。

C#
serialPort.Write("TEST");

一方、WriteLineメソッドは、文字列の末尾にNewLineプロパティで指定された改行コードを付けて送信します。

C#
serialPort.WriteLine("TEST");

初期状態のNewLineは環境や設定に依存するため、機器仕様に合わせて明示的に設定するのがおすすめです。

C#
serialPort.NewLine = "\r\n";
serialPort.WriteLine("TEST");

機器がコマンド終端として\rを要求する場合は、次のように設定します。

C#
serialPort.NewLine = "\r";
serialPort.WriteLine("MEASURE");

WriteLineは便利ですが、意図しない改行コードが付くと機器がコマンドを認識しない場合があります。機器仕様が明確な場合は、Writeで改行コードまで明示的に送る方法もあります。

C#
serialPort.Write("MEASURE\r");

5-2. 文字列データを送信するサンプル

次のコードは、COMポートに接続して文字列コマンドを送信するサンプルです。

C#
using System;
using System.IO.Ports;

class Program
{
static void Main()
{
using (SerialPort serialPort = new SerialPort("COM3", 9600, Parity.None, 8, StopBits.One))
{
serialPort.NewLine = "\r\n";
serialPort.WriteTimeout = 3000;

try
{
serialPort.Open();

string command = "STATUS";
serialPort.WriteLine(command);

Console.WriteLine($"送信しました: {command}");
}
catch (Exception ex)
{
Console.WriteLine($"送信エラー: {ex.Message}");
}
}
}
}

この例では、STATUSという文字列に改行コードを付けて送信しています。測定器やマイコンが改行コードを受け取ったタイミングでコマンド処理を開始する仕様の場合に有効です。

5-3. バイト配列を送信するサンプル

機器によっては、文字列ではなくバイナリ形式のコマンドを送信する場合があります。その場合は、バイト配列を使います。

C#
byte[] command = new byte[] { 0x02, 0x30, 0x31, 0x03 };

serialPort.Write(command, 0, command.Length);

全体のサンプルは次のとおりです。

C#
using System;
using System.IO.Ports;

class Program
{
static void Main()
{
using (SerialPort serialPort = new SerialPort("COM3", 9600, Parity.None, 8, StopBits.One))
{
serialPort.WriteTimeout = 3000;

try
{
serialPort.Open();

byte[] command = new byte[] { 0x02, 0x30, 0x31, 0x03 };
serialPort.Write(command, 0, command.Length);

Console.WriteLine("バイト配列を送信しました。");
}
catch (Exception ex)
{
Console.WriteLine($"送信エラー: {ex.Message}");
}
}
}
}

バイナリ通信では、STX、ETX、チェックサム、BCCなどを使うプロトコルもあります。機器仕様書を確認し、必要なバイト列を正しく組み立てることが重要です。

5-4. 改行コードを含むコマンド送信の注意点

シリアル通信でよくあるトラブルが、改行コードの不一致です。

機器によって、コマンドの終端に必要な文字が異なります。代表的な改行コードは次のとおりです。

\r    CR
\n LF
\r\n CRLF

たとえば、機器仕様書に「コマンド終端はCR」と書かれている場合、C#では次のように送信します。

C#
serialPort.Write("READ\r");

「コマンド終端はCRLF」の場合は、次のように送信します。

C#
serialPort.Write("READ\r\n");

WriteLineを使う場合は、NewLineを合わせます。

C#
serialPort.NewLine = "\r";
serialPort.WriteLine("READ");

WriteLine("READ")と書くだけでは、機器が期待する改行コードと一致しない可能性があります。通信できない場合は、まず改行コードを確認しましょう。

5-5. Encoding設定と文字化け対策

SerialPortで文字列を送受信する場合、文字コードの設定も重要です。初期設定ではASCII相当の文字を扱うケースが多く、日本語や特殊文字を扱う場合は文字化けすることがあります。

UTF-8で通信する場合は、次のように設定します。

C#
serialPort.Encoding = Encoding.UTF8;

Shift_JISで通信する機器の場合は、次のように設定します。

C#
serialPort.Encoding = Encoding.GetEncoding("shift_jis");

.NETの環境によっては、Shift_JISなどのコードページを使うために、System.Text.Encoding.CodePagesパッケージを追加し、起動時に次の登録が必要になる場合があります。

C#
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

文字化けが発生する場合は、ボーレートなどの通信条件だけでなく、Encodingの設定も確認してください。

6. SerialPortでデータを受信する方法

C#のSerialPortでデータを受信する方法には、同期的に読み取る方法と、DataReceivedイベントを使って非同期に読み取る方法があります。

コマンド送信後にすぐ応答を待つ処理では同期受信が分かりやすく、常時データが送られてくる機器では非同期受信が便利です。

6-1. ReadLine・ReadExisting・Readメソッドの違い

ReadLineは、改行コードまでの文字列を読み取るメソッドです。受信データが行単位で送られてくる機器に向いています。

C#
string response = serialPort.ReadLine();

ただし、指定した改行コードが受信されるまで待ち続けるため、改行コードが一致していないとタイムアウトします。

ReadExistingは、現在受信バッファにあるデータを文字列として読み取ります。

C#
string data = serialPort.ReadExisting();

すでに届いている分だけ読み取るため、データが途中で分割される可能性があります。

Readは、バイト配列に指定したサイズ分だけ読み取るメソッドです。バイナリ通信や固定長データの受信に向いています。

C#
byte[] buffer = new byte[256];
int length = serialPort.Read(buffer, 0, buffer.Length);

文字列通信ではReadLineReadExisting、バイナリ通信ではReadを使うことが多いです。

6-2. 同期受信でデータを読み取るサンプル

次のコードは、コマンドを送信した後、ReadLineで応答を受信するサンプルです。

C#
using System;
using System.IO.Ports;

class Program
{
static void Main()
{
using (SerialPort serialPort = new SerialPort("COM3", 9600, Parity.None, 8, StopBits.One))
{
serialPort.NewLine = "\r\n";
serialPort.ReadTimeout = 3000;
serialPort.WriteTimeout = 3000;

try
{
serialPort.Open();

serialPort.WriteLine("STATUS");
Console.WriteLine("コマンドを送信しました。");

string response = serialPort.ReadLine();
Console.WriteLine($"受信: {response}");
}
catch (TimeoutException)
{
Console.WriteLine("受信がタイムアウトしました。");
}
catch (Exception ex)
{
Console.WriteLine($"通信エラー: {ex.Message}");
}
}
}
}

ReadLineを使う場合、機器が送信する応答の末尾とserialPort.NewLineが一致している必要があります。機器が\rで終端するなら、C#側もserialPort.NewLine = "\r";にします。

6-3. DataReceivedイベントで非同期受信するサンプル

常時データが送られてくる機器や、いつ応答が返ってくるか分からない機器では、DataReceivedイベントを使う方法が便利です。

C#
using System;
using System.IO.Ports;

class Program
{
private static SerialPort _serialPort;

static void Main()
{
_serialPort = new SerialPort("COM3", 9600, Parity.None, 8, StopBits.One);
_serialPort.NewLine = "\r\n";
_serialPort.DataReceived += SerialPort_DataReceived;

try
{
_serialPort.Open();
Console.WriteLine("受信待機中です。Enterキーで終了します。");

Console.ReadLine();
}
catch (Exception ex)
{
Console.WriteLine($"エラー: {ex.Message}");
}
finally
{
if (_serialPort.IsOpen)
{
_serialPort.Close();
}

_serialPort.Dispose();
}
}

private static void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
try
{
string data = _serialPort.ReadExisting();
Console.WriteLine($"受信: {data}");
}
catch (Exception ex)
{
Console.WriteLine($"受信エラー: {ex.Message}");
}
}
}

DataReceivedイベントは、受信データが到着したタイミングで発生します。ただし、1回のイベントで1メッセージ全体が必ず受信できるとは限りません。そのため、実用的な実装では受信データをバッファに蓄積し、改行コードや終端文字で1メッセージを判定します。

6-4. BytesToReadを使って受信バッファを確認する方法

BytesToReadプロパティを使うと、受信バッファに何バイトのデータがあるか確認できます。

C#
int count = serialPort.BytesToRead;

バイト配列として受信する場合は、BytesToReadのサイズに合わせて読み取ることもできます。

C#
int bytesToRead = serialPort.BytesToRead;
byte[] buffer = new byte[bytesToRead];

int readBytes = serialPort.Read(buffer, 0, bytesToRead);

ReadExistingは文字列として読み取りますが、バイナリデータを扱う場合はReadBytesToReadを組み合わせるほうが安全です。

6-5. 受信データが分割される場合の対処法

シリアル通信では、送信側が1回で送ったデータでも、受信側では複数回に分割されることがあります。

たとえば、機器が次の文字列を送信したとします。

TEMP:25.4\r\n

しかし、C#側のDataReceivedイベントでは、次のように分割されて受信される可能性があります。

TEMP:
25.4
\r\n

そのため、DataReceivedイベント内で受け取ったデータをそのまま1件のメッセージとして処理すると、途中で切れたデータを処理してしまうことがあります。

対策として、受信データを一時バッファに追加し、改行コードが見つかったタイミングで1行として取り出します。

C#
private static string _receiveBuffer = "";

private static void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
string data = _serialPort.ReadExisting();
_receiveBuffer += data;

while (_receiveBuffer.Contains("\r\n"))
{
int index = _receiveBuffer.IndexOf("\r\n");
string line = _receiveBuffer.Substring(0, index);

_receiveBuffer = _receiveBuffer.Substring(index + 2);

Console.WriteLine($"1行受信: {line}");
}
}

このように、受信データを蓄積してからメッセージ単位で処理することで、データ分割による不具合を防ぎやすくなります。

6-6. UIスレッドへの反映とInvoke処理の注意点

Windows FormsでDataReceivedイベントを使う場合、イベントはUIスレッドとは別のスレッドで実行されます。そのため、イベント内から直接TextBoxLabelを更新すると例外が発生することがあります。

Windows Formsでは、Invokeを使ってUIスレッドに処理を渡します。

C#
private void serialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
string data = serialPort.ReadExisting();

this.Invoke(new Action(() =>
{
textBoxReceive.AppendText(data);
}));
}

WPFの場合は、Dispatcher.Invokeを使います。

C#
Dispatcher.Invoke(() =>
{
textBoxReceive.AppendText(data);
});

UIアプリでシリアル通信を行う場合、受信処理と画面更新のスレッドが異なることを意識する必要があります。

7. 送受信を組み合わせた実用サンプル

実際のシリアル通信では、データ送信だけ、受信だけではなく、コマンドを送信して応答を受信する流れがよく使われます。ここでは、送受信を組み合わせた実用的な実装例を紹介します。

7-1. コマンド送信後に応答を受信する基本フロー

基本的な流れは次のとおりです。

まずCOMポートを開きます。次に、必要であれば受信バッファをクリアします。その後、機器にコマンドを送信します。最後に、ReadLineReadで応答を受信します。

C#
serialPort.DiscardInBuffer();
serialPort.WriteLine("READ");
string response = serialPort.ReadLine();

DiscardInBufferは、受信バッファに残っている古いデータを破棄するメソッドです。コマンド送信前に古いデータを消しておくことで、前回の応答を誤って読み取るリスクを減らせます。

実用的なコード例は次のとおりです。

C#
public string SendCommand(string command)
{
if (!serialPort.IsOpen)
{
throw new InvalidOperationException("COMポートが開いていません。");
}

serialPort.DiscardInBuffer();
serialPort.WriteLine(command);

return serialPort.ReadLine();
}

7-2. 送信ボタンと受信表示を備えたWindows Formsサンプル

Windows Formsで、送信ボタンと受信表示欄を備えた簡単なサンプルを考えます。

フォームには、次の部品がある想定です。

comboBoxPorts    COMポート選択
buttonConnect 接続ボタン
textBoxSend 送信文字列
buttonSend 送信ボタン
textBoxReceive 受信表示

サンプルコードは次のとおりです。

C#
using System;
using System.IO.Ports;
using System.Windows.Forms;

public partial class Form1 : Form
{
private SerialPort serialPort = new SerialPort();

public Form1()
{
InitializeComponent();

comboBoxPorts.Items.AddRange(SerialPort.GetPortNames());

serialPort.BaudRate = 9600;
serialPort.Parity = Parity.None;
serialPort.DataBits = 8;
serialPort.StopBits = StopBits.One;
serialPort.NewLine = "\r\n";
serialPort.DataReceived += SerialPort_DataReceived;
}

private void buttonConnect_Click(object sender, EventArgs e)
{
try
{
if (!serialPort.IsOpen)
{
serialPort.PortName = comboBoxPorts.Text;
serialPort.Open();

MessageBox.Show("接続しました。");
}
}
catch (Exception ex)
{
MessageBox.Show($"接続エラー: {ex.Message}");
}
}

private void buttonSend_Click(object sender, EventArgs e)
{
try
{
if (serialPort.IsOpen)
{
serialPort.WriteLine(textBoxSend.Text);
}
else
{
MessageBox.Show("COMポートが開いていません。");
}
}
catch (Exception ex)
{
MessageBox.Show($"送信エラー: {ex.Message}");
}
}

private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
string data = serialPort.ReadExisting();

this.Invoke(new Action(() =>
{
textBoxReceive.AppendText(data);
}));
}

private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
if (serialPort.IsOpen)
{
serialPort.Close();
}

serialPort.Dispose();
}
}

Windows Formsでは、フォームを閉じるときに必ずCOMポートを閉じるようにします。

7-3. コンソールアプリでの送受信サンプル

コンソールアプリでは、キーボードから入力した文字列を送信し、機器からの応答を表示する簡単なテストツールを作れます。

C#
using System;
using System.IO.Ports;

class Program
{
static void Main()
{
using (SerialPort serialPort = new SerialPort("COM3", 9600, Parity.None, 8, StopBits.One))
{
serialPort.NewLine = "\r\n";
serialPort.ReadTimeout = 3000;
serialPort.WriteTimeout = 3000;

try
{
serialPort.Open();
Console.WriteLine("接続しました。終了するには exit と入力してください。");

while (true)
{
Console.Write("送信> ");
string command = Console.ReadLine();

if (command == "exit")
{
break;
}

serialPort.WriteLine(command);

try
{
string response = serialPort.ReadLine();
Console.WriteLine($"受信> {response}");
}
catch (TimeoutException)
{
Console.WriteLine("応答がありませんでした。");
}
}
}
catch (Exception ex)
{
Console.WriteLine($"エラー: {ex.Message}");
}
}
}
}

このサンプルは、測定器やArduinoとの通信確認に便利です。最初にターミナルソフトで動作確認し、その後C#コードに置き換えるとトラブルを切り分けやすくなります。

7-4. タイムアウト付きで応答を待つ実装例

機器から応答が返ってこない場合、ReadLineReadTimeoutで指定した時間まで待機します。タイムアウトを適切に処理することで、アプリの停止を防げます。

C#
public string SendCommandWithTimeout(string command, int timeout)
{
serialPort.ReadTimeout = timeout;

try
{
serialPort.DiscardInBuffer();
serialPort.WriteLine(command);

return serialPort.ReadLine();
}
catch (TimeoutException)
{
return "TIMEOUT";
}
}

より実用的には、タイムアウト時に再送するリトライ処理を追加します。

C#
public string SendCommandWithRetry(string command, int retryCount)
{
for (int i = 0; i < retryCount; i++)
{
try
{
serialPort.DiscardInBuffer();
serialPort.WriteLine(command);

return serialPort.ReadLine();
}
catch (TimeoutException)
{
Console.WriteLine($"タイムアウトしました。リトライ {i + 1}/{retryCount}");
}
}

throw new TimeoutException("規定回数リトライしましたが、応答がありませんでした。");
}

通信が不安定な環境では、タイムアウトとリトライを組み合わせることで安定性を高められます。

7-5. Arduinoや測定器との通信で使う実装パターン

ArduinoとC#で通信する場合、Arduino側ではSerial.begin(9600)のようにボーレートを設定します。C#側も同じボーレートに合わせます。

Arduino側の例です。

C++
void setup() {
Serial.begin(9600);
}

void loop() {
if (Serial.available() > 0) {
String command = Serial.readStringUntil('\n');

if (command == "LED_ON") {
Serial.println("OK");
} else {
Serial.println("UNKNOWN");
}
}
}

C#側では次のように送信します。

C#
serialPort.NewLine = "\n";
serialPort.WriteLine("LED_ON");

string response = serialPort.ReadLine();
Console.WriteLine(response);

測定器の場合は、機器仕様書に記載されたコマンドを送信します。たとえば、測定値取得コマンドがMEAS?で、終端がCRLFの場合は次のようにします。

C#
serialPort.NewLine = "\r\n";
serialPort.WriteLine("MEAS?");

string value = serialPort.ReadLine();
Console.WriteLine($"測定値: {value}");

Arduinoや測定器では、起動直後や接続直後に応答できない時間がある場合があります。接続後すぐに送信して失敗する場合は、少し待ってから送信する設計も有効です。

8. C#のシリアル通信でよくあるエラーと原因

C#のシリアル通信では、COMポートの使用状況、デバイス接続状態、通信条件、受信タイミングなどによってさまざまなエラーが発生します。ここでは代表的な例外と原因を解説します。

8-1. UnauthorizedAccessException:COMポートが使用中

UnauthorizedAccessExceptionは、指定したCOMポートにアクセスできない場合に発生します。代表的な原因は、別のアプリがすでに同じCOMポートを使用していることです。

たとえば、Arduino IDEのシリアルモニタ、Tera Term、別のC#アプリなどがCOMポートを開いていると、C#側でOpenできません。

対策として、他のアプリを閉じてから再実行します。また、自作アプリ側でも終了時にCloseDisposeを確実に呼び出すことが重要です。

C#
catch (UnauthorizedAccessException)
{
Console.WriteLine("COMポートが使用中、またはアクセスできません。");
}

8-2. IOException:接続切断・デバイス未接続

IOExceptionは、通信中にデバイスが切断された場合や、ポートの状態に問題がある場合に発生することがあります。

USBシリアル変換器を抜いた、ケーブルが接触不良になった、機器の電源が切れた、ドライバに問題がある、といったケースで発生します。

C#
catch (IOException ex)
{
Console.WriteLine($"入出力エラー: {ex.Message}");
}

対策として、通信中の例外を捕捉し、必要に応じてポートを閉じて再接続できるようにします。常時稼働するアプリでは、切断検出と再接続処理を設計しておくと安定します。

8-3. TimeoutException:受信待ちタイムアウト

TimeoutExceptionは、ReadLineReadで指定時間内にデータを受信できなかった場合に発生します。

主な原因は、機器から応答が返っていない、改行コードが一致していない、送信コマンドが間違っている、ボーレートなどの通信条件が合っていない、などです。

C#
catch (TimeoutException)
{
Console.WriteLine("受信タイムアウトが発生しました。");
}

ReadLineを使っている場合は、serialPort.NewLineと機器が返す終端文字が一致しているかを確認しましょう。

8-4. ArgumentException:存在しないCOMポート指定

ArgumentExceptionは、不正なポート名を指定した場合に発生することがあります。

C#
serialPort.PortName = "COM999";

存在しないCOMポートを指定している、ポート名の形式が正しくない、空文字を指定している、といったケースです。

対策として、SerialPort.GetPortNames()で取得した一覧の中から選択させるようにします。

C#
string[] ports = SerialPort.GetPortNames();

if (!ports.Contains("COM3"))
{
Console.WriteLine("指定したCOMポートは存在しません。");
}

8-5. 文字化け・改行コード不一致・通信条件ミス

ポートは開けるのに受信データが文字化けする場合、通信条件の不一致が疑われます。特に、ボーレート、パリティ、データビット、ストップビットが機器側と一致しているか確認してください。

また、日本語や特殊文字を扱う場合は、Encodingの不一致でも文字化けが発生します。

C#
serialPort.Encoding = Encoding.UTF8;

機器がShift_JISを使っている場合は、C#側もShift_JISに合わせる必要があります。

改行コードの不一致もよくある原因です。ReadLineで止まる場合や、コマンドを送っても反応しない場合は、\r\n\r\nの違いを確認してください。

8-6. DataReceivedイベントが発生しない原因

DataReceivedイベントが発生しない場合、まずイベントハンドラが正しく登録されているか確認します。

C#
serialPort.DataReceived += SerialPort_DataReceived;

次に、COMポートが開いているか確認します。

C#
if (!serialPort.IsOpen)
{
serialPort.Open();
}

また、機器が実際にデータを送信しているかをターミナルソフトで確認します。C#側の問題ではなく、機器側が応答していないケースも多いです。

受信データが少ない場合やタイミングによっては、ReceivedBytesThresholdの設定も関係します。通常は初期値のままで問題ありませんが、特定のバイト数を受信するまでイベントを発生させたくない場合に設定します。

C#
serialPort.ReceivedBytesThreshold = 1;

9. シリアル通信のトラブルシューティング

シリアル通信で問題が発生した場合は、コードだけを見るのではなく、機器、ケーブル、ドライバ、COMポート、通信条件、改行コードを順番に確認することが重要です。

9-1. デバイスマネージャーでCOMポートを確認する

まず、Windowsのデバイスマネージャーで対象機器がCOMポートとして認識されているか確認します。

「ポート(COMとLPT)」を開き、接続した機器が表示されているかを確認します。表示されていない場合は、USBケーブル、機器の電源、ドライバ、USBポートを確認します。

表示されている場合は、COM番号を確認し、C#コードのPortNameと一致しているか確認します。

C#
serialPort.PortName = "COM5";

COM番号が分からない場合は、機器を抜き差しして、デバイスマネージャー上で増減するCOMポートを確認すると見つけやすいです。

9-2. ターミナルソフトで機器の応答を確認する

C#コードで通信できない場合、まずTera Term、PuTTY、RealTermなどのターミナルソフトで通信できるか確認すると切り分けがしやすくなります。

ターミナルソフトで同じCOMポート、同じボーレート、同じパリティ、同じ改行コードを設定し、機器にコマンドを送信します。ターミナルソフトで応答が返るなら、機器やケーブルではなくC#コード側に原因がある可能性が高いです。

逆に、ターミナルソフトでも応答がない場合は、機器側の設定、ケーブル、ドライバ、通信条件を確認します。

9-3. ボーレートやパリティを再確認する

シリアル通信では、ボーレートやパリティが1つでも違うと正しく通信できません。

よくある設定は次の組み合わせです。

9600bps, 8bit, None, 1stop
115200bps, 8bit, None, 1stop

C#では次のように指定します。

C#
serialPort.BaudRate = 9600;
serialPort.DataBits = 8;
serialPort.Parity = Parity.None;
serialPort.StopBits = StopBits.One;

機器仕様書に9600, 7, Even, 1のように書かれている場合は、C#側も次のように合わせます。

C#
serialPort.BaudRate = 9600;
serialPort.DataBits = 7;
serialPort.Parity = Parity.Even;
serialPort.StopBits = StopBits.One;

通信条件は推測ではなく、機器仕様書や設定画面で確認することが大切です。

9-4. DtrEnable・RtsEnableが必要なケース

一部の機器では、DTRRTSの制御信号を有効にしないと通信が始まらない場合があります。特に、モデム制御線を使う機器や、一部のUSBシリアル機器では注意が必要です。

C#では次のように設定できます。

C#
serialPort.DtrEnable = true;
serialPort.RtsEnable = true;

Arduinoの場合、DTRの状態によってリセットが発生することがあります。接続直後にArduinoがリセットされ、すぐにコマンドを送ると応答できない場合があります。その場合は、Open後に少し待ってから送信します。

C#
serialPort.Open();
Thread.Sleep(2000);
serialPort.WriteLine("START");

機器仕様書にDTRやRTSに関する記載がある場合は、その内容に合わせて設定します。

9-5. USBシリアル変換器・ドライバの問題を切り分ける

USBシリアル変換器を使っている場合、ドライバの問題で通信が不安定になることがあります。

デバイスマネージャーで警告アイコンが表示されていないか確認します。また、変換器のチップに合ったドライバがインストールされているか確認します。代表的なチップにはFTDI、CP210x、CH340、Prolificなどがあります。

通信が途中で切れる、COMポートが突然消える、PCによって認識されたりされなかったりする場合は、別のUSBポート、別のUSBケーブル、別のUSBシリアル変換器で試すと原因を切り分けやすくなります。

9-6. ケーブル・配線・クロス/ストレートの確認

RS-232C接続では、ケーブルの種類や配線も重要です。

機器によって、ストレートケーブルが必要な場合と、クロスケーブルが必要な場合があります。送信線と受信線が正しく接続されていないと、ポートは開けてもデータは届きません。

RS-232Cでは、TXD、RXD、GNDの接続を確認します。マイコンのTTLシリアルとPCのRS-232Cは電圧レベルが異なるため、直接接続してはいけません。必要に応じてレベル変換回路やUSBシリアル変換モジュールを使用します。

USB接続の場合でも、充電専用ケーブルでは通信できません。データ通信対応のUSBケーブルを使用してください。

10. 安定したシリアル通信を実装するための設計ポイント

シリアル通信を実用アプリに組み込む場合、単にOpenしてWriteLineするだけでは不十分です。例外処理、ログ、タイムアウト、リトライ、バッファ管理、終了処理を設計しておくことで、安定した通信アプリを作れます。

10-1. 送受信処理をクラス化する

シリアル通信処理は、フォームや画面のコードに直接書くよりも、専用クラスにまとめるのがおすすめです。

C#
public class SerialCommunication : IDisposable
{
private readonly SerialPort _serialPort;

public SerialCommunication(string portName, int baudRate)
{
_serialPort = new SerialPort(portName, baudRate, Parity.None, 8, StopBits.One)
{
NewLine = "\r\n",
ReadTimeout = 3000,
WriteTimeout = 3000
};
}

public void Open()
{
if (!_serialPort.IsOpen)
{
_serialPort.Open();
}
}

public string SendAndReceive(string command)
{
if (!_serialPort.IsOpen)
{
throw new InvalidOperationException("COMポートが開いていません。");
}

_serialPort.DiscardInBuffer();
_serialPort.WriteLine(command);

return _serialPort.ReadLine();
}

public void Close()
{
if (_serialPort.IsOpen)
{
_serialPort.Close();
}
}

public void Dispose()
{
Close();
_serialPort.Dispose();
}
}

通信処理をクラス化すると、画面側のコードが整理され、テストや再利用もしやすくなります。

10-2. 例外処理とログ出力を実装する

シリアル通信では、機器切断、タイムアウト、ポート使用中などの例外が発生する前提で実装する必要があります。

C#
try
{
string response = serial.SendAndReceive("READ");
Console.WriteLine(response);
}
catch (TimeoutException ex)
{
Console.WriteLine($"タイムアウト: {ex.Message}");
}
catch (UnauthorizedAccessException ex)
{
Console.WriteLine($"ポート使用中: {ex.Message}");
}
catch (IOException ex)
{
Console.WriteLine($"入出力エラー: {ex.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"予期しないエラー: {ex.Message}");
}

実用アプリでは、画面に表示するだけでなく、ログファイルにも記録しておくと原因調査がしやすくなります。

C#
File.AppendAllText("serial.log", $"{DateTime.Now}: {ex}\r\n");

送信データ、受信データ、タイムアウト、再接続、例外内容をログに残すと、現場でのトラブル対応に役立ちます。

10-3. タイムアウトとリトライ処理を入れる

機器からの応答が常に一定時間内に返るとは限りません。ノイズ、処理遅延、機器の状態によって応答が遅れることがあります。

そのため、ReadTimeoutを設定し、必要に応じてリトライ処理を実装します。

C#
serialPort.ReadTimeout = 3000;
serialPort.WriteTimeout = 3000;

リトライ処理の例です。

C#
public string SendWithRetry(string command, int maxRetry)
{
for (int i = 0; i < maxRetry; i++)
{
try
{
serialPort.DiscardInBuffer();
serialPort.WriteLine(command);

return serialPort.ReadLine();
}
catch (TimeoutException)
{
if (i == maxRetry - 1)
{
throw;
}
}
}

throw new TimeoutException("応答がありません。");
}

ただし、同じコマンドを何度も送ると機器側で重複処理される場合があります。リトライしてよいコマンドかどうかは、機器仕様に合わせて判断してください。

10-4. 受信バッファを適切に管理する

シリアル通信では、古い受信データがバッファに残っていると、次のコマンドの応答と混ざることがあります。

コマンド送信前に古い受信データを破棄する場合は、DiscardInBufferを使います。

C#
serialPort.DiscardInBuffer();

送信バッファを破棄する場合は、DiscardOutBufferを使います。

C#
serialPort.DiscardOutBuffer();

ただし、通信中に不用意にバッファを破棄すると、本来受け取るべきデータまで消してしまう可能性があります。バッファクリアは、コマンド送信前や再接続直後など、タイミングを決めて使うことが重要です。

非同期受信では、受信データを文字列バッファやバイト配列バッファに蓄積し、終端文字、固定長、ヘッダー・フッターなどでメッセージ単位を判定します。

10-5. アプリ終了時にCOMポートを確実に閉じる

アプリ終了時にCOMポートを閉じ忘れると、次回起動時にポートが使用中になることがあります。Windows Formsでは、FormClosingイベントで閉じる処理を入れます。

C#
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
if (serialPort != null)
{
if (serialPort.IsOpen)
{
serialPort.Close();
}

serialPort.Dispose();
}
}

コンソールアプリでは、try-finallyusingを使って確実に解放します。

C#
try
{
serialPort.Open();
// 通信処理
}
finally
{
if (serialPort.IsOpen)
{
serialPort.Close();
}

serialPort.Dispose();
}

通信アプリでは、正常終了だけでなく、例外発生時にもCOMポートを閉じる設計が必要です。

10-6. 同期処理と非同期処理の使い分け

シリアル通信では、同期処理と非同期処理を用途に応じて使い分けます。

コマンドを送って、すぐに応答を待つだけの処理なら、WriteLineReadLineを組み合わせた同期処理が分かりやすいです。

C#
serialPort.WriteLine("READ");
string response = serialPort.ReadLine();

一方、機器からいつデータが届くか分からない場合や、常時データを受信する場合は、DataReceivedイベントを使った非同期処理が向いています。

C#
serialPort.DataReceived += SerialPort_DataReceived;

ただし、非同期受信では、データの分割、UIスレッドへの反映、バッファ管理を考慮する必要があります。単純な要求応答型の通信では同期処理、常時監視型の通信では非同期処理、という考え方で選ぶとよいでしょう。

11. C#のシリアル通信に関するよくある質問

ここでは、C#のシリアル通信でよくある質問をまとめます。初めてSerialPortを使う場合につまずきやすいポイントを確認しておきましょう。

11-1. COMポート番号はどうやって調べる?

COMポート番号は、Windowsのデバイスマネージャーで確認できます。

デバイスマネージャーを開き、「ポート(COMとLPT)」を確認します。USBシリアル変換器やArduinoなどを接続している場合、COM3COM5のような番号が表示されます。

C#から取得する場合は、SerialPort.GetPortNames()を使います。

C#
string[] ports = SerialPort.GetPortNames();

foreach (string port in ports)
{
Console.WriteLine(port);
}

Windows Formsでは、この一覧をコンボボックスに表示して選択できるようにすると便利です。

11-2. SerialPortが.NETで使えない場合はどうする?

SerialPortが見つからない場合は、まずusing System.IO.Ports;を追加しているか確認します。

C#
using System.IO.Ports;

それでも使えない場合は、System.IO.Portsパッケージを追加します。

Bash
dotnet add package System.IO.Ports

Visual Studioを使っている場合は、「NuGetパッケージの管理」からSystem.IO.Portsを検索して追加できます。

また、プロジェクトの種類や対象フレームワークによって利用方法が異なる場合があります。WindowsでCOMポート通信を行う場合は、Windows Formsアプリやコンソールアプリで動作確認すると切り分けやすいです。

11-3. ReadLineで止まる原因は?

ReadLineで処理が止まる主な原因は、改行コードが受信されていないことです。

ReadLineは、SerialPort.NewLineで指定された文字列を受信するまで待機します。たとえば、C#側が\r\nを待っているのに、機器側が\rしか返していない場合、ReadLineは行の終端を検出できません。

対策として、機器の仕様に合わせてNewLineを設定します。

C#
serialPort.NewLine = "\r";

また、タイムアウトを設定しておくことも重要です。

C#
serialPort.ReadTimeout = 3000;

これにより、応答がない場合でも永久に待ち続けることを防げます。

11-4. DataReceivedで受信データが途中で切れるのはなぜ?

DataReceivedイベントは、1つのメッセージ全体を受信したタイミングで発生するとは限りません。受信バッファにデータが入ったタイミングで発生するため、送信側が1行で送ったデータでも、C#側では複数回に分かれて受信されることがあります。

そのため、ReadExistingで取得した文字列をそのまま処理すると、途中で切れたデータを処理してしまう場合があります。

対策として、受信データをバッファに蓄積し、改行コードや終端文字が来たタイミングで1メッセージとして処理します。

C#
receiveBuffer += serialPort.ReadExisting();

if (receiveBuffer.Contains("\r\n"))
{
// 1行分を取り出して処理する
}

固定長のバイナリデータを受信する場合は、必要なバイト数がそろうまで待ってから処理します。

11-5. ArduinoとC#で通信するには何が必要?

ArduinoとC#で通信するには、Arduino側のシリアル設定とC#側のSerialPort設定を一致させる必要があります。

Arduino側では、たとえば次のように設定します。

C++
Serial.begin(9600);

C#側でも同じボーレートを指定します。

C#
SerialPort serialPort = new SerialPort("COM3", 9600);

また、Arduino IDEのシリアルモニタを開いたままだと、C#側で同じCOMポートを開けません。C#アプリを実行する前に、シリアルモニタを閉じてください。

ArduinoはC#からCOMポートを開いたタイミングでリセットされることがあります。接続直後に送信して応答がない場合は、少し待ってから送信すると改善することがあります。

C#
serialPort.Open();
Thread.Sleep(2000);
serialPort.WriteLine("HELLO");

11-6. 複数のCOMポートを同時に扱える?

C#では、複数のSerialPortオブジェクトを作成すれば、複数のCOMポートを同時に扱えます。

C#
SerialPort port1 = new SerialPort("COM3", 9600);
SerialPort port2 = new SerialPort("COM4", 9600);

port1.Open();
port2.Open();

ただし、同じCOMポートを複数のSerialPortオブジェクトや複数のアプリから同時に開くことはできません。COM3を1つのアプリが使用している間、別のアプリから同じCOM3を開こうとするとエラーになります。

複数ポートを扱う場合は、ポートごとに受信イベント、バッファ、ログ、例外処理を分けて管理する必要があります。クラス化してポートごとのインスタンスを作ると管理しやすくなります。

まとめ

C#のシリアル通信は、SerialPortクラスを使うことで、COMポート接続、データ送信、データ受信、非同期受信を比較的簡単に実装できます。Arduino、測定器、PLC、バーコードリーダー、電子天秤、USBシリアル変換器など、多くの機器との連携に利用できます。

基本的な流れは、COMポート名、ボーレート、パリティ、データビット、ストップビットを設定し、Openで接続し、WriteWriteLineで送信し、ReadLineReadExistingReadDataReceivedで受信し、最後にCloseDisposeで安全に閉じることです。

シリアル通信でトラブルが起きた場合は、C#コードだけでなく、COMポート番号、通信条件、改行コード、文字コード、USBシリアルドライバ、ケーブル、機器側の設定を順番に確認することが重要です。

安定したアプリを作るには、例外処理、タイムアウト、リトライ、ログ出力、受信バッファ管理、アプリ終了時のポート解放をしっかり実装しましょう。特にDataReceivedイベントでは、受信データが分割される前提でバッファ処理を行うことが大切です。

C#でシリアル通信を実装できるようになると、PCアプリからさまざまな外部機器を制御できるようになります。まずはコンソールアプリで接続、送信、受信を確認し、その後Windows FormsやWPFに組み込むと、実用的な通信アプリを効率よく開発できます。