C# SerialPortの使い方完全ガイド|シリアル通信の接続・送受信・エラー対策までサンプルコードで解説

はじめに

C#でArduino、PLC、バーコードリーダー、計測器、マイコン、USBシリアル変換ケーブルなどと通信する場合、よく使われるのがSerialPortクラスです。

c# serialportで調べると、接続・送信・受信のサンプルコードは多く見つかります。しかし実際の開発では、単にOpen()してReadLine()するだけではうまくいかないことも少なくありません。

たとえば、次のようなトラブルが発生します。

  • COMポートが開けない

  • 受信データが途中で切れる

  • 文字化けする

  • DataReceivedイベントが発火しない

  • UIアプリで画面更新時に例外が出る

  • USBシリアル変換ケーブルを抜いたらアプリが落ちる

この記事では、C#のSerialPortの基本から、接続、送信、受信、非同期受信、WinForms・WPFでの使い方、エラー対策、安定運用のコツまで、サンプルコード付きで解説します。

1. C#のSerialPortとは?シリアル通信の基本を押さえよう

SerialPortは、C#でシリアルポートを扱うためのクラスです。名前空間はSystem.IO.Portsで、主にRS-232C、USBシリアル変換、仮想COMポートなどを使った通信に利用されます。

Microsoft公式ドキュメントでも、SerialPortクラスはシリアルポートのファイルリソースを制御するためのクラスとして説明されており、同期I/O、イベント駆動I/O、ピン状態やブレーク状態へのアクセスなどを扱えます。Microsoft Learn+1

1-1. SerialPortでできること

C#のSerialPortを使うと、主に次のような処理ができます。

C#
using System.IO.Ports;
  • COMポートを開く

  • 通信速度を設定する

  • 文字列を送信する

  • バイト配列を送信する

  • 受信データを読み取る

  • 受信イベントを使って非同期に処理する

  • タイムアウトを設定する

  • フロー制御を設定する

  • ポート一覧を取得する

  • 通信エラーを例外として処理する

たとえば、Arduinoにコマンドを送ったり、PLCからステータスを受け取ったり、バーコードリーダーから読み取り結果を取得したりできます。

1-2. シリアル通信が使われる主な場面

シリアル通信は古くからある通信方式ですが、現在でも現場では広く使われています。

代表的な用途は次のとおりです。

  • ArduinoやRaspberry Pi Picoなどのマイコン通信

  • PLCとの通信

  • 産業用計測器との通信

  • 電子天秤からの重量データ取得

  • バーコードリーダー、QRコードリーダーからの読み取り

  • GPSモジュールからのデータ取得

  • 温度計、電圧計、オシロスコープなどの測定器制御

  • USBシリアル変換ケーブル経由のRS-232C通信

特に製造業、検査装置、IoT、組み込み開発、設備制御の分野では、C#アプリからシリアル通信を行う場面が多くあります。

1-3. C#でシリアル通信を扱うメリット

C#でSerialPortを使うメリットは、Windowsアプリや業務アプリと連携しやすい点です。

たとえば、WinFormsやWPFで画面を作成し、ボタン操作で機器にコマンドを送信し、受信結果をテキストボックスや表に表示できます。

また、C#はファイル保存、データベース連携、CSV出力、ログ管理、非同期処理なども書きやすいため、シリアル通信アプリを業務システムに組み込みやすい言語です。

1-4. SerialPortクラスの基本的な役割

SerialPortクラスの役割は、アプリケーションとシリアルデバイスの間にある通信口を制御することです。

基本的な流れは次のようになります。

C#
SerialPort serialPort = new SerialPort();

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

serialPort.Open();

serialPort.WriteLine("HELLO");

string response = serialPort.ReadLine();

serialPort.Close();

ポイントは、接続先の機器とC#側の通信設定を一致させることです。通信速度、パリティ、データビット、ストップビット、改行コードなどが一致していないと、文字化けや受信失敗の原因になります。

2. C# SerialPortを使う前に必要な準備

C#でSerialPortを使う前に、開発環境、ライブラリ、COMポート番号、ドライバなどを確認しておきましょう。

2-1. 使用する名前空間とライブラリ

SerialPortを使うには、次の名前空間を指定します。

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

.NET Frameworkでは、多くの場合そのままSystem.IO.Portsを使用できます。

一方、.NET 6、.NET 7、.NET 8以降のプロジェクトでは、環境によってSystem.IO.Portsパッケージの追加が必要になる場合があります。NuGetのSystem.IO.Portsパッケージは、同期I/O、イベント駆動I/O、ピン状態へのアクセス、BaseStream経由のストリーム利用などを提供しています。nuget.org

NuGetで追加する場合は、次のコマンドを使います。

Bash
dotnet add package System.IO.Ports

Visual Studioの場合は、次の手順で追加できます。

  1. プロジェクトを右クリック

  2. 「NuGetパッケージの管理」を選択

  3. System.IO.Portsを検索

  4. インストール

2-2. .NET Frameworkと.NET 6以降での違い

.NET Frameworkと.NET 6以降では、SerialPortの使い方自体は大きく変わりません。

ただし、次の点に注意が必要です。

