C#でシリアル通信を始める方法|SerialPortの接続・送受信・エラー対処をサンプル付きで解説

はじめに

C#でシリアル通信を行う場合、最もよく使われるのがSystem.IO.Ports.SerialPortクラスです。シリアル通信は、計測器、PLC、バーコードリーダー、Arduino、マイコン、電子天秤、温度計、USBシリアル変換ケーブルなど、さまざまな機器とのデータ送受信に使われます。

この記事では、「C# シリアル」というキーワードで調べている方に向けて、SerialPortを使った接続、送信、受信、エラー対処、実用的なサンプルコードまでをまとめて解説します。

C#でシリアル通信を始めるときは、単にコードを書くだけでなく、COMポート番号、ボーレート、パリティ、改行コード、文字コード、配線、他アプリとの競合なども確認する必要があります。この記事を読みながら順番に確認すれば、基本的なシリアル通信アプリを作成できるようになります。

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

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

シリアル通信とは、データを1ビットずつ順番に送受信する通信方式です。PCと外部機器を接続して、文字列やバイナリデータをやり取りするときによく使われます。

Windowsでは、シリアル通信の接続先は一般的にCOM1COM2COM3のようなCOMポート名で扱われます。USB接続の機器であっても、内部的にUSBシリアル変換として認識される場合は、Windows上ではCOMポートとして表示されます。

RS-232Cは、シリアル通信で使われる代表的な物理インターフェースの規格です。現在はPCにRS-232C端子が直接付いていないことも多いため、USB-RS232C変換ケーブルやUSBシリアル変換モジュールを使うケースが一般的です。

C#でシリアル通信を行う場合は、物理的な接続方式に関係なく、OSがCOMポートとして認識していればSerialPortクラスから通信できます。

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

C#のシリアル通信は、業務アプリや制御アプリでよく使われます。代表的な用途は次のとおりです。

・ArduinoやRaspberry Pi Picoなどのマイコンとの通信
・PLCや制御装置とのデータ送受信
・バーコードリーダーから読み取りデータを取得
・電子天秤、温度計、測定器から計測値を取得
・モーターコントローラーやリレー制御装置へのコマンド送信
・ログデータやセンサーデータの収集
・RS-232C機器の制御アプリ作成

たとえば、計測器に"READ"というコマンドを送信し、返ってきた測定値をC#アプリで表示・保存するような処理ができます。

1-3. SerialPortクラスとは

SerialPortクラスは、C#でシリアルポートを扱うためのクラスです。ポート名、ボーレート、パリティ、データビット、ストップビットなどを設定し、ポートを開いてデータを送受信できます。

主に次のような操作ができます。

・COMポートを開く
・COMポートを閉じる
・文字列を送信する
・バイト配列を送信する
・受信データを読み取る
・受信イベントを使って非同期に処理する
・タイムアウトを設定する
・利用可能なCOMポート一覧を取得する

C#でシリアル通信を実装するときは、まずSerialPortの基本的な使い方を理解することが重要です。

1-4. .NET Framework/.NET 6以降で使うときの違い

.NET FrameworkのWindowsアプリでは、System.IO.Ports名前空間を使うことでSerialPortを利用できます。Windows Formsアプリやコンソールアプリでよく使われてきました。

一方、.NET 6、.NET 7、.NET 8以降のプロジェクトでは、環境によってSystem.IO.Portsパッケージの追加が必要になる場合があります。Visual StudioでSerialPortが見つからない場合は、NuGetからSystem.IO.Portsをインストールしてください。

.NET Frameworkの場合の例です。

C#
using System.IO.Ports;

.NET 6以降でコンパイルエラーになる場合は、NuGetパッケージマネージャーから次のパッケージを追加します。

System.IO.Ports

Windows向けのシリアル通信アプリを作る場合は、.NET Frameworkでも.NET 6以降でも基本的な考え方はほぼ同じです。ただし、プロジェクト形式やパッケージ参照の有無が異なるため、環境に合わせて確認しましょう。

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

2-1. Visual Studioでプロジェクトを作成する

C#でシリアル通信を試す場合、最初はコンソールアプリで動作確認するのがおすすめです。画面処理が少なく、送受信の流れを理解しやすいからです。

Visual Studioで次のようにプロジェクトを作成します。

  1. Visual Studioを起動する

  2. 「新しいプロジェクトの作成」を選択する

  3. 「コンソール アプリ」を選択する

  4. 言語はC#を選択する

  5. プロジェクト名を入力する

  6. 使用する.NETのバージョンを選択する

Windows Formsで画面付きアプリを作りたい場合は、「Windows フォーム アプリ」を選択します。最初からUIアプリにすると、スレッド処理やイベント処理でつまずきやすいため、初心者はコンソールアプリで基本を確認してからWindows Formsに進むとよいでしょう。

2-2. System.IO.Portsを追加する

C#でSerialPortを使うには、コードの先頭に次のusingを追加します。

C#
using System.IO.Ports;

