C# SerialPort.Writeの使い方完全ガイド|文字列・バイト送信、改行、文字化け・タイムアウト対策まで

はじめに

C#でRS-232C、USBシリアル変換、Arduino、マイコン、PLC、測定器などと通信するときによく使うのが、System.IO.Ports.SerialPortクラスです。その中でもSerialPort.Writeは、PC側から外部機器へ文字列やバイト列を送信するための基本メソッドです。

ただし、シリアル通信は「コードが正しく見えるのに機器が反応しない」ことが珍しくありません。原因は、COMポート番号、ボーレート、改行コード、文字コード、ハンドシェイク、タイムアウト、機器側のコマンド仕様など多岐にわたります。

この記事では、C#のSerialPort.Writeの基本構文から、文字列送信、バイト送信、改行コード、文字化け、タイムアウト、Windows FormsやWPFでの実践例、トラブルシューティングまでをまとめて解説します。

1. C#のSerialPort.Writeとは?基本役割と使いどころ

1-1. SerialPortクラスでできること

SerialPortクラスは、C#からシリアルポートを制御するためのクラスです。COMポートを開き、外部機器へデータを送信したり、外部機器から送られてきたデータを受信したりできます。

System.IO.Portsパッケージは、同期I/O、イベント駆動I/O、ピン状態やブレーク状態へのアクセス、BaseStreamを通じたストリーム操作などを提供します。Microsoft Learn+1

主な用途は次のとおりです。

  • Arduinoやマイコンへのコマンド送信

  • PLCや測定器への制御コマンド送信

  • バーコードリーダー、電子天秤、温度計などの外部機器との通信

  • RS-232C、RS-485、USBシリアル変換アダプタを使った通信

  • 産業機器、検査装置、治具、IoTデバイスとの連携

1-2. Writeメソッドの役割

SerialPort.Writeは、シリアルポートの出力バッファーにデータを書き込むメソッドです。Microsoft公式ドキュメントでも、Writeは「シリアルポート出力バッファーにデータを書き込む」メソッドとして説明されています。Microsoft Learn

Writeには、主に次の3つのオーバーロードがあります。

C#
Write(string text)
Write(byte[] buffer, int offset, int count)
Write(char[] buffer, int offset, int count)

文字列コマンドを送る場合はWrite(string text)、バイナリコマンドやHEXデータを送る場合はWrite(byte[] buffer, int offset, int count)を使うのが一般的です。

1-3. WriteLine・Read・ReadLineとの違い

Writeと似たメソッドにWriteLineがあります。

Writeは、指定したデータをそのまま送信します。一方、WriteLineは文字列の末尾にNewLineプロパティで指定された改行コードを付けて送信します。NewLineの既定値は\nで、ReadLineWriteLineの行末判定に使われます。Microsoft Learn

受信側のメソッドとしては、ReadReadExistingReadLineなどがあります。

  • Write:データを送信する

  • WriteLine:改行付きで文字列を送信する

  • Read:指定した文字数またはバイト数を読み取る

  • ReadExisting:受信バッファーにあるデータをまとめて読み取る

  • ReadLine:改行コードまで読み取る

たとえば、機器仕様書に「コマンド末尾はCRLF」と書かれている場合、Write("CMD\r\n")またはNewLine = "\r\n"を設定したうえでWriteLine("CMD")を使います。

1-4. SerialPort.Writeを使う代表的な場面

SerialPort.Writeは、次のような場面で使います。

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

このように、外部機器へ「開始」「停止」「設定変更」「データ要求」などのコマンドを送るケースが代表的です。

バイナリプロトコルの機器では、次のようにバイト配列を送信します。

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

文字列ベースの機器ではWrite(string)、バイナリベースの機器ではWrite(byte[], int, int)を使い分けるのが基本です。

2. SerialPort.Writeを使う前の準備

2-1. System.IO.Ports名前空間の追加

C#でSerialPortを使うには、まず次の名前空間を追加します。

C#
using System.IO.Ports;

.NET Frameworkでは標準で使えることが多いですが、.NET 6、.NET 8、.NET 9以降のプロジェクトでは、必要に応じてNuGetパッケージSystem.IO.Portsを追加します。NuGetのSystem.IO.Portsは.NET 8、.NET 9、.NET 10などにも対応しています。nuget

Visual Studioの場合は、NuGetパッケージマネージャーでSystem.IO.Portsを検索してインストールします。コマンドで追加する場合は、次のように実行します。

Bash
dotnet add package System.IO.Ports

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

SerialPortオブジェクトは、次のように作成できます。

C#
using System.IO.Ports;

SerialPort serialPort = new SerialPort();

コンストラクタでポート名やボーレートを指定することもできます。

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

より実践的には、パリティ、データビット、ストップビットまで明示します。

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

2-3. COMポート名・ボーレート・パリティ・データビット・ストップビットの設定

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

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

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

よく使われる設定は「9600bps、パリティなし、8データビット、1ストップビット」です。表記としては「9600, N, 8, 1」や「9600 8N1」と書かれることがあります。

ただし、実際には機器仕様書に合わせる必要があります。測定器やPLCでは、19200bps、38400bps、115200bpsなどが指定されていることもあります。

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

SerialPort.Writeを呼び出す前に、必ずOpenメソッドでポートを開きます。

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

ポートを開かずにWriteを呼び出すと、InvalidOperationExceptionが発生します。Writeの公式ドキュメントでも、指定されたポートが開いていない場合はInvalidOperationExceptionが発生するとされています。Microsoft Learn