項目.NET Framework.NET 6以降
主な用途WindowsデスクトップアプリWindows、Linuxなど
ライブラリ標準で使えることが多いNuGet追加が必要な場合あり
ポート名COM1、COM3などWindowsはCOM、Linuxは/dev/ttyUSB0など
UIアプリWinForms、WPFWinForms、WPF、MAUIなど
配布.NET Frameworkランタイム依存自己完結配布も可能

Windowsで開発する場合は、COM3COM5のようなポート名を指定します。Linuxでは/dev/ttyUSB0/dev/ttyACM0のようなデバイス名を使うことがあります。

2-3. COMポート番号の確認方法

WindowsでCOMポート番号を確認するには、デバイスマネージャーを使います。

手順は次のとおりです。

  1. USBシリアル変換ケーブルやArduinoをPCに接続する

  2. スタートボタンを右クリック

  3. 「デバイスマネージャー」を開く

  4. 「ポート(COMとLPT)」を展開する

  5. 表示されているCOM3COM4などを確認する

C#からポート一覧を取得することもできます。

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

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

SerialPort.GetPortNames()は、現在のコンピューターで有効なシリアルポート名の配列を取得するメソッドです。なお、返されるポート名の順序は指定されていません。Microsoft Learn+1

2-4. USBシリアル変換ケーブルを使う場合の注意点

USBシリアル変換ケーブルを使う場合は、次の点を確認してください。

  • ドライバが正しくインストールされているか

  • COMポート番号が認識されているか

  • ケーブルのチップに対応したドライバを使っているか

  • TXDとRXDの接続が逆になっていないか

  • GNDが接続されているか

  • RS-232C、TTL、RS-485など電気的仕様が合っているか

  • 通信速度やパリティが相手機器と一致しているか

特に注意したいのは、RS-232CレベルとTTLレベルの違いです。見た目が似ていても、電圧レベルが異なると通信できないだけでなく、機器を壊す可能性があります。

3. SerialPortの基本設定項目

SerialPortで安定した通信を行うには、通信相手の仕様に合わせて設定する必要があります。

代表的な設定項目は次のとおりです。

C#
SerialPort serialPort = new SerialPort
{
PortName = "COM3",
BaudRate = 9600,
Parity = Parity.None,
DataBits = 8,
StopBits = StopBits.One,
Handshake = Handshake.None,
ReadTimeout = 1000,
WriteTimeout = 1000,
Encoding = Encoding.ASCII,
NewLine = "\r\n"
};

3-1. PortName:COMポートの指定

PortNameは、接続するポート名を指定するプロパティです。

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

C#
serialPort.PortName = "COM3";

Linuxでは、環境によって次のように指定します。

C#
serialPort.PortName = "/dev/ttyUSB0";

ポート番号は固定ではありません。USBポートを差し替えたり、別の変換ケーブルを使ったりすると、COMポート番号が変わることがあります。

そのため、実用的なアプリではSerialPort.GetPortNames()で一覧を取得し、ユーザーに選択させる実装がおすすめです。

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

3-2. BaudRate:通信速度の設定

BaudRateは通信速度を指定します。

よく使われる値は次のとおりです。

  • 9600

  • 19200

  • 38400

  • 57600

  • 115200

例:

C#
serialPort.BaudRate = 9600;

相手機器が9600bpsなのにC#側を115200bpsに設定すると、正しく通信できません。必ず機器の仕様書を確認しましょう。

3-3. Parity:パリティビットの設定

Parityは、通信データの誤り検出に使われるパリティビットを指定します。

代表的な値は次のとおりです。

C#
serialPort.Parity = Parity.None;
serialPort.Parity = Parity.Even;
serialPort.Parity = Parity.Odd;

一般的にはParity.Noneがよく使われますが、PLCや産業機器ではEvenが指定されていることもあります。

3-4. DataBits:データビット数の設定

DataBitsは、1文字あたりのデータビット数です。

多くの場合は8ビットです。

C#
serialPort.DataBits = 8;

古い機器や特殊なプロトコルでは7ビットが使われることもあります。

3-5. StopBits:ストップビットの設定

StopBitsは、1文字の終わりを示すビット数です。

よく使う設定は次のとおりです。

C#
serialPort.StopBits = StopBits.One;
serialPort.StopBits = StopBits.Two;

一般的にはStopBits.Oneが多いですが、機器仕様に合わせて設定してください。

3-6. Handshake:フロー制御の設定

Handshakeは、送受信の流量を制御する設定です。

主な値は次のとおりです。

C#
serialPort.Handshake = Handshake.None;
serialPort.Handshake = Handshake.XOnXOff;
serialPort.Handshake = Handshake.RequestToSend;
serialPort.Handshake = Handshake.RequestToSendXOnXOff;

通常のマイコン通信ではHandshake.Noneがよく使われます。

一方、RS-232C機器やモデム、古い計測器などではRTS/CTS制御が必要な場合があります。その場合はHandshake.RequestToSendを指定します。

3-7. ReadTimeout・WriteTimeoutの設定

ReadTimeoutWriteTimeoutは、読み取り・書き込み時のタイムアウト時間をミリ秒で指定します。

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