もしSerialPortが認識されない場合は、NuGetパッケージを追加します。

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

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

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

  3. 「参照」タブでSystem.IO.Portsを検索

  4. パッケージをインストール

または、パッケージマネージャーコンソールで次のように実行します。

PowerShell
Install-Package System.IO.Ports

.NET CLIを使う場合は次のコマンドです。

Bash
dotnet add package System.IO.Ports

2-3. 使用するCOMポートを確認する方法

シリアル通信では、まず接続先機器がどのCOMポートとして認識されているかを確認する必要があります。

Windowsの場合は、次の手順で確認できます。

  1. デバイスマネージャーを開く

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

  3. 接続した機器のCOM番号を確認する

たとえば、「USB Serial Port (COM5)」と表示されていれば、C#ではCOM5を指定します。

C#から利用可能なCOMポート一覧を取得することもできます。

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

class Program
{
static void Main()
{
string[] ports = SerialPort.GetPortNames();

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

COMポート番号は、USBを差し替えたり、ドライバーを再インストールしたりすると変わることがあります。動作しない場合は、最初にCOMポート番号を確認しましょう。

2-4. 接続先機器の通信条件を確認する

シリアル通信では、PC側と機器側の通信条件が一致していないと正しく通信できません。

確認すべき主な項目は次のとおりです。

・COMポート名
・ボーレート
・パリティ
・データビット
・ストップビット
・フロー制御
・改行コード
・文字コード
・送信コマンドの形式
・応答データの形式

たとえば、機器側が「9600bps、データビット8、パリティなし、ストップビット1」の設定であれば、C#側も同じ設定にする必要があります。

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

BaudRate: 9600
DataBits: 8
Parity: None
StopBits: One
Handshake: None

ただし、機器によって設定は異なります。必ず接続先機器のマニュアルを確認してください。

2-5. Tera Termなどで事前に通信確認しておく

C#のコードを書く前に、Tera Termなどのターミナルソフトで通信できるか確認しておくと、問題の切り分けが簡単になります。

Tera Termで通信できる場合は、配線、COMポート、ドライバー、機器側の設定はおおむね正しいと判断できます。その状態でC#アプリだけ通信できない場合は、コードやSerialPortの設定に原因がある可能性が高くなります。

逆に、Tera Termでも通信できない場合は、C#のコード以前に、次のような問題が考えられます。

・COMポート番号が間違っている
・ボーレートが違う
・TX/RXの配線が間違っている
・ドライバーが入っていない
・機器の電源が入っていない
・別アプリがCOMポートを使用している
・機器側がコマンド待ち状態ではない

シリアル通信では、C#のコードだけでなく、周辺環境の確認も非常に重要です。

3. SerialPortの基本設定項目

3-1. PortName:COMポート名

PortNameは、使用するCOMポート名を指定するプロパティです。

C#
serialPort.PortName = "COM3";

WindowsではCOM1COM2COM10のように指定します。デバイスマネージャーで表示されているCOM番号と一致させてください。

存在しないCOMポートを指定すると、Open時に例外が発生します。また、別のアプリが同じCOMポートを使用している場合も開けません。

3-2. BaudRate:ボーレート

BaudRateは、1秒あたりの通信速度を指定します。単位はbpsです。

C#
serialPort.BaudRate = 9600;

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

9600
19200
38400
57600
115200

PC側と機器側のボーレートが一致していないと、文字化けしたり、正常に受信できなかったりします。

3-3. Parity:パリティ

Parityは、通信エラー検出用のパリティビットを指定します。

C#
serialPort.Parity = Parity.None;

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

C#
Parity.None
Parity.Odd
Parity.Even

一般的にはParity.Noneが多いですが、産業機器や古いRS-232C機器ではEvenOddが指定されていることもあります。接続先機器の仕様に合わせて設定します。

3-4. DataBits:データビット

DataBitsは、1文字あたりのデータビット数を指定します。

C#
serialPort.DataBits = 8;

多くの機器では8が使われます。古い機器や特殊な通信では7が指定される場合もあります。

一般的な設定は「8ビット、パリティなし、ストップビット1」で、よく「8N1」と表記されます。

8N1 = DataBits 8 / Parity None / StopBits 1

3-5. StopBits:ストップビット

StopBitsは、1文字の終わりを示すストップビットを指定します。

C#
serialPort.StopBits = StopBits.One;

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

C#
StopBits.One
StopBits.Two

通常はStopBits.Oneです。機器の仕様が2になっている場合は、C#側もStopBits.Twoにします。

3-6. Handshake:フロー制御

Handshakeは、データの送受信タイミングを制御するフロー制御の設定です。

C#
serialPort.Handshake = Handshake.None;

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

C#
Handshake.None
Handshake.XOnXOff
Handshake.RequestToSend
Handshake.RequestToSendXOnXOff

通常のUSBシリアル通信やマイコン通信ではHandshake.Noneが多いです。RS-232C機器でRTS/CTS制御を使う場合はRequestToSendを指定します。

フロー制御の設定が合っていないと、送信できない、受信できない、途中で止まるといった問題が起きることがあります。

3-7. ReadTimeout/WriteTimeout:タイムアウト設定

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

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

タイムアウトを設定しておくと、機器から応答が返ってこない場合に処理が止まり続けるのを防げます。

たとえば、ReadLineで受信待ちをしているとき、指定時間内に改行が来なければTimeoutExceptionが発生します。

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

実用アプリでは、タイムアウトを設定したうえで、リトライやエラーメッセージ表示を行うのが基本です。

3-8. NewLineとEncodingの設定

NewLineは、ReadLineWriteLineで使用する改行コードを指定します。

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

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

\r\n  CRLF
\n LF
\r CR

接続先機器によって、コマンドの終端に必要な改行コードが異なります。たとえば、コマンド末尾にCRだけが必要な機器もあります。

Encodingは、文字列のエンコードを指定します。

C#
serialPort.Encoding = System.Text.Encoding.ASCII;

日本語を扱う場合はShift_JISUTF-8が必要になることもあります。

C#
serialPort.Encoding = System.Text.Encoding.UTF8;

文字化けする場合は、ボーレートだけでなく、Encodingの違いも確認しましょう。

4. C#でSerialPortに接続・切断する方法

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

SerialPortを使うには、まずインスタンスを作成します。

C#
SerialPort serialPort = new SerialPort();

作成後に各プロパティを設定します。

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

コンストラクターでまとめて指定することもできます。

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

どちらの書き方でも問題ありません。設定項目が多い場合は、プロパティで明示的に書くと読みやすくなります。

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

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

C#
serialPort.Open();

ポートが正常に開くと、送受信できる状態になります。

ただし、次のような場合は例外が発生します。

・指定したCOMポートが存在しない
・別のアプリがCOMポートを使用している
・ポート名が不正
・アクセス権限がない
・USBシリアル機器が取り外されている

そのため、実用コードではtry-catchで例外処理を入れるようにします。

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

IsOpenプロパティを使うと、シリアルポートが開いているか確認できます。

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

送信や受信を行う前にIsOpenを確認しておくと、未接続状態でアクセスしてInvalidOperationExceptionが発生するのを防ぎやすくなります。

4-4. Close/Disposeで安全に切断する

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

C#
serialPort.Close();

Disposeを使うと、内部リソースも解放できます。

C#
serialPort.Dispose();

usingを使うと、処理終了時に自動でDisposeされます。

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

Windows FormsアプリなどでSerialPortをフィールドとして保持する場合は、フォーム終了時に必ず閉じるようにしましょう。

4-5. 接続・切断のサンプルコード

以下は、C#でSerialPortに接続し、安全に切断する基本サンプルです。

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

class Program
{
static void Main()
{
using 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 = 3000;
serialPort.WriteTimeout = 3000;

try
{
serialPort.Open();

if (serialPort.IsOpen)
{
Console.WriteLine("シリアルポートを開きました。");
}
}
catch (UnauthorizedAccessException)
{
Console.WriteLine("ポートが他のアプリで使用中、またはアクセスできません。");
}
catch (IOException ex)
{
Console.WriteLine("I/Oエラーが発生しました: " + ex.Message);
}
catch (ArgumentException ex)
{
Console.WriteLine("ポート名または設定値が不正です: " + ex.Message);
}
finally
{
if (serialPort.IsOpen)
{
serialPort.Close();
Console.WriteLine("シリアルポートを閉じました。");
}
}
}
}

まずはこのコードで、指定したCOMポートを開けるか確認するとよいでしょう。

5. C#でシリアル通信のデータを送信する方法

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

文字列を送信する場合は、Writeメソッドを使います。

C#
serialPort.Write("HELLO");

Writeは指定した文字列をそのまま送信します。改行コードは自動で付きません。接続先機器がコマンドの最後に改行を必要とする場合は、自分で追加します。

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

機器によっては、コマンド末尾にCRだけが必要な場合もあります。

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

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

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

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

上記の場合、実際にはREAD\r\nが送信されます。

改行コードが機器仕様と合っていないと、機器がコマンドを認識しないことがあります。WriteLineを使う場合は、必ずNewLineの設定を確認しましょう。

5-3. バイト配列でバイナリデータを送信する

文字列ではなく、バイナリデータを送信する場合は、バイト配列を使います。

C#
byte[] data = { 0x01, 0x02, 0x03, 0x04 };
serialPort.Write(data, 0, data.Length);

バイナリプロトコルの機器では、コマンド、データ長、チェックサム、終端コードなどをバイト単位で組み立てて送信します。

文字列送信ではEncodingの影響を受けるため、バイナリ通信ではbyte[]で送信する方が安全です。

5-4. STX・ETXなど制御コードを含む送信方法

シリアル通信では、STXやETXなどの制御コードを使うことがあります。

STX = 0x02
ETX = 0x03
CR = 0x0D
LF = 0x0A

たとえば、STXで開始し、ETXで終了するコマンドを送信する場合は次のように書けます。

C#
byte STX = 0x02;
byte ETX = 0x03;

byte[] command = new byte[]
{
STX,
(byte)'R',
(byte)'E',
(byte)'A',
(byte)'D',
ETX
};

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

ASCII文字列と制御コードを組み合わせる場合は、次のように書くこともできます。

C#
byte[] body = System.Text.Encoding.ASCII.GetBytes("READ");
byte[] command = new byte[body.Length + 2];

command[0] = 0x02;
Array.Copy(body, 0, command, 1, body.Length);
command[command.Length - 1] = 0x03;

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

5-5. 送信処理のサンプルコード

以下は、C#でシリアルポートを開き、文字列コマンドを送信するサンプルです。

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();

serialPort.WriteLine("READ");

Console.WriteLine("送信しました。");
}
catch (TimeoutException)
{
Console.WriteLine("送信タイムアウトが発生しました。");
}
catch (Exception ex)
{
Console.WriteLine("エラー: " + ex.Message);
}
}
}