2-5. IsOpenで接続状態を確認する

送信前には、IsOpenプロパティでポートが開いているか確認できます。

C#
if (serialPort.IsOpen)
{
serialPort.Write("START\r\n");
}

アプリケーションによっては、送信ボタンが押されたタイミングでポートが閉じられている可能性があります。そのため、実務では次のようにチェックしてから送信すると安全です。

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

serialPort.Write("START\r\n");

ただし、毎回OpenCloseを繰り返すと、機器によっては再接続処理や初期化待ちが必要になることがあります。通常は、通信開始時にOpenし、アプリ終了時や切断時にCloseする設計が扱いやすいです。

2-6. Close・Disposeでポートを安全に閉じる

使い終わったシリアルポートは、CloseまたはDisposeで解放します。

C#
serialPort.Close();
serialPort.Dispose();

usingを使うと、処理終了時に自動で破棄できます。

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

serialPort.Open();
serialPort.Write("START\r\n");

Windows FormsやWPFなど、アプリケーションの起動中にポートを保持する場合は、フォーム終了時やウィンドウ終了時にCloseDisposeを呼び出すのが基本です。

3. SerialPort.Writeの基本構文

3-1. Write(string text)で文字列を送信する

文字列を送信する場合は、次のように書きます。

C#
serialPort.Write("HELLO");

改行コードが必要な機器では、末尾に\r\n\r\nのいずれかを付けます。

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

Write(string text)は、指定した文字列をシリアルポートへ書き込むためのオーバーロードです。Microsoft Learn

3-2. Write(byte[] buffer, int offset, int count)でバイト配列を送信する

バイト配列を送信する場合は、次のように書きます。

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

Write(byte[], int, int)は、指定したバイト数をバッファーからシリアルポートへ書き込むオーバーロードです。Microsoft Learn

バイナリコマンド、制御コード、STX/ETX付きプロトコル、チェックサム付きデータなどを扱う場合は、この形式を使います。

3-3. Write(char[] buffer, int offset, int count)で文字配列を送信する

文字配列を送る場合は、次のように書きます。

C#
char[] chars = { 'A', 'B', 'C' };
serialPort.Write(chars, 0, chars.Length);

ただし、実務ではstringまたはbyte[]を使うケースがほとんどです。文字列コマンドならWrite(string)、正確なバイト列を送りたいならWrite(byte[])を選ぶとよいでしょう。

3-4. offsetとcountの意味

Write(byte[] buffer, int offset, int count)offsetcountは、配列のどこから何バイト送るかを指定する値です。

C#
byte[] data = { 0x10, 0x20, 0x30, 0x40 };

// data[1]から2バイト送る → 0x20, 0x30
serialPort.Write(data, 1, 2);

通常は配列全体を送ることが多いため、次の書き方がよく使われます。

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

offsetcountが配列の範囲外になると例外が発生します。送信データの一部だけを送る必要がない場合は、0data.Lengthを指定するのが安全です。

3-5. 送信前に確認すべきポイント

SerialPort.Writeを実行する前に、最低限次の点を確認しましょう。

  • COMポート番号が正しいか

  • ボーレート、パリティ、データビット、ストップビットが機器側と一致しているか

  • ポートがOpenされているか

  • 改行コードが機器仕様に合っているか

  • 文字列送信の場合、Encodingが合っているか

  • バイト送信の場合、送信バイト列が仕様書どおりか

  • 他のアプリが同じCOMポートを使用していないか

特に「送信しているのに機器が反応しない」場合は、改行コードと通信設定の不一致がよくある原因です。

4. 文字列を送信するSerialPort.Writeの使い方

4-1. 文字列送信のサンプルコード

次のサンプルは、COM3に文字列HELLOを送信する基本例です。

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

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

serialPort.Open();

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

Console.WriteLine("送信しました。");
}
}

Writeは改行を自動で付けないため、必要な場合は"HELLO\r\n"のように明示的に付けます。

4-2. コマンド文字列を送る例

測定器やPLCでは、ASCII文字列のコマンドを送ることがあります。

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

たとえば、機器仕様書に「測定値要求コマンド:MEASURE?、終端:CRLF」と書かれている場合は、次のように送ります。

C#
string command = "MEASURE?";
serialPort.Write(command + "\r\n");

コマンド本体と終端文字を分けて管理すると、あとから変更しやすくなります。

C#
string command = "MEASURE?";
string terminator = "\r\n";

serialPort.Write(command + terminator);

4-3. 送信データに改行コードを付ける方法

改行コードを付ける方法は大きく2つあります。

1つ目は、Writeで文字列末尾に直接付ける方法です。

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

2つ目は、NewLineを設定してWriteLineを使う方法です。

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

機器仕様が明確で、すべてのコマンドに同じ終端文字を付けるならWriteLineが便利です。一方、コマンドごとに終端文字が異なる場合や、バイナリデータを送る場合はWriteで明示する方が安全です。

4-4. \r・\n・\r\nの違い

シリアル通信でよく使う改行コードは次の3種類です。

C#
"\r"   // CR: Carriage Return, 0x0D
"\n" // LF: Line Feed, 0x0A
"\r\n" // CRLF: 0x0D 0x0A

古い測定器やRS-232C機器ではCR、Unix系のテキストではLF、Windows系や多くの通信プロトコルではCRLFが使われます。

ただし、どれが正しいかは機器によって異なります。必ず仕様書の「ターミネータ」「デリミタ」「終端文字」「Delimiter」「Terminator」などの項目を確認してください。