タイムアウトを設定していないと、受信待ちで処理が止まってしまうことがあります。

たとえば、ReadLine()は改行コードが来るまで待ちます。相手機器が改行を送ってこない場合、タイムアウト設定がないとアプリが固まったように見えることがあります。

4. C# SerialPortで接続する基本コード

ここからは、C#でSerialPortを使って実際に接続する基本コードを見ていきます。

4-1. SerialPortインスタンスを作成する

まず、SerialPortのインスタンスを作成します。

C#
SerialPort serialPort = new SerialPort();

プロパティを個別に設定する場合は、次のように書けます。

C#
SerialPort serialPort = new SerialPort();

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

オブジェクト初期化子を使うと、より見やすく書けます。

C#
SerialPort serialPort = new SerialPort
{
PortName = "COM3",
BaudRate = 9600,
Parity = Parity.None,
DataBits = 8,
StopBits = StopBits.One,
Handshake = Handshake.None,
ReadTimeout = 1000,
WriteTimeout = 1000
};

4-2. Openメソッドでポートを開く

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

C#
serialPort.Open();

実際には例外が発生する可能性があるため、try-catchで囲むのが基本です。

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

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

ポートが開いているか確認するには、IsOpenプロパティを使います。

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

送信前や切断前にIsOpenを確認すると、不要な例外を減らせます。

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

4-4. Closeメソッドでポートを閉じる

通信が終わったら、Close()でポートを閉じます。

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

ポートを閉じ忘れると、次回接続時に「ポートが使用中」となり、開けないことがあります。

4-5. using・Disposeで安全にリソースを解放する

SerialPortは外部リソースを扱うため、使い終わったらDispose()で解放することが重要です。

コンソールアプリなどでは、usingを使うと安全です。

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

serialPort.Open();
serialPort.WriteLine("HELLO");

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

古い書き方では、次のように書けます。

C#
using (SerialPort serialPort = new SerialPort("COM3", 9600))
{
serialPort.Open();
serialPort.WriteLine("HELLO");

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

WinFormsやWPFのようにアプリ起動中ずっと接続する場合は、フォーム終了時やウィンドウ終了時にClose()Dispose()を呼び出しましょう。

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

SerialPortでデータを送信する方法には、文字列送信、改行付き送信、バイト配列送信などがあります。

5-1. Writeメソッドで文字列を送信する

Write()は、指定した文字列をそのまま送信します。

C#
serialPort.Write("HELLO");

改行は自動では付きません。

相手機器が「コマンドの末尾に改行が必要」という仕様の場合、次のように明示的に改行を付けます。

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

5-2. WriteLineメソッドで改行付きデータを送信する

WriteLine()を使うと、文字列の末尾にNewLineで指定された改行コードを付けて送信できます。

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

ArduinoのSerial.println()とやり取りする場合は、改行付きの通信が扱いやすいです。

ただし、相手機器が\nを期待しているのか、\r\nを期待しているのか、\rを期待しているのかは機器によって異なります。

5-3. バイト配列を送信する方法

バイナリプロトコルや制御コードを送る場合は、バイト配列を使います。

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

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

STX、ETX、チェックサムを使うプロトコルでは、文字列よりもバイト配列で扱う方が安全です。

5-4. 文字コードを指定して送信する方法

文字列を送信する場合、Encodingの設定が重要です。

C#
serialPort.Encoding = Encoding.ASCII;
serialPort.WriteLine("ABC123");

日本語を送信する場合は、相手機器が対応している文字コードに合わせます。

C#
serialPort.Encoding = Encoding.UTF8;
serialPort.WriteLine("こんにちは");

Shift_JISを使う場合は、.NET Core/.NET 5以降でコードページを有効化する必要がある場合があります。

C#
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

serialPort.Encoding = Encoding.GetEncoding("shift_jis");
serialPort.WriteLine("テスト");

ただし、産業機器やマイコンではASCIIのみ対応していることも多いため、日本語送信が必要かどうかは仕様書で確認しましょう。

5-5. 送信時に発生しやすいエラーと対策

送信時によくあるエラーは次のとおりです。

原因対策
ポートが開いていないIsOpenを確認してから送信する
COMポートが他アプリで使用中ターミナルソフトや他アプリを閉じる
相手機器が未接続ケーブル、電源、ドライバを確認する
改行コードが違うNewLineや送信末尾を見直す
文字コードが違うEncodingを合わせる
タイムアウトするWriteTimeoutを設定・調整する

送信処理は、必ず例外処理を入れておきましょう。

C#
try
{
if (serialPort.IsOpen)
{
serialPort.WriteLine("START");
}
}
catch (TimeoutException)
{
Console.WriteLine("送信がタイムアウトしました。");
}
catch (InvalidOperationException)
{
Console.WriteLine("ポートが開いていません。");
}
catch (Exception ex)
{
Console.WriteLine($"送信エラー: {ex.Message}");
}

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

受信方法には、同期的に読み取る方法と、イベントで非同期に受け取る方法があります。

6-1. Readメソッドでバイトデータを受信する

Read()を使うと、指定したバイト数をバッファに読み込めます。

C#
byte[] buffer = new byte[256];

int bytesRead = serialPort.Read(buffer, 0, buffer.Length);

Console.WriteLine($"受信バイト数: {bytesRead}");

受信したバイトを16進数で表示する例です。

C#
for (int i = 0; i < bytesRead; i++)
{
Console.WriteLine(buffer[i].ToString("X2"));
}

バイナリ通信では、ReadLine()ではなくRead()で処理するのが基本です。

6-2. ReadLineメソッドで1行ずつ受信する

テキストベースの通信では、ReadLine()が便利です。

C#
string line = serialPort.ReadLine();
Console.WriteLine(line);

ただし、ReadLine()は改行コードを受信するまで待ちます。改行が来ない通信ではタイムアウトするため、ReadTimeoutを設定しておきます。

C#
serialPort.ReadTimeout = 1000;

try
{
string line = serialPort.ReadLine();
Console.WriteLine(line);
}
catch (TimeoutException)
{
Console.WriteLine("受信がタイムアウトしました。");
}

6-3. ReadExistingメソッドで受信済みデータを取得する

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

C#
string data = serialPort.ReadExisting();
Console.WriteLine(data);

DataReceivedイベントの中で、受信済みのデータをまとめて取り出す用途によく使われます。

C#
private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
string data = serialPort.ReadExisting();
Console.WriteLine(data);
}