送信できない場合は、COMポート番号、ボーレート、フロー制御、機器のコマンド仕様、改行コードを確認してください。

6. C#でシリアル通信のデータを受信する方法

6-1. ReadExistingで受信データを読み取る

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

C#
string data = serialPort.ReadExisting();

現在受信済みのデータをまとめて取得できるため、DataReceivedイベント内で使われることがあります。

ただし、ReadExistingは「その時点でバッファにあるデータ」を読むだけです。1つのメッセージ全体が必ず取得できるとは限りません。データが途中で分割されて届くこともあるため、受信データを一時バッファにためて、終端文字や固定長で区切る設計が必要です。

6-2. ReadLineで1行ずつ受信する

ReadLineは、NewLineで指定された改行コードまでのデータを1行として読み取ります。

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

機器が必ず改行コード付きで応答する場合は、ReadLineが便利です。

ただし、改行コードが届かない場合は、タイムアウトまで待機します。そのため、ReadTimeoutを設定しておきましょう。

C#
serialPort.ReadTimeout = 3000;

try
{
string line = serialPort.ReadLine();
Console.WriteLine("受信: " + line);
}
catch (TimeoutException)
{
Console.WriteLine("受信タイムアウトです。");
}

6-3. Readメソッドでバイト単位に受信する