4-5. WriteとWriteLineの使い分け

Writeは、指定したデータをそのまま送信したい場合に使います。

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

WriteLineは、NewLineで指定した改行コードを自動で付けたい場合に使います。

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

迷った場合は、最初はWriteで改行コードまで明示する方がデバッグしやすいです。送信内容がコード上で完全に見えるため、「本当にCRLFを送っているか」を確認しやすくなります。

4-6. NewLineプロパティの設定方法

NewLineは、WriteLineで付加する行末文字、またはReadLineで行末として扱う文字列を指定するプロパティです。既定値は\nです。Microsoft Learn

CRLFを使う機器なら、次のように設定します。

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

CRだけを使う機器なら、次のように設定します。

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

WriteLineを使っているのに機器が反応しない場合は、NewLineが機器仕様と合っているかを必ず確認しましょう。

5. バイトデータを送信するSerialPort.Writeの使い方

5-1. byte配列を送信するサンプルコード

バイト列を送る基本コードは次のとおりです。

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

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

serialPort.Open();

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

Console.WriteLine("バイトデータを送信しました。");
}
}

0x02はSTX、0x03はETXとして使われることがあります。バイナリプロトコルの機器では、こうした制御コードを含むデータを送信するためにbyte[]を使います。

5-2. HEXデータを送信する方法

仕様書に「送信データ:02 30 31 03」のようにHEX表記で書かれている場合は、C#では次のようにバイト配列にします。

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

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

文字列として入力されたHEXをバイト配列に変換する場合は、次のようにできます。

C#
using System;
using System.Linq;

string hex = "02 30 31 03";

byte[] data = hex
.Split(' ', StringSplitOptions.RemoveEmptyEntries)
.Select(x => Convert.ToByte(x, 16))
.ToArray();

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

注意点は、"02"という文字列をそのまま送るのと、0x02という1バイトを送るのはまったく別物だということです。

C#
serialPort.Write("02");        // 文字 '0' と '2' を送る
serialPort.Write(new byte[] { 0x02 }, 0, 1); // 1バイトの 0x02 を送る

5-3. 文字列をバイト配列に変換して送る方法

文字列を明示的にバイト配列へ変換してから送信することもできます。

C#
using System.Text;

string command = "START\r\n";
byte[] data = Encoding.ASCII.GetBytes(command);

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

この方法のメリットは、送信するバイト列をログ出力しやすいことです。

C#
Console.WriteLine(BitConverter.ToString(data));
// 例: 53-54-41-52-54-0D-0A

文字化けや改行コードの問題を調査するときは、実際に送っているバイト列を確認するのが有効です。

5-4. Encoding.ASCII・UTF8・Shift_JISの違い

Write(string)で文字列を送る場合、内部的にはSerialPort.Encodingに従って文字がバイト列へ変換されます。Encodingプロパティの既定値はASCIIEncodingです。Microsoft Learn

ASCIIは英数字や記号中心の文字コードです。日本語は表現できません。公式ドキュメントでも、既定のASCIIでは127より大きい文字が?としてエンコードされることが説明されています。Microsoft Learn

日本語をUTF-8で送る場合は、次のようにします。

C#
serialPort.Encoding = Encoding.UTF8;
serialPort.Write("開始\r\n");

Shift_JISで送る場合は、.NETの環境によってはコードページプロバイダーを登録します。

C#
using System.Text;

Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

serialPort.Encoding = Encoding.GetEncoding("shift_jis");
serialPort.Write("開始\r\n");

ただし、外部機器が日本語に対応していない場合、C#側で文字コードを変えても正しく表示・解釈されません。機器側がASCIIコマンドのみ対応している場合は、日本語を送らないようにしましょう。

5-5. バイナリコマンド送信時の注意点

バイナリコマンドを送る場合は、Write(string)ではなくWrite(byte[])を使うのが基本です。

理由は、文字列にするとエンコーディング変換が入るため、意図したバイト列と違うデータになる可能性があるからです。

たとえば、次のようなデータを送りたいとします。

C#
02 10 FF 03

この場合は、必ず次のようにバイト配列で送ります。

C#
byte[] command = { 0x02, 0x10, 0xFF, 0x03 };
serialPort.Write(command, 0, command.Length);

0xFFのような値を文字列で扱うと、文字コード変換の影響を受けやすくなります。バイナリ通信では「仕様書のHEX値どおりにbyte配列を作る」ことが重要です。

5-6. チェックサム付きデータを送る場合の考え方

機器によっては、送信データの末尾にチェックサムやBCC、CRCを付ける必要があります。

単純な加算チェックサムの例は次のとおりです。

C#
static byte CalcChecksum(byte[] data)
{
byte sum = 0;

foreach (byte b in data)
{
sum += b;
}

return sum;
}

送信例です。

C#
byte[] body = { 0x02, 0x30, 0x31 };
byte checksum = CalcChecksum(body);

byte[] packet = new byte[body.Length + 2];
Array.Copy(body, packet, body.Length);
packet[packet.Length - 2] = checksum;
packet[packet.Length - 1] = 0x03;

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

実際のチェックサム方式は機器ごとに異なります。加算、XOR、2の補数、CRC-16など方式が違うため、必ず仕様書に従って実装してください。

6. SerialPort.Writeでよく使う実践サンプル

6-1. Arduinoへコマンドを送信する例

ArduinoへLED_ONLED_OFFのような文字列コマンドを送る例です。

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

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