ただし、ReadExisting()は「1メッセージ単位で必ず取れる」わけではありません。通信タイミングによって、途中までしか取得できない場合や、複数メッセージがまとめて取得される場合があります。

6-4. DataReceivedイベントで非同期に受信する

DataReceivedイベントを使うと、データ受信時に処理を実行できます。

C#
serialPort.DataReceived += SerialPort_DataReceived;
serialPort.Open();

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

Microsoft公式ドキュメントでは、DataReceivedイベントはSerialPortが表すポートを通じてデータを受信したことを示すイベントと説明されています。また、このイベントはセカンダリスレッドで発生するため、UI要素を直接更新するとスレッド関連の例外が発生する可能性があります。Microsoft Learn+1

6-5. 受信データの文字化けを防ぐEncoding設定

文字化けが発生する場合は、送信側と受信側の文字コードが一致していない可能性があります。

ASCIIの例です。

C#
serialPort.Encoding = Encoding.ASCII;

UTF-8の例です。

C#
serialPort.Encoding = Encoding.UTF8;

Shift_JISの例です。

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

受信データが英数字のみの場合は、ASCIIで問題ないことが多いです。日本語や記号を含む場合は、相手機器の文字コードを確認してください。

6-6. 改行コードを正しく扱う方法

ReadLine()WriteLine()では、NewLineプロパティの設定が重要です。

C#
serialPort.NewLine = "\r\n";

よく使われる改行コードは次のとおりです。

改行コード意味主な用途
\nLFUnix系、Arduinoなど
\r\nCRLFWindows、RS-232C機器
\rCR古い機器、計測器

相手機器が\rで終端するのに、C#側が\nを待っていると、ReadLine()が終わりません。

その場合は、次のように設定します。

C#
serialPort.NewLine = "\r";
string line = serialPort.ReadLine();

7. DataReceivedイベントを使った実用的な受信処理

DataReceivedイベントは便利ですが、使い方を間違えると、データ欠損やUI例外の原因になります。

7-1. DataReceivedイベントの基本的な書き方

基本形は次のとおりです。

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

serialPort.DataReceived += SerialPort_DataReceived;
serialPort.Open();

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

より安全に書くなら、senderSerialPortにキャストします。

C#
private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
SerialPort sp = (SerialPort)sender;

string data = sp.ReadExisting();

Console.WriteLine(data);
}

7-2. イベント内でUIを直接更新してはいけない理由

WinFormsやWPFでは、画面部品を作成したUIスレッド以外から直接更新すると例外が発生することがあります。

悪い例です。

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

textBoxLog.AppendText(data); // UIスレッド以外から操作しているため危険
}

DataReceivedイベントはメインUIスレッドとは別のスレッドで発生するため、UIを更新する場合はWinFormsならInvoke、WPFならDispatcherを使います。Microsoft Learn

7-3. WinFormsでInvokeを使って画面に表示する方法

WinFormsでは、次のようにInvokeを使います。

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

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

IsHandleCreatedIsDisposedを確認すると、フォーム終了時の例外を減らせます。

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

if (!this.IsDisposed && this.IsHandleCreated)
{
this.BeginInvoke(new Action(() =>
{
textBoxLog.AppendText(data);
}));
}
}

Invokeは同期的にUIスレッドへ処理を渡します。受信頻度が高い場合は、BeginInvokeで非同期に渡す方が画面が固まりにくくなります。

7-4. WPFでDispatcherを使って画面に表示する方法

WPFでは、Dispatcher.InvokeまたはDispatcher.BeginInvokeを使います。

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

Dispatcher.Invoke(() =>
{
TextBoxLog.AppendText(data);
});
}

受信頻度が高い場合は、BeginInvokeを使います。

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

Dispatcher.BeginInvoke(new Action(() =>
{
TextBoxLog.AppendText(data);
}));
}