バイナリデータを受信する場合は、Readメソッドを使います。

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

戻り値には、実際に読み取ったバイト数が入ります。

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

バイナリ通信では、文字列に変換せず、受信したバイト列をそのまま解析するのが基本です。チェックサムやデータ長があるプロトコルでは、受信バッファにデータをためながら解析します。

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

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

C#
serialPort.DataReceived += SerialPort_DataReceived;

イベントハンドラーの例です。

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

Console.WriteLine("受信: " + data);
}

DataReceivedイベントは、UIスレッドとは別のスレッドで発生します。Windows FormsやWPFで画面を更新する場合は、そのままコントロールを操作せず、InvokeBeginInvokeを使ってUIスレッドに処理を渡す必要があります。

また、DataReceivedイベントは「1メッセージ受信完了」を意味するとは限りません。途中までのデータでイベントが発生することもあるため、受信データの区切りを考慮した実装が必要です。

6-5. 受信データを画面に表示するサンプルコード

コンソールアプリでDataReceivedイベントを使い、受信データを表示するサンプルです。

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

class Program
{
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 != null && serialPort.IsOpen)
{
serialPort.Close();
}
}
}

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

Console.WriteLine("受信: " + data);
}
}

このサンプルは簡単に動作確認できますが、実用アプリでは受信データをバッファにためて、改行や終端コードで1件分に分割する処理を追加しましょう。

6-6. UIスレッド更新時のInvoke/BeginInvokeの使い方

Windows FormsでDataReceivedイベントからTextBoxを更新する場合、直接次のように書くとエラーになることがあります。

C#
textBoxLog.AppendText(data);

DataReceivedイベントはUIスレッド以外で実行されるためです。正しくはBeginInvokeを使います。

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

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

InvokeはUIスレッドで処理が終わるまで待機し、BeginInvokeは非同期に処理を依頼します。ログ表示のような用途ではBeginInvokeが使いやすいです。

7. C#シリアル通信の実用サンプルコード

7-1. コンソールアプリで送受信するサンプル

以下は、C#のコンソールアプリでシリアル通信の送受信を行う基本サンプルです。

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("接続しました。");
Console.Write("送信する文字列を入力してください: ");

string? command = Console.ReadLine();

if (!string.IsNullOrEmpty(command))
{
serialPort.WriteLine(command);
Console.WriteLine("送信しました。");

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

コマンドを送信し、改行付きの応答を1行受信する構成です。測定器やマイコンが「1コマンドに対して1行応答」を返す場合に使いやすい形です。

7-2. Windows Formsで送受信するサンプル

Windows Formsで、ボタンを押して送信し、受信データをTextBoxに表示する例です。

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

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

public Form1()
{
InitializeComponent();

serialPort.PortName = "COM3";
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.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");
}
else
{
MessageBox.Show("シリアルポートが開いていません。");
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}

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

BeginInvoke(new Action(() =>
{
textBoxLog.AppendText("受信: " + data + "\r\n");
}));
}

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