serialPort.Open();

// Arduinoはポートオープン時にリセットされることがあるため少し待つ
Thread.Sleep(2000);

serialPort.WriteLine("LED_ON");

Console.WriteLine("Arduinoへ送信しました。");
}
}

Arduino側でSerial.readStringUntil('\n')のようにLFまで読む実装にしている場合、C#側はNewLine = "\n"WriteLineを使うと合わせやすいです。

6-2. マイコン・PLC・測定器へ文字列コマンドを送る例

測定器へ測定開始コマンドを送る例です。

C#
using SerialPort serialPort = new SerialPort("COM4", 19200, Parity.None, 8, StopBits.One)
{
NewLine = "\r\n",
Encoding = Encoding.ASCII,
WriteTimeout = 1000,
ReadTimeout = 2000
};

serialPort.Open();

serialPort.Write("START\r\n");

応答を受け取る場合は、次のようにします。

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

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

ReadLineは改行コードを受信するまで待つため、機器の応答末尾とNewLineの設定が一致している必要があります。

6-3. バーコードリーダーや外部機器へ制御コマンドを送る例

バーコードリーダーや外部機器では、読み取り開始、読み取り停止、設定変更などの制御コマンドを送ることがあります。

C#
byte[] triggerOn = { 0x16, 0x54, 0x0D };
serialPort.Write(triggerOn, 0, triggerOn.Length);

仕様書にHEXでコマンドが書かれている場合は、文字列ではなくbyte[]で送るのが安全です。

C#
// NG: "16 54 0D"という文字列が送られる
serialPort.Write("16 54 0D");

// OK: 0x16, 0x54, 0x0D の3バイトが送られる
byte[] command = { 0x16, 0x54, 0x0D };
serialPort.Write(command, 0, command.Length);

6-4. ボタンクリックでシリアル送信するWindows Formsサンプル

Windows Formsでボタンを押したときにSerialPort.Writeを実行する例です。

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

public partial class Form1 : Form
{
private SerialPort _serialPort;

public Form1()
{
InitializeComponent();

_serialPort = new SerialPort("COM3", 9600, Parity.None, 8, StopBits.One)
{
Encoding = Encoding.ASCII,
NewLine = "\r\n",
WriteTimeout = 1000,
ReadTimeout = 1000
};
}

private void Form1_Load(object sender, EventArgs e)
{
_serialPort.Open();
}

private void buttonSend_Click(object sender, EventArgs e)
{
if (!_serialPort.IsOpen)
{
MessageBox.Show("シリアルポートが開いていません。");
return;
}

string command = textBoxCommand.Text + "\r\n";
_serialPort.Write(command);
}

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

_serialPort.Dispose();
}
}
}

UIアプリでは、送信処理中に例外が出る可能性を考慮し、実際にはtry-catchを入れるのがおすすめです。

6-5. WPFアプリでSerialPort.Writeを使う例

WPFでも基本は同じです。ボタンクリックで送信する例は次のとおりです。

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

public partial class MainWindow : Window
{
private readonly SerialPort _serialPort;

public MainWindow()
{
InitializeComponent();

_serialPort = new SerialPort("COM3", 9600, Parity.None, 8, StopBits.One)
{
Encoding = Encoding.ASCII,
NewLine = "\r\n",
WriteTimeout = 1000
};

_serialPort.Open();
}

private void SendButton_Click(object sender, RoutedEventArgs e)
{
try
{
if (!_serialPort.IsOpen)
{
MessageBox.Show("ポートが開いていません。");
return;
}

_serialPort.Write("START\r\n");
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}

protected override void OnClosed(EventArgs e)
{
if (_serialPort.IsOpen)
{
_serialPort.Close();
}

_serialPort.Dispose();

base.OnClosed(e);
}
}

送信に時間がかかる可能性がある場合は、UIスレッドを止めないようにTask.Runなどで別スレッドに逃がす設計を検討します。

6-6. 送信後に応答を受信する基本パターン

コマンド送信後に応答を受け取る基本パターンです。

C#
try
{
serialPort.Write("STATUS?\r\n");

string response = serialPort.ReadLine();

Console.WriteLine("応答: " + response);
}
catch (TimeoutException)
{
Console.WriteLine("応答がタイムアウトしました。");
}

ReadLineを使う場合、応答データの末尾にNewLineで指定した改行コードが必要です。応答が改行で終わらない機器では、ReadReadExistingを使って独自に受信完了条件を判定します。

7. SerialPort.Writeで改行を送る方法

7-1. 送信データ末尾に改行コードを付ける

Writeで改行を送る最も分かりやすい方法は、文字列末尾に改行コードを付けることです。

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

CRだけなら次のようにします。

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

LFだけなら次のようにします。

C#
serialPort.Write("COMMAND\n");

機器が「コマンドを受け取っているが実行しない」場合、終端文字が不足している可能性があります。

7-2. WriteLineで自動的に改行を付ける

毎回同じ改行コードを付けるなら、WriteLineを使えます。

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

この場合、実際に送られるデータは次のようになります。

COMMAND\r\n

ただし、NewLineを設定していない場合、既定値は\nです。CRLFが必要な機器に対してWriteLineをそのまま使うと、LFしか送られず反応しないことがあります。Microsoft Learn

7-3. 機器仕様に合わせてCR・LF・CRLFを選ぶ

改行コードは、C#側の好みではなく機器仕様に合わせます。

仕様書に次のように書かれている場合があります。

  • Terminator: CR