7-5. 受信データをバッファリングして処理する方法

DataReceivedイベントでは、1回のイベントで1メッセージ全体が受信できるとは限りません。

たとえば、相手機器が次のデータを送ったとします。

TEMP:25.4

しかし、受信側では次のように分割されることがあります。

TE
MP:25
.4

逆に、複数のメッセージがまとめて届くこともあります。

TEMP:25.4
TEMP:25.5

そのため、実用的には受信データを一度バッファにためて、改行コードなどの区切りで処理します。

C#
private readonly StringBuilder receiveBuffer = new StringBuilder();

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

lock (receiveBuffer)
{
receiveBuffer.Append(data);

string bufferText = receiveBuffer.ToString();

int newlineIndex;
while ((newlineIndex = bufferText.IndexOf('\n')) >= 0)
{
string line = bufferText.Substring(0, newlineIndex).TrimEnd('\r', '\n');

ProcessLine(line);

bufferText = bufferText.Substring(newlineIndex + 1);
}

receiveBuffer.Clear();
receiveBuffer.Append(bufferText);
}
}

private void ProcessLine(string line)
{
Console.WriteLine($"受信行: {line}");
}

このようにバッファリングすると、データが途中で切れても安全に処理できます。

8. C# SerialPortのサンプルコード

ここでは、実際に使えるSerialPortのサンプルコードを紹介します。

8-1. コンソールアプリで接続・送信・受信するサンプル

まずは、最も基本的なコンソールアプリの例です。

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

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

try
{
serialPort.Open();

Console.WriteLine("接続しました。");

serialPort.WriteLine("STATUS");
Console.WriteLine("送信: STATUS");

string response = serialPort.ReadLine();
Console.WriteLine($"受信: {response}");
}
catch (TimeoutException)
{
Console.WriteLine("タイムアウトしました。");
}
catch (UnauthorizedAccessException)
{
Console.WriteLine("COMポートにアクセスできません。他のアプリが使用中の可能性があります。");
}
catch (Exception ex)
{
Console.WriteLine($"エラー: {ex.Message}");
}
finally
{
if (serialPort.IsOpen)
{
serialPort.Close();
}

Console.WriteLine("終了しました。");
}
}
}

このコードでは、COM3に接続し、STATUSという文字列を送信して、1行分の応答を受信します。

8-2. WinFormsでシリアル通信するサンプル

WinFormsでボタンから接続・送信し、受信データをテキストボックスに表示する例です。

C#
using System;
using System.IO.Ports;
using System.Text;
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.Handshake = Handshake.None;
serialPort.Encoding = Encoding.ASCII;
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();

textBoxLog.AppendText("接続しました。\r\n");
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "接続エラー");
}
}

private void buttonSend_Click(object sender, EventArgs e)
{
try
{
if (serialPort.IsOpen)
{
serialPort.WriteLine(textBoxSend.Text);
textBoxLog.AppendText($"送信: {textBoxSend.Text}\r\n");
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "送信エラー");
}
}

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

if (!IsDisposed && IsHandleCreated)
{
BeginInvoke(new Action(() =>
{
textBoxLog.AppendText($"受信: {data}");
}));
}
}

private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
try
{
serialPort.DataReceived -= SerialPort_DataReceived;

if (serialPort.IsOpen)
{
serialPort.Close();
}

serialPort.Dispose();
}
catch
{
// 終了処理中の例外は必要に応じてログ出力する
}
}
}

WinFormsでは、DataReceivedイベント内で直接textBoxLogを更新せず、BeginInvokeでUIスレッドへ処理を渡すのがポイントです。

8-3. WPFでシリアル通信するサンプル

WPFで接続、送信、受信表示を行う例です。

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

namespace SerialPortWpfSample
{
public partial class MainWindow : Window
{
private readonly SerialPort serialPort = new SerialPort();

public MainWindow()
{
InitializeComponent();

ComboBoxPorts.ItemsSource = SerialPort.GetPortNames();

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

private void ButtonConnect_Click(object sender, RoutedEventArgs e)
{
try
{
if (!serialPort.IsOpen)
{
serialPort.PortName = ComboBoxPorts.Text;
serialPort.Open();

TextBoxLog.AppendText("接続しました。\r\n");
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "接続エラー");
}
}

private void ButtonSend_Click(object sender, RoutedEventArgs e)
{
try
{
if (serialPort.IsOpen)
{
serialPort.WriteLine(TextBoxSend.Text);
TextBoxLog.AppendText($"送信: {TextBoxSend.Text}\r\n");
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "送信エラー");
}
}

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

Dispatcher.BeginInvoke(new Action(() =>
{
TextBoxLog.AppendText($"受信: {data}");
}));
}

protected override void OnClosed(EventArgs e)
{
try
{
serialPort.DataReceived -= SerialPort_DataReceived;

if (serialPort.IsOpen)
{
serialPort.Close();
}

serialPort.Dispose();
}
catch
{
// 必要に応じてログ出力
}

base.OnClosed(e);
}
}
}

WPFでは、Dispatcher.BeginInvokeを使ってUIスレッドに処理を渡します。