serialPort.Dispose();
}
}

Windows Formsでは、DataReceivedイベント内で直接UIを更新しないことが重要です。必ずInvokeまたはBeginInvokeを使いましょう。

7-3. COMポート一覧を取得して選択するサンプル

アプリ起動時に使用可能なCOMポート一覧を取得し、ComboBoxに表示する例です。

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

private void Form1_Load(object sender, EventArgs e)
{
comboBoxPorts.Items.Clear();

string[] ports = SerialPort.GetPortNames();
Array.Sort(ports);

foreach (string port in ports)
{
comboBoxPorts.Items.Add(port);
}

if (comboBoxPorts.Items.Count > 0)
{
comboBoxPorts.SelectedIndex = 0;
}
}

選択されたCOMポートを使って接続する例です。

C#
private void buttonConnect_Click(object sender, EventArgs e)
{
if (comboBoxPorts.SelectedItem == null)
{
MessageBox.Show("COMポートを選択してください。");
return;
}

serialPort.PortName = comboBoxPorts.SelectedItem.ToString();
serialPort.BaudRate = 9600;

try
{
serialPort.Open();
MessageBox.Show("接続しました。");
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}

COMポート番号を固定で書くよりも、ユーザーが選択できるようにしておくと実用性が上がります。

7-4. Arduinoやマイコンと通信するサンプル

Arduino側でシリアル通信を行う簡単な例です。

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

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

if (command == "LED_ON")
{
Serial.println("OK:LED_ON");
}
else if (command == "LED_OFF")
{
Serial.println("OK:LED_OFF");
}
else
{
Serial.println("ERROR:UNKNOWN_COMMAND");
}
}
}

C#側からArduinoへコマンドを送信する例です。

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

class Program
{
static void Main()
{
using SerialPort serialPort = new SerialPort("COM3", 9600);
serialPort.NewLine = "\n";
serialPort.ReadTimeout = 3000;

try
{
serialPort.Open();

serialPort.WriteLine("LED_ON");

string response = serialPort.ReadLine();
Console.WriteLine("Arduinoからの応答: " + response);
}
catch (TimeoutException)
{
Console.WriteLine("Arduinoから応答がありません。");
}
catch (Exception ex)
{
Console.WriteLine("エラー: " + ex.Message);
}
}
}

Arduinoでは改行コードが\nで処理されることが多いため、C#側のNewLine\nに合わせています。通信できない場合は、Arduino IDEのシリアルモニターで先に動作確認すると切り分けしやすくなります。

7-5. 受信ログをテキストファイルに保存するサンプル

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

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

class Program
{
static SerialPort? serialPort;
static readonly string logFilePath = "serial_log.txt";
static readonly object lockObj = new object();

static void Main()
{
serialPort = new SerialPort("COM3", 9600, Parity.None, 8, StopBits.One);
serialPort.DataReceived += SerialPort_DataReceived;

try
{
serialPort.Open();
Console.WriteLine("ログ記録を開始しました。Enterで終了します。");
Console.ReadLine();
}
finally
{
if (serialPort.IsOpen)
{
serialPort.Close();
}
}
}

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

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

lock (lockObj)
{
File.AppendAllText(logFilePath, log + Environment.NewLine);
}

Console.WriteLine(log);
}
}

ログを保存しておくと、通信内容の確認、エラー原因の調査、機器側の応答分析に役立ちます。実用アプリでは、送信ログと受信ログの両方を残すとさらに便利です。

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

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

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

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

・Tera Termなど他のアプリが同じCOMポートを開いている
・別の自作アプリがポートを開いたままになっている
・前回のアプリ終了時にCloseされていない
・アクセス権限の問題がある

対処法としては、まず他のターミナルソフトやアプリを終了します。それでも解決しない場合は、USBシリアル機器を抜き差しする、アプリを再起動する、PCを再起動するなどを試します。

C#
catch (UnauthorizedAccessException)
{
Console.WriteLine("COMポートが使用中です。他のアプリを閉じてください。");
}

8-2. IOException:接続先が見つからない・切断された

IOExceptionは、入出力処理中に問題が発生したときに出ることがあります。

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

・USBシリアルケーブルが抜けた
・接続先機器の電源が切れた
・ドライバーが正常に動作していない
・ポートを開いた後に機器が切断された
・通信中にハードウェアエラーが発生した

USB機器は、接続が不安定だと通信中にCOMポートが消えることがあります。長時間動作させるアプリでは、IOExceptionを想定して、再接続処理やエラーログ出力を実装しておくと安心です。

8-3. TimeoutException:応答が返ってこない

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

よくある原因は次のとおりです。

・機器が応答を返していない
・送信コマンドが間違っている
・改行コードが違う
・ReadLineで待っているが改行が届いていない
・ボーレートなど通信条件が違う
・配線が間違っている

たとえばReadLineは、NewLineで指定した改行コードを受信するまで待ちます。機器が改行なしで応答する場合、データ自体は届いていてもReadLineは完了しません。この場合はReadExistingReadを使うか、終端条件を見直します。