  • Delimiter: LF

  • End Code: CR+LF

  • Command format: <command><CR>

  • Response format: <data><CR><LF>

この場合、C#側の送信コードも合わせます。

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

// LF
serialPort.Write("READ\n");

// CRLF
serialPort.Write("READ\r\n");

7-4. 改行が認識されないときの確認ポイント

改行コードが認識されないときは、次を確認します。

  • Writeを使っているのに改行コードを付け忘れていないか

  • WriteLineNewLineが機器仕様と合っているか

  • 機器側がCR、LF、CRLFのどれを要求しているか

  • コマンド文字列に余計なスペースが入っていないか

  • 送信文字コードがASCII想定になっているか

  • ターミナルソフトで同じコマンドを送ると反応するか

特に、WriteLineを使えば必ずCRLFが付くと思い込むのは危険です。NewLineの既定値はLFです。

7-5. ターミナルソフトで改行コードを検証する方法

C#のコードを疑う前に、Tera Term、RealTerm、PuTTYなどのターミナルソフトで機器に直接コマンドを送ると原因を切り分けやすくなります。

確認手順は次のとおりです。

  1. ターミナルソフトで同じCOMポートを開く

  2. ボーレートなどを機器仕様に合わせる

  3. 改行コードをCR、LF、CRLFに切り替えながらコマンドを送る

  4. 反応する組み合わせを確認する

  5. C#側のWriteまたはNewLine設定を同じにする

ターミナルソフトで反応しない場合、C#以前に通信設定、配線、ポート番号、機器側設定が間違っている可能性があります。

8. SerialPort.Writeで文字化けする原因と対策

8-1. Encoding設定が合っていない

Write(string)で送信する場合、文字列はSerialPort.Encodingに従ってバイト列へ変換されます。Encodingの既定値はASCIIです。Microsoft Learn

英数字だけならASCIIで問題ないことが多いですが、日本語や特殊文字を送る場合は文字化けの原因になります。

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

C#
serialPort.Encoding = Encoding.UTF8;
serialPort.Write("温度取得\r\n");

Shift_JISで送る場合は、次のように設定します。

C#
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

serialPort.Encoding = Encoding.GetEncoding("shift_jis");
serialPort.Write("温度取得\r\n");

8-2. 日本語送信時に注意すべき文字コード

日本語を送る場合、C#側と機器側で文字コードを一致させる必要があります。

よくある組み合わせは次のとおりです。

  • PCアプリ:UTF-8

  • 古い国内機器:Shift_JIS

  • コマンド制御機器:ASCIIのみ

  • マイコン:UTF-8として受信する実装も多いが、処理はスケッチ次第

たとえば、C#側がUTF-8で「開始」を送っても、機器側がShift_JISとして解釈すれば文字化けします。逆に、C#側がShift_JISで送っても、機器側がUTF-8前提なら文字化けします。

8-3. ASCII前提の機器に日本語を送っている

測定器、PLC、バーコードリーダー、産業機器などの制御コマンドは、ASCII前提であることが多いです。

この場合、次のような日本語コマンドは使えません。

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

機器仕様書にASCIIコマンドとしてSTARTSTOPが定義されているなら、次のように送ります。

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

表示用の日本語と、機器へ送るコマンド文字列は分けて設計しましょう。

C#
string displayText = "開始";
string command = "START\r\n";

serialPort.Write(command);

8-4. ボーレートなど通信設定が一致していない

文字化けの原因は文字コードだけではありません。ボーレート、パリティ、データビット、ストップビットが一致していない場合も、受信側では文字化けしたようなデータになります。

たとえば、機器側が19200bps, Even, 7bit, 1stopなのに、C#側が9600bps, None, 8bit, 1stopだと正しく通信できません。

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

通信設定は、必ず機器仕様書または機器本体の設定画面と合わせてください。

8-5. バイト列をログ出力して原因を確認する方法

文字化け調査では、「送った文字列」ではなく「送ったバイト列」を確認することが重要です。

C#
string command = "開始\r\n";
byte[] bytes = serialPort.Encoding.GetBytes(command);

Console.WriteLine(BitConverter.ToString(bytes));

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

ログ例です。

E9-96-8B-E5-A7-8B-0D-0A

これはUTF-8で「開始\r\n」を送っている例です。Shift_JISなら別のバイト列になります。仕様書に記載されたHEX値と比較すれば、文字コードや改行コードの不一致を見つけやすくなります。

9. SerialPort.Writeでタイムアウトする原因と対策

9-1. WriteTimeoutプロパティの設定方法

WriteTimeoutは、書き込み操作が完了しない場合にタイムアウトするまでのミリ秒数を指定するプロパティです。既定値はInfiniteTimeoutです。Microsoft Learn

C#
serialPort.WriteTimeout = 1000;

この設定では、書き込みが1秒以内に完了しない場合にTimeoutExceptionが発生します。

送信処理が無限に待ち続けるのを避けたい場合は、適切な値を設定しておくと安全です。

9-2. TimeoutExceptionが発生する主な原因

SerialPort.WriteTimeoutExceptionが発生する主な原因は次のとおりです。

  • 送信バッファーが詰まっている

  • ハンドシェイクにより送信が停止している

  • 接続先デバイスが受信可能状態になっていない

  • ケーブルやUSBシリアル変換器に問題がある

  • デバイスが電源OFFまたは異常状態になっている