8-4. バイト配列で送受信するサンプル

バイナリデータを扱う場合のサンプルです。

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 = 1000;
serialPort.WriteTimeout = 1000;

try
{
serialPort.Open();

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

Console.WriteLine("送信:");
Console.WriteLine(BitConverter.ToString(sendData));

byte[] receiveBuffer = new byte[256];
int readLength = serialPort.Read(receiveBuffer, 0, receiveBuffer.Length);

byte[] receivedData = new byte[readLength];
Array.Copy(receiveBuffer, receivedData, readLength);

Console.WriteLine("受信:");
Console.WriteLine(BitConverter.ToString(receivedData));
}
catch (TimeoutException)
{
Console.WriteLine("受信タイムアウト");
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
}

BitConverter.ToString()を使うと、受信データを02-30-31-03のような16進表記で確認できます。

8-5. 受信ログをファイル保存するサンプル

受信データをログファイルに保存する例です。

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

class SerialLogger
{
private readonly SerialPort serialPort;
private readonly string logFilePath = "serial_log.txt";
private readonly object lockObject = new object();

public SerialLogger()
{
serialPort = new SerialPort
{
PortName = "COM3",
BaudRate = 9600,
Parity = Parity.None,
DataBits = 8,
StopBits = StopBits.One,
Encoding = Encoding.ASCII,
NewLine = "\r\n"
};

serialPort.DataReceived += SerialPort_DataReceived;
}

public void Start()
{
serialPort.Open();
}

public void Stop()
{
if (serialPort.IsOpen)
{
serialPort.Close();
}

serialPort.Dispose();
}

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

string log = $"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} {data}";

lock (lockObject)
{
File.AppendAllText(logFilePath, log, Encoding.UTF8);
}
}
}

ログには時刻を付けておくと、通信タイミングや異常発生時の状況を追跡しやすくなります。

9. C# SerialPortでよくあるエラーと解決策

SerialPortを使うときによく発生するエラーと、その対策を解説します。

9-1. UnauthorizedAccessException:COMポートにアクセスできない

UnauthorizedAccessExceptionは、指定したCOMポートにアクセスできない場合に発生します。

主な原因は次のとおりです。

  • すでに別のアプリがCOMポートを使用している

  • 同じアプリ内で二重にOpen()している

  • 前回のアプリ終了時にポートを正しく閉じていない

  • 権限やドライバの問題がある

対策は次のとおりです。

C#
try
{
if (!serialPort.IsOpen)
{
serialPort.Open();
}
}
catch (UnauthorizedAccessException)
{
Console.WriteLine("COMポートにアクセスできません。他のアプリが使用中の可能性があります。");
}

Tera Term、Arduino IDEのシリアルモニタ、PLCツールなどがポートを開いていると、C#側から同じポートを開けません。

9-2. IOException:通信中にデバイスが切断された

USBシリアル変換ケーブルを抜いたり、デバイスの電源が落ちたりすると、IOExceptionが発生することがあります。

C#
try
{
serialPort.WriteLine("STATUS");
}
catch (IOException)
{
Console.WriteLine("通信中にデバイスが切断された可能性があります。");
}

対策として、送受信処理のすべてに例外処理を入れ、切断時はポートを閉じて再接続できる状態にします。

C#
private void ClosePortSafely()
{
try
{
if (serialPort.IsOpen)
{
serialPort.Close();
}
}
catch
{
// 必要に応じてログ出力
}
}

9-3. TimeoutException:読み取り・書き込みがタイムアウトする

TimeoutExceptionは、指定時間内に読み取りまたは書き込みが完了しない場合に発生します。

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

読み取りタイムアウトの例です。

C#
try
{
string line = serialPort.ReadLine();
}
catch (TimeoutException)
{
Console.WriteLine("指定時間内にデータを受信できませんでした。");
}

原因としては、次のようなものがあります。

  • 相手機器がデータを送っていない

  • 改行コードが一致していない

  • 通信速度が違う

  • ケーブル接続が間違っている

  • フロー制御が一致していない

特にReadLine()を使っている場合は、NewLineの設定を確認してください。

9-4. InvalidOperationException:ポートが開いていない

InvalidOperationExceptionは、ポートが開いていない状態で送受信しようとした場合などに発生します。

悪い例です。

C#
serialPort.WriteLine("START"); // Openしていない

対策として、送受信前にIsOpenを確認します。

C#
if (serialPort.IsOpen)
{
serialPort.WriteLine("START");
}
else
{
Console.WriteLine("ポートが開いていません。");
}

9-5. 文字化け・データ欠損が発生する

文字化けやデータ欠損が発生する場合は、次の項目を確認します。

  • BaudRateが一致しているか

  • Parityが一致しているか

  • DataBitsが一致しているか

  • StopBitsが一致しているか

  • Encodingが一致しているか

  • 改行コードが一致しているか

  • 受信処理が遅すぎないか

  • 1回のDataReceivedで1メッセージと決めつけていないか

特に、DataReceivedイベントで受信した文字列をそのまま1メッセージとして処理する実装は危険です。

C#
string data = serialPort.ReadExisting();