8-4. ArgumentException:COMポート名や設定値が不正

ArgumentExceptionは、SerialPortに不正な設定値を指定した場合に発生することがあります。

例として、次のようなケースがあります。

・ポート名が空文字
・COMポート名の形式が不正
・DataBitsに範囲外の値を指定している
・StopBits.Noneなど不適切な値を指定している

C#
serialPort.PortName = "";

このような設定はエラーの原因になります。ユーザー入力からCOMポート名を受け取る場合は、空文字や不正値をチェックしましょう。

8-5. InvalidOperationException:ポート未接続のまま送受信している

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

C#
serialPort.WriteLine("READ");

上記の前にserialPort.Open()が呼ばれていなければ、エラーになります。

送信前には次のように確認します。

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

画面アプリでは、接続ボタンを押す前に送信ボタンが押されることがあります。未接続時は送信ボタンを無効化するなど、UI側でも対策するとよいでしょう。

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

DataReceivedイベントが発生しない場合、次のような原因が考えられます。

・イベントハンドラーを登録していない
・ポートをOpenしていない
・機器からデータが送信されていない
・COMポート番号が違う
・通信条件が合っていない
・配線が間違っている
・ReceivedBytesThresholdの設定が大きすぎる
・別アプリがポートを使用している

イベント登録の例です。

C#
serialPort.DataReceived += SerialPort_DataReceived;

また、DataReceivedイベントは必ず1バイトごとに発生するわけではありません。受信タイミングやバッファ状態によって発生するため、「イベントが発生したら1メッセージが完成している」と考えないようにしましょう。

9. シリアル通信がうまくいかないときの対処法

9-1. COMポート番号が正しいか確認する

C#のシリアル通信で最初に確認すべきなのは、COMポート番号です。

デバイスマネージャーで表示されているCOM番号と、コードで指定しているPortNameが一致しているか確認します。

C#
serialPort.PortName = "COM5";

USBポートを差し替えるとCOM番号が変わることがあります。昨日はCOM3だった機器が、今日はCOM5になっていることもあります。

アプリ側でCOMポート一覧を表示し、ユーザーが選択できるようにしておくと、運用時のトラブルを減らせます。

9-2. ボーレートなど通信条件を合わせる

COMポート番号が正しくても、通信条件が合っていないと正常に通信できません。

確認する項目は次のとおりです。

・BaudRate
・Parity
・DataBits
・StopBits
・Handshake

特にボーレートが違うと、文字化けしたり、意味のないデータが受信されたりします。

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

機器のマニュアルに「9600bps, 8bit, No parity, 1 stop bit」と書かれていれば、C#側も同じ設定にします。

9-3. TX/RXの配線やUSB変換ケーブルを確認する

マイコンやRS-232C機器と接続する場合は、配線も重要です。

UART通信では、基本的に送信と受信をクロス接続します。

PC側 TX → 機器側 RX
PC側 RX ← 機器側 TX
GND → GND

TX同士、RX同士を接続すると通信できません。また、GNDが共通になっていないと信号が安定しないことがあります。

RS-232CとTTLレベルのUARTは電圧レベルが異なります。直接接続すると機器を壊す可能性があるため、接続先に応じた変換モジュールを使用してください。

9-4. 他のアプリがポートを使用していないか確認する

シリアルポートは、基本的に複数のアプリから同時に開くことはできません。

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

次のようなエラーが出る場合は、他アプリがポートを使用している可能性があります。

UnauthorizedAccessException

対処法は、同じCOMポートを使用しているアプリを閉じることです。Arduino IDEを使っている場合は、シリアルモニターを閉じてからC#アプリを実行してください。

9-5. 改行コードや文字コードの違いを確認する

送信しているのに機器が反応しない場合、改行コードが違う可能性があります。

機器によって、コマンドの終端に必要な文字が異なります。

CRLF: \r\n
CR: \r
LF: \n
なし

たとえば、機器がCRを要求しているのに、C#側でLFだけを送っていると、コマンドとして認識されないことがあります。

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

文字化けする場合は、Encodingも確認します。

C#
serialPort.Encoding = System.Text.Encoding.ASCII;

日本語を含むデータでは、接続先がShift_JISなのかUTF-8なのかを確認してください。

9-6. 受信バッファとデータ区切りを確認する

受信データが途中で切れる、複数メッセージがつながる、1回の受信で全部取れないという問題はよくあります。

シリアル通信では、送信側が1回で送ったデータが、受信側でも1回でまとまって届くとは限りません。

そのため、次のようなデータ区切りを設計する必要があります。

・改行コードで区切る
・STX/ETXで囲む
・固定長で受信する
・先頭にデータ長を付ける
・一定時間無通信なら1件とみなす

たとえば、改行区切りで受信する場合は、受信データをStringBuilderにためて、改行が来たら1行として処理します。

C#
using System.Text;

StringBuilder receiveBuffer = new StringBuilder();