  • WriteTimeoutが短すぎる

公式ドキュメントでも、出力バッファーにバイトが多すぎ、HandshakeXOnXOffに設定されている場合、デバイスがより多くのデータを受け入れる準備が整うのを待つ間にTimeoutExceptionが発生する可能性があると説明されています。Microsoft Learn

9-3. ポートが開いていない場合の対処

ポートが開いていない場合は、タイムアウトではなくInvalidOperationExceptionが発生します。

送信前にIsOpenを確認しましょう。

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

serialPort.Write("START\r\n");

ただし、すでに他のアプリがポートを開いている場合は、Open時に別の例外が発生します。

9-4. ケーブル・COMポート・デバイス接続の確認

タイムアウトや送信不可が起きる場合は、コードだけでなく物理接続も確認します。

  • USBシリアル変換アダプタが認識されているか

  • デバイスマネージャーでCOM番号が確認できるか

  • ケーブルが抜けていないか

  • TX/RXの接続が逆になっていないか

  • GNDが接続されているか

  • RS-232CとTTLレベルを直接つないでいないか

  • 機器の電源が入っているか

  • 他のターミナルソフトで通信できるか

特にマイコンとの接続では、電圧レベルの違いに注意が必要です。RS-232C、TTLシリアル、RS-485は電気的な仕様が異なります。

9-5. ハンドシェイク設定による送信停止に注意する

Handshakeは、送受信の流量制御を行う設定です。既定値はNoneです。Microsoft Learn

C#
serialPort.Handshake = Handshake.None;

機器がハードウェアフロー制御を使わないのに、C#側でRequestToSendなどを設定すると、送信が止まることがあります。

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

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

通常のArduinoや単純な測定器ではHandshake.Noneでよいことが多いですが、産業機器やモデムではフロー制御が必要な場合があります。ここも機器仕様書に合わせます。

9-6. try-catchで例外処理を実装する

実務では、SerialPort.Writeの周辺に例外処理を入れます。

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

serialPort.Write("START\r\n");
}
catch (TimeoutException)
{
Console.WriteLine("送信がタイムアウトしました。");
}
catch (InvalidOperationException)
{
Console.WriteLine("ポートが開いていません。");
}
catch (UnauthorizedAccessException)
{
Console.WriteLine("COMポートにアクセスできません。他のアプリが使用している可能性があります。");
}
catch (IOException ex)
{
Console.WriteLine("入出力エラー: " + ex.Message);
}
catch (Exception ex)
{
Console.WriteLine("予期しないエラー: " + ex.Message);
}

通信アプリでは、エラーメッセージだけでなく、COMポート名、送信データ、時刻もログに残すと原因を追いやすくなります。

10. SerialPort.Writeで送信できないときのトラブルシューティング

10-1. UnauthorizedAccessExceptionが出る場合

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

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

  • 他のアプリが同じCOMポートを開いている

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

  • 権限の問題でポートにアクセスできない

  • COMポートがOS側で使用中になっている

Tera TermやArduino IDEのシリアルモニタを開いたままC#アプリを起動すると、このエラーが出ることがあります。シリアルポートは通常、複数アプリから同時に開けません。

10-2. IOExceptionが出る場合

IOExceptionは、入出力処理中に問題が起きた場合に発生します。

たとえば、通信中にUSBシリアル変換アダプタが抜けた、デバイスが切断された、ドライバが不安定になった、ポート状態が無効になった、といったケースです。

対策としては、次のような処理を検討します。

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

// 再接続処理やユーザーへの通知を行う
}

10-3. InvalidOperationExceptionが出る場合

InvalidOperationExceptionは、主にポートが開いていない状態でWriteした場合に発生します。Writeの公式ドキュメントにも、指定されたポートが開いていない場合の例外として記載されています。Microsoft Learn

対策は、送信前にIsOpenを確認することです。

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

serialPort.Write("START\r\n");

フォーム終了後や再接続処理後に、破棄済みのSerialPortへアクセスしていないかも確認しましょう。

10-4. COMポート番号が間違っている場合

COMポート番号が間違っていると、当然ながら目的の機器には送信できません。

現在利用可能なポート一覧は、次のコードで取得できます。

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

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

USBシリアル変換アダプタは、差し込むUSBポートを変えるとCOM番号が変わることがあります。アプリの設定画面でCOMポートを選択できるようにしておくと運用しやすくなります。

10-5. 他のアプリがポートを使用している場合

同じCOMポートをTera Term、Arduino IDE、PLC設定ツール、測定器メーカーの専用ソフトなどが使用していると、C#アプリから開けません。

対策は次のとおりです。

  • ターミナルソフトを閉じる

  • Arduino IDEのシリアルモニタを閉じる

  • メーカー製ツールを終了する

  • C#アプリの二重起動を防ぐ

  • アプリ終了時に必ずCloseDisposeを実行する

特に開発中は、デバッグ停止時にポートが解放されないように見えることがあります。その場合はアプリのプロセスが残っていないか確認してください。

10-6. 送信しているのに機器が反応しない場合

Writeで例外が出ていないのに機器が反応しない場合、次を確認します。

  • 改行コードが合っているか

  • コマンド文字列が仕様書どおりか

  • 大文字・小文字が正しいか

  • コマンド間に必要な待ち時間があるか

  • 送信後にACK応答を読む必要があるか

  • ボーレートなどの通信設定が一致しているか

  • 機器側がコマンド受付モードになっているか

  • DTRやRTSの制御が必要か

  • バイナリコマンドを文字列で送っていないか

送信データをHEXログとして出力すると、改行コードや制御コードの誤りを見つけやすくなります。