このdataは、メッセージの一部である可能性があります。改行、固定長、STX/ETXなど、プロトコルに応じた区切り処理を実装しましょう。

9-6. DataReceivedイベントが発火しない

DataReceivedイベントが発火しない場合は、次の点を確認します。

  • DataReceived +=でイベント登録しているか

  • Open()より前にイベント登録しているか

  • 相手機器が本当にデータを送信しているか

  • TXD/RXDが正しく接続されているか

  • GNDが接続されているか

  • BaudRateなどの通信条件が一致しているか

  • 受信データが少なすぎないか

  • ReceivedBytesThresholdの設定が高すぎないか

ReceivedBytesThresholdを設定している場合、指定バイト数に達するまでイベントが発生しないことがあります。

C#
serialPort.ReceivedBytesThreshold = 1;

まずはターミナルソフトで通信できるか確認し、その後C#アプリで試すと原因を切り分けやすくなります。

10. SerialPort通信を安定させる実装のコツ

SerialPortは簡単に使い始められますが、安定運用にはいくつかの工夫が必要です。

10-1. 例外処理を必ず入れる

シリアル通信は外部機器との通信なので、アプリ側だけで完全に制御できません。

次のような状況はいつでも起こり得ます。

  • ケーブルが抜ける

  • 機器の電源が落ちる

  • COMポート番号が変わる

  • 他アプリがポートを使用する

  • ノイズでデータが壊れる

  • 応答が返ってこない

そのため、接続、送信、受信、切断のすべてに例外処理を入れます。

C#
try
{
serialPort.Open();
serialPort.WriteLine("STATUS");
string response = serialPort.ReadLine();
}
catch (UnauthorizedAccessException ex)
{
Console.WriteLine($"アクセスエラー: {ex.Message}");
}
catch (TimeoutException ex)
{
Console.WriteLine($"タイムアウト: {ex.Message}");
}
catch (IOException ex)
{
Console.WriteLine($"IOエラー: {ex.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"その他のエラー: {ex.Message}");
}
finally
{
if (serialPort.IsOpen)
{
serialPort.Close();
}
}

10-2. 接続前にポート一覧を取得する

COMポート番号を固定で書くと、環境が変わったときに動かなくなります。

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

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

実用アプリでは、コンボボックスに表示してユーザーに選択させるのがおすすめです。

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

10-3. 通信中の切断を検知する

USBシリアル変換ケーブルは、通信中に抜かれることがあります。

完全にリアルタイムで切断を検知するのは難しい場合がありますが、送信時や受信時の例外を捕捉することで異常を検知できます。

C#
try
{
serialPort.WriteLine("PING");
}
catch (IOException)
{
Console.WriteLine("デバイスが切断された可能性があります。");
ClosePortSafely();
}
catch (InvalidOperationException)
{
Console.WriteLine("ポートが閉じています。");
}

また、定期的に確認コマンドを送るハートビート処理を入れると、応答がない機器を検知しやすくなります。

10-4. 送受信処理をUIスレッドから分離する

WinFormsやWPFで、ボタンクリック内に重い受信処理を書くと、画面が固まります。

悪い例です。

C#
private void buttonRead_Click(object sender, EventArgs e)
{
string line = serialPort.ReadLine(); // 応答がないとUIが止まる
textBoxLog.AppendText(line);
}

ReadLine()のように待ちが発生する処理は、別スレッド、Task.Run()、またはDataReceivedイベントで処理します。

C#
private async void buttonRead_Click(object sender, EventArgs e)
{
try
{
string line = await Task.Run(() => serialPort.ReadLine());
textBoxLog.AppendText(line + Environment.NewLine);
}
catch (TimeoutException)
{
textBoxLog.AppendText("タイムアウトしました。" + Environment.NewLine);
}
}

10-5. ログ出力で通信内容を確認する

通信トラブルを解決するには、ログが非常に重要です。

最低限、次の情報を記録すると原因を追いやすくなります。

  • 接続日時

  • 切断日時

  • 送信データ

  • 受信データ

  • エラー内容

  • COMポート名

  • 通信設定

  • 受信データの16進表記

例です。

C#
private void WriteLog(string message)
{
string log = $"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} {message}";
File.AppendAllText("serial.log", log + Environment.NewLine, Encoding.UTF8);
}

送受信時にログを出力します。

C#
serialPort.WriteLine("STATUS");
WriteLog("送信: STATUS");

string response = serialPort.ReadLine();
WriteLog("受信: " + response);

バイナリ通信では、16進数ログが便利です。

C#
WriteLog("受信HEX: " + BitConverter.ToString(buffer, 0, bytesRead));

10-6. 終了時に必ずClose・Disposeする

アプリ終了時にポートを閉じないと、次回起動時にCOMポートが使用中になることがあります。

WinFormsではFormClosingで解放します。

C#
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
serialPort.DataReceived -= SerialPort_DataReceived;

if (serialPort.IsOpen)
{
serialPort.Close();
}

serialPort.Dispose();
}

WPFではOnClosedで解放します。

C#
protected override void OnClosed(EventArgs e)
{
serialPort.DataReceived -= SerialPort_DataReceived;

if (serialPort.IsOpen)
{
serialPort.Close();
}

serialPort.Dispose();

base.OnClosed(e);
}