void OnReceive(string data)
{
receiveBuffer.Append(data);

string text = receiveBuffer.ToString();
int index;

while ((index = text.IndexOf("\n")) >= 0)
{
string line = text.Substring(0, index).TrimEnd('\r');
Console.WriteLine("1行受信: " + line);

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

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

実用的なC#シリアル通信では、このような受信バッファ処理が重要です。

9-7. Tera Termで送受信できるか切り分ける

C#アプリで通信できない場合は、Tera Termなどで同じ条件を設定して確認します。

Tera Termで通信できるなら、次のようなC#側の問題が考えられます。

・PortNameが違う
・NewLineが違う
・Encodingが違う
・ReadLineで待ち続けている
・DataReceivedイベントの処理が間違っている
・UIスレッド更新で例外が発生している

Tera Termでも通信できない場合は、次のような環境側の問題が考えられます。

・配線ミス
・機器の電源OFF
・ドライバー未インストール
・COMポート番号違い
・機器側の通信設定違い
・USBシリアル変換ケーブルの不良

まずC#のコードを疑うのではなく、ターミナルソフトで通信できるかを確認すると、原因を効率よく絞り込めます。

10. C#シリアル通信を安定させる実装のポイント

10-1. using/try-catch-finallyでリソースを確実に解放する

シリアルポートはOSのリソースを使用します。アプリ終了時やエラー発生時にポートを閉じ忘れると、次回起動時にCOMポートを開けなくなることがあります。

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

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

明示的に閉じる場合は、finallyで処理します。

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

serialPort.Dispose();
}

エラーが発生しても確実にCloseされるように実装しましょう。

10-2. 送受信処理をUIスレッドで直接実行しない

Windows FormsやWPFで、重い送受信処理をUIスレッドで直接実行すると、画面が固まる原因になります。

特に、ReadLineのような待機処理をボタンクリックイベント内で直接実行すると、応答が返るまで画面操作ができなくなることがあります。

対策としては、次のような方法があります。

・DataReceivedイベントを使う
・Task.Runでバックグラウンド処理にする
・async/awaitを使う
・タイムアウトを必ず設定する
・UI更新はInvoke/BeginInvokeで行う

UIアプリでは、「通信処理」と「画面更新処理」を分ける設計が重要です。

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

実機とのシリアル通信では、常に正常な応答が返るとは限りません。

・機器が一時的に忙しい
・ケーブルが抜けかけている
・ノイズでデータが壊れる
・コマンドの応答に時間がかかる
・機器がリセット中

このような状況に備えて、タイムアウトとリトライ処理を入れておくと安定します。

C#
for (int retry = 0; retry < 3; retry++)
{
try
{
serialPort.WriteLine("READ");
string response = serialPort.ReadLine();

Console.WriteLine("受信: " + response);
break;
}
catch (TimeoutException)
{
Console.WriteLine($"タイムアウトしました。リトライ回数: {retry + 1}");
}
}

ただし、リトライを入れる場合は、同じコマンドを複数回送っても問題ないか機器仕様を確認してください。制御コマンドの場合、二重実行が危険なケースもあります。

10-4. 受信データの終端文字・固定長・可変長を設計する

安定したシリアル通信を作るには、受信データをどこで1件と判断するかが重要です。

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

・改行コードまでを1件とする
・ETXまでを1件とする
・固定バイト数を受信したら1件とする
・ヘッダーに含まれるデータ長を見て1件とする
・一定時間データが来なければ1件とする

たとえば、CSVのようなテキストデータなら改行区切りが扱いやすいです。

TEMP,25.3
HUM,60.1

バイナリ通信なら、データ長やチェックサムを含める設計がよく使われます。

[STX][LEN][DATA...][CHECKSUM][ETX]

C#側では、受信したデータを一時バッファにためて、1件分がそろった時点で解析するようにします。

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

シリアル通信のトラブルは、ログがあるかどうかで調査のしやすさが大きく変わります。

最低限、次の情報をログに残すと便利です。

・送信日時
・送信データ
・受信日時
・受信データ
・エラー内容
・COMポート名
・通信条件
・タイムアウト発生回数

文字列データだけでなく、バイナリ通信では16進数ログも有効です。

C#
byte[] data = { 0x02, 0x31, 0x32, 0x03 };
string hex = BitConverter.ToString(data);

Console.WriteLine(hex);

出力例です。

02-31-32-03

バイナリデータはそのまま表示すると見えない制御コードがあるため、16進数で記録すると解析しやすくなります。

10-6. アプリ終了時にポートを閉じる

アプリ終了時には、必ずシリアルポートを閉じます。

Windows Formsでは、FormClosingイベントで閉じるのが一般的です。

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

serialPort.Dispose();
}
}

終了処理を入れておかないと、アプリが異常終了したときにポートが使用中のままになることがあります。実用アプリでは、通常終了だけでなく、例外発生時にもリソースを解放できるようにしましょう。

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

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

C#のSerialPortは、.NET 6や.NET 8のプロジェクトでも利用できます。ただし、プロジェクトの種類や環境によってはSystem.IO.Portsパッケージの追加が必要です。