C#
byte[] bytes = Encoding.ASCII.GetBytes("START\r\n");
Console.WriteLine(BitConverter.ToString(bytes));
serialPort.Write(bytes, 0, bytes.Length);

10-7. DataReceivedイベントと組み合わせた確認方法

送信後の応答を確認するには、DataReceivedイベントを使う方法があります。DataReceivedは、SerialPortでデータを受信したことを示すイベントです。公式ドキュメントでは、このイベントはセカンダリスレッドで発生し、受信したバイトごとに必ず発生するとは限らないと説明されています。Microsoft Learn

C#
serialPort.DataReceived += SerialPort_DataReceived;

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

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

Windows FormsやWPFでUIを更新する場合は、イベントがUIスレッドではない点に注意します。Windows FormsならInvoke、WPFならDispatcher.Invokeを使ってUIスレッドへ戻します。

11. SerialPort.Writeを安全に使うための実装ポイント

11-1. usingまたはDisposeでリソースを解放する

SerialPortはOSのシリアルポートリソースを扱うため、使い終わったら必ず解放します。

コンソールアプリならusingが簡単です。

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

serialPort.Open();
serialPort.Write("START\r\n");

常駐型アプリなら、終了処理でCloseDisposeを呼びます。

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

serialPort.Dispose();

11-2. UIスレッドをブロックしないようにする

SerialPort.Writeは同期メソッドです。送信が詰まると呼び出し元のスレッドが待たされます。

Windows FormsやWPFのボタンクリック内で重い送信処理を行うと、画面が固まったように見えることがあります。

短いコマンド送信なら問題になりにくいですが、大量送信や応答待ちを含む処理では、バックグラウンド処理に分けることを検討しましょう。

11-3. 送信処理を非同期化する考え方

簡単な方法は、Task.Runで送信処理をUIスレッドから外すことです。

C#
await Task.Run(() =>
{
serialPort.Write("START\r\n");
});

ただし、複数スレッドから同時にSerialPort.Writeを呼ぶとデータが混ざる可能性があります。送信キューを用意し、1つずつ順番に送る設計にすると安全です。

C#
private readonly object _serialLock = new object();

void SendCommand(string command)
{
lock (_serialLock)
{
serialPort.Write(command);
}
}

11-4. 連続送信時は送信間隔を設ける

外部機器によっては、連続でコマンドを送ると処理が追いつかないことがあります。

C#
serialPort.Write("CMD1\r\n");
Thread.Sleep(100);

serialPort.Write("CMD2\r\n");
Thread.Sleep(100);

serialPort.Write("CMD3\r\n");

より堅牢にするなら、固定待ちではなく、機器からのACKや応答を受け取ってから次のコマンドを送ります。

C#
serialPort.Write("CMD1\r\n");
string ack = serialPort.ReadLine();

if (ack == "OK")
{
serialPort.Write("CMD2\r\n");
}

11-5. 送信ログを残してデバッグしやすくする

シリアル通信の不具合は、再現時の送信データが分からないと調査が難しくなります。

送信ログには、次の情報を残すと便利です。

  • 送信日時

  • COMポート名

  • 通信設定

  • 送信文字列

  • 送信HEX

  • 受信データ

  • 発生した例外

ログ出力例です。

C#
void WriteLog(byte[] data)
{
string hex = BitConverter.ToString(data);
Console.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} TX {hex}");
}

送信前にログを残します。

C#
byte[] data = Encoding.ASCII.GetBytes("START\r\n");
WriteLog(data);
serialPort.Write(data, 0, data.Length);

11-6. 機器仕様書に合わせて送信フォーマットを決める

SerialPort.Writeの実装で最も重要なのは、C#の都合ではなく機器仕様に合わせることです。

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

  • 通信速度

  • パリティ

  • データビット

  • ストップビット

  • フロー制御

  • コマンド形式

  • 改行コード

  • 文字コード

  • 応答形式

  • タイムアウト

  • チェックサム

  • リトライ条件

シリアル通信では、1文字違い、1バイト違い、改行コード違いで機器が無反応になることがあります。実装前に仕様書を読み、送信フォーマットを明確にしてからコードを書くのが近道です。

12. SerialPort.Writeと関連プロパティ・メソッド

12-1. BaudRate

BaudRateは通信速度を設定するプロパティです。

C#
serialPort.BaudRate = 9600;

PC側と機器側で一致していないと、文字化けや通信失敗の原因になります。

12-2. Encoding

Encodingは、文字列を送受信するときの文字コードを指定するプロパティです。既定値はASCIIです。Microsoft Learn

C#
serialPort.Encoding = Encoding.ASCII;
serialPort.Encoding = Encoding.UTF8;

日本語を送る場合は、機器側の文字コードに合わせて設定します。

12-3. NewLine

NewLineは、WriteLineで付ける改行コード、ReadLineで行末とみなす文字列を指定します。

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

既定値は\nです。CRLFが必要な機器では明示的に設定しましょう。Microsoft Learn

12-4. WriteTimeout

WriteTimeoutは、書き込みが完了しない場合にタイムアウトするまでの時間をミリ秒で指定します。

C#
serialPort.WriteTimeout = 1000;

既定値はInfiniteTimeoutです。Microsoft Learn

12-5. Handshake

Handshakeはフロー制御を設定します。

C#
serialPort.Handshake = Handshake.None;

既定値はNoneです。機器がRTS/CTSやXON/XOFFを必要とする場合のみ、仕様に合わせて変更します。Microsoft Learn