終了時は、イベント解除、Close()Dispose()の順で処理すると安全です。

11. C# SerialPortのよくある質問

最後に、C#のSerialPortでよくある質問をまとめます。

11-1. SerialPortは.NET 8でも使える?

はい、.NET 8でもSerialPortは利用できます。

ただし、プロジェクトの種類や環境によっては、NuGetでSystem.IO.Portsパッケージを追加する必要があります。NuGetのSystem.IO.Portsパッケージは、シリアルポートファイルリソース、同期I/O、イベント駆動I/O、BaseStream経由のストリーム利用などを提供しています。nuget.org

追加コマンドは次のとおりです。

Bash
dotnet add package System.IO.Ports

WindowsではCOM3のようなポート名を使います。Linuxでは/dev/ttyUSB0/dev/ttyACM0のようなデバイス名を指定する場合があります。

11-2. COMポートの一覧を取得するには?

SerialPort.GetPortNames()を使います。

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

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

WinFormsのコンボボックスに表示する例です。

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

WPFでコンボボックスに表示する例です。

C#
ComboBoxPorts.ItemsSource = SerialPort.GetPortNames();

11-3. ArduinoやPLCとの通信にも使える?

はい、使えます。

Arduinoの場合は、USB接続すると仮想COMポートとして認識されることが多く、C#のSerialPortから接続できます。

Arduino側の例です。

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

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

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

C#側の例です。

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

serialPort.NewLine = "\n";
serialPort.Open();

serialPort.WriteLine("LED_ON");

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

PLCの場合も、通信仕様がシリアル通信で公開されていれば利用できます。ただし、PLCでは独自プロトコル、Modbus RTU、チェックサム、BCC、STX/ETXなどが必要になることがあります。

11-4. 複数のSerialPortを同時に使える?

はい、複数のSerialPortインスタンスを作成すれば、複数のCOMポートを同時に扱えます。

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

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

port1.WriteLine("STATUS1");
port2.WriteLine("STATUS2");

ただし、同じCOMポートを複数のSerialPortインスタンスで同時に開くことはできません。

また、複数ポートを扱う場合は、受信イベント、ログ、例外処理、切断処理をポートごとに分けて管理する必要があります。

11-5. 受信データが途中で切れる場合はどうすればよい?

受信データが途中で切れる場合、まず「途中で切れている」のではなく「分割されて届いている」可能性を疑いましょう。

シリアル通信では、送信側が1回で送ったデータを、受信側が1回でまとめて受け取れるとは限りません。

対策は次のとおりです。

  • 改行コードまでバッファにためる

  • 固定長になるまでバッファにためる

  • STX/ETXなどの開始・終了コードで判定する

  • チェックサムでデータの正当性を確認する

  • DataReceivedの1回分を1メッセージとして扱わない

改行区切りの例です。

C#
private readonly StringBuilder buffer = new StringBuilder();

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

lock (buffer)
{
buffer.Append(received);

string text = buffer.ToString();

int index;
while ((index = text.IndexOf('\n')) >= 0)
{
string line = text.Substring(0, index).TrimEnd('\r', '\n');

HandleMessage(line);

text = text.Substring(index + 1);
}

buffer.Clear();
buffer.Append(text);
}
}

private void HandleMessage(string message)
{
Console.WriteLine($"メッセージ: {message}");
}

このように、受信データをいったんバッファリングしてからメッセージ単位に分解することで、データ欠損のように見える問題を防ぎやすくなります。

まとめ

C#のSerialPortを使うと、Arduino、PLC、計測器、バーコードリーダー、USBシリアル変換ケーブルなど、さまざまな機器とシリアル通信できます。

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

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

serialPort.Open();

serialPort.WriteLine("HELLO");

string response = serialPort.ReadLine();

serialPort.Close();

ただし、実用的なアプリでは、これだけでは不十分です。

安定したシリアル通信を実装するには、次のポイントを押さえる必要があります。

  • PortNameBaudRateParityDataBitsStopBitsを相手機器に合わせる

  • ReadTimeoutWriteTimeoutを設定する

  • 送信時は改行コードと文字コードを確認する

  • 受信時はReadLine()Read()ReadExisting()を使い分ける

  • DataReceivedイベントではUIを直接更新しない

  • WinFormsではInvoke、WPFではDispatcherを使う

  • 受信データはバッファリングして処理する

  • 例外処理を必ず入れる

  • 終了時にClose()Dispose()を呼ぶ

  • ログを出力して通信内容を確認する

特に重要なのは、DataReceivedイベントの1回分を1メッセージとして扱わないことです。シリアル通信ではデータが分割されて届くことも、まとめて届くこともあります。改行、固定長、STX/ETXなど、通信プロトコルに合わせて正しく区切り処理を実装しましょう。

c# serialportの基本を理解すれば、PCアプリから外部機器を制御したり、計測データを取得したり、業務システムと現場機器を連携したりできます。まずはコンソールアプリで接続・送受信を確認し、その後WinFormsやWPFに組み込む流れで進めると、トラブルを切り分けながら安全に開発できます。