SerialPortが見つからない場合は、NuGetでSystem.IO.Portsを追加してください。

Bash
dotnet add package System.IO.Ports

Windows向けのデスクトップアプリであれば、従来の.NET Frameworkと近い感覚でSerialPortを使えます。

11-2. USB接続の機器でもSerialPortで通信できる?

USB接続の機器でも、Windows上でCOMポートとして認識されるタイプであれば、SerialPortで通信できます。

たとえば、次のような機器はCOMポートとして認識されることがあります。

・USBシリアル変換ケーブル
・Arduino
・USB接続のバーコードリーダー
・USB仮想COMポート対応の測定器
・マイコンボード

デバイスマネージャーの「ポート(COMとLPT)」に表示されていれば、C#からCOM3などを指定して通信できます。

ただし、すべてのUSB機器がSerialPortで扱えるわけではありません。HIDデバイスや独自USBドライバーの機器は、別のAPIやSDKが必要になる場合があります。

11-3. COMポートが表示されないときはどうする?

COMポートが表示されない場合は、次の点を確認します。

・USBケーブルが正しく接続されているか
・機器の電源が入っているか
・ドライバーがインストールされているか
・別のUSBポートに挿してみる
・別のUSBケーブルを使ってみる
・デバイスマネージャーに不明なデバイスがないか
・機器が仮想COMポート対応か

USBシリアル変換チップによっては、専用ドライバーが必要です。ドライバーが入っていないと、COMポートとして表示されません。

11-4. 文字化けする原因は?

C#のシリアル通信で文字化けする原因として多いのは、次の3つです。

・ボーレートなど通信条件が合っていない
・文字コードが合っていない
・バイナリデータを文字列として表示している

ボーレートが違うと、受信データが意味のない文字列になります。まずは機器側とC#側の通信条件を一致させてください。

文字コードが原因の場合は、Encodingを設定します。

C#
serialPort.Encoding = System.Text.Encoding.ASCII;

日本語を扱う場合は、接続先機器の仕様に合わせてUTF-8やShift_JISを指定します。

C#
serialPort.Encoding = System.Text.Encoding.UTF8;

バイナリデータの場合は、文字列として表示するのではなく、16進数で表示すると確認しやすくなります。

11-5. 受信データが途中で切れる原因は?

受信データが途中で切れるように見える場合、実際にはSerialPortの仕様や受信タイミングの問題で、データが複数回に分かれて届いているだけのことがあります。

DataReceivedイベントは、1メッセージの受信完了を保証するものではありません。そのため、イベントが発生した時点でReadExistingを呼んでも、まだ後続データが届いていない場合があります。

対策としては、受信バッファを用意して、終端文字やデータ長をもとに1件分を組み立てます。

C#
StringBuilder buffer = new StringBuilder();

void AddReceivedData(string data)
{
buffer.Append(data);

if (buffer.ToString().Contains("\n"))
{
string message = buffer.ToString();
Console.WriteLine(message);
buffer.Clear();
}
}

また、ReadBufferSizeが小さすぎる、大量データをUI処理で詰まらせている、受信処理が遅いといった原因も考えられます。

11-6. 複数のシリアルポートを同時に扱える?

C#では、複数のSerialPortインスタンスを作成すれば、複数のシリアルポートを同時に扱えます。

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

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

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

複数ポートを扱う場合は、それぞれのポートごとに受信処理、エラー処理、ログ出力を分けて管理します。DataReceivedイベントを使う場合も、どのポートから受信したデータなのかを識別できるようにしておきましょう。

C#
private void Port_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
SerialPort sp = (SerialPort)sender;
string data = sp.ReadExisting();

Console.WriteLine($"{sp.PortName}: {data}");
}

複数ポートを同時に使うアプリでは、ポートごとの設定、接続状態、ログファイル名を明確に管理することが大切です。

まとめ

C#でシリアル通信を行うには、System.IO.Ports.SerialPortクラスを使います。SerialPortを使えば、COMポートを開き、文字列やバイナリデータを送信し、機器からの応答を受信できます。

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

  1. 使用するCOMポートを確認する

  2. ボーレートなど通信条件を機器に合わせる

  3. SerialPortインスタンスを作成する

  4. Openで接続する

  5. WriteやWriteLineで送信する

  6. ReadLine、ReadExisting、Readで受信する

  7. CloseやDisposeで切断する

C#のシリアル通信でトラブルが起きる場合、原因はコードだけとは限りません。COMポート番号、ボーレート、パリティ、配線、USBシリアル変換ケーブル、改行コード、文字コード、他アプリの使用状況などを順番に確認することが重要です。

特に実用アプリでは、タイムアウト、例外処理、受信バッファ、ログ出力、UIスレッド対策、アプリ終了時のClose処理を入れることで、安定したシリアル通信を実装できます。

まずはコンソールアプリでSerialPortの基本的な接続・送受信を確認し、その後Windows FormsやWPFなどの画面アプリに応用していくと、C#でのシリアル通信をスムーズに習得できます。