12-6. DtrEnable・RtsEnable

DtrEnableRtsEnableは、DTR信号やRTS信号を制御するプロパティです。

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

Arduinoでは、DTR信号によりリセットが発生することがあります。また、一部の機器ではDTRやRTSを有効にしないと通信を開始しないことがあります。

12-7. BytesToWrite

BytesToWriteは、出力バッファー内に残っている送信待ちバイト数を取得するプロパティです。

C#
int remaining = serialPort.BytesToWrite;

大量データを送る場合や、送信バッファーの状態を確認したい場合に使います。

12-8. DiscardOutBuffer

DiscardOutBufferは、送信バッファー内のデータを破棄するメソッドです。

C#
serialPort.DiscardOutBuffer();

通信エラー後に送信バッファーをクリアしたい場合などに使います。ただし、未送信データが失われるため、通常の送信処理中に安易に呼び出すべきではありません。

13. SerialPort.Writeのよくある質問

13-1. WriteとWriteLineはどちらを使うべき?

機器が改行コードを必要としない、または改行コードを自分で明示したい場合はWriteを使います。

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

毎回同じ改行コードを付けるなら、NewLineを設定してWriteLineを使うと便利です。

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

迷った場合は、まずWriteで送信内容を明示するのがおすすめです。

13-2. 文字列ではなくHEXを送信するには?

HEX表記のコマンドは、byte[]にして送ります。

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

"02 30 31 03"という文字列を送るのではなく、0x02などのバイト値として送る点に注意してください。

13-3. 改行コードは\r\nでよい?

必ずしも\r\nでよいとは限りません。機器によって、CR、LF、CRLFのどれを使うかが異なります。

C#
serialPort.Write("CMD\r");   // CR
serialPort.Write("CMD\n"); // LF
serialPort.Write("CMD\r\n"); // CRLF

仕様書に終端文字が書かれていない場合は、ターミナルソフトでCR、LF、CRLFを試して確認します。

13-4. 日本語を送ると文字化けするのはなぜ?

主な原因は、C#側のEncodingと機器側の文字コードが一致していないためです。

SerialPort.Encodingの既定値はASCIIであり、ASCIIでは日本語を表現できません。Microsoft Learn+1

UTF-8で送るなら次のようにします。

C#
serialPort.Encoding = Encoding.UTF8;
serialPort.Write("開始\r\n");

Shift_JISで送るなら次のようにします。

C#
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
serialPort.Encoding = Encoding.GetEncoding("shift_jis");
serialPort.Write("開始\r\n");

ただし、機器が日本語非対応なら、英数字のASCIIコマンドを使う必要があります。

13-5. Arduinoに送信しても反応しない原因は?

Arduinoに送信しても反応しない場合、次を確認してください。

  • COMポート番号が正しいか

  • ボーレートがArduino側のSerial.begin()と一致しているか

  • Arduino側が改行コードを待っていないか

  • C#側で\n\r\nを送っているか

  • ポートオープン後、Arduinoのリセット待ちを入れているか

  • Arduino IDEのシリアルモニタを閉じているか

  • Arduino側の受信処理が正しいか

Arduinoはポートを開いた直後にリセットされることがあるため、C#側でOpen後に少し待ってから送信すると改善する場合があります。

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

13-6. 送信後に受信データを待つには?

応答が改行で終わるなら、ReadLineを使えます。

C#
serialPort.Write("STATUS?\r\n");
string response = serialPort.ReadLine();

応答が固定長なら、Readを使います。

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

応答の終端条件が独自の場合は、ReadExistingReadで受信しながら、STX/ETX、バイト数、チェックサムなどを自前で判定します。

13-7. SerialPort.Writeは.NET 6や.NET 8でも使える?

使えます。ただし、プロジェクトによってはSystem.IO.Portsパッケージの追加が必要です。NuGetのSystem.IO.Portsパッケージは、.NET 8以降のターゲットにも対応しています。nuget

.csprojにパッケージ参照を追加するか、次のコマンドで追加します。

Bash
dotnet add package System.IO.Ports

コード側では、従来どおり次の名前空間を使います。

C#
using System.IO.Ports;

.NET Frameworkから.NET 6や.NET 8へ移行した場合、「SerialPortが見つからない」というエラーが出たら、まずNuGetパッケージが追加されているか確認してください。

まとめ

C# SerialPort.Writeは、外部機器へデータを送信するための基本メソッドです。文字列コマンドを送るならWrite(string)、HEXやバイナリコマンドを送るならWrite(byte[], int, int)を使います。

実装で特に重要なのは、次のポイントです。

  • 送信前にOpenする

  • IsOpenで状態を確認する

  • COMポート番号と通信設定を機器側に合わせる

  • 改行コードはCR、LF、CRLFのどれかを仕様書で確認する

  • 文字列送信ではEncodingに注意する

  • バイナリ送信ではbyte[]を使う

  • WriteTimeoutと例外処理を設定する

  • UIアプリではスレッドブロックに注意する

  • 送信データをHEXログで確認できるようにする

  • 機器仕様書どおりのフォーマットで送信する

シリアル通信の不具合は、コードの文法ミスよりも、改行コード、文字コード、通信条件、機器仕様の不一致によって起こることが多いです。SerialPort.Writeの使い方を理解したうえで、実際に送っているバイト列を確認しながら実装すれば、Arduino、PLC、測定器、バーコードリーダーなど多くの外部機器と安定して通信できるようになります。