1. はじめに — なぜシリアル通信が今も現役なのか
Ethernet と TCP/IP が標準になった現代でも、産業機器の世界ではシリアル通信が大量に残っています。理由はシンプルで、シリアル通信はシンプルで安価で、長年の実績があるからです。温調器、流量計、はかり、バーコードリーダ、古い PLC、計測機器——どれも RS-232C か RS-485 で動いています。
新規設備でも、コスト要件と安定性の観点から RS-485(Modbus RTU 含む)が採用されることはまだ多々あります。Python エンジニアとしては「シリアルが必要になったときに躊躇なく書ける」状態にしておくと、現場で困ることがほぼなくなります。
2. RS-232C と RS-485 の違い
2.1 RS-232C
- 1 対 1 接続、全二重(送受信同時)
- 距離は 15m 程度まで
- 信号レベルは ±12V 系(実機は ±5〜12V のことが多い)
- D-sub 9 ピンコネクタが定番
2.2 RS-485
- 1 対多接続(マルチドロップ)、半二重が一般的
- 距離は低速時で 1.2km 程度まで(ボーレートが上がるほど短くなる)
- 差動信号で外乱に強い、長距離・複数台に向く
- 2 線式(A / B)または 4 線式
- Modbus RTU の物理層として使われることが多い
Python アプリ側からは、どちらも「USB-シリアル変換ケーブル」を介して COM ポート(Windows)または /dev/ttyUSB*(Linux)として見えるため、コードはほぼ同じです。
3. 環境構築
3.1 ライブラリのインストール
pyserial 公式ドキュメントが第一情報源です。本記事のコードは pyserial 3.5 / Python 3.10 以降を前提にしています(3.5 は 2020 年リリースの現行最新版。開発は活発ではありませんが、機能的に完成しており Python のデファクト標準として広く使われています)。
python -m pip install pyserial
紛らわしい注意点として、インストール名は pyserial、import 名は serial です(import pyserial と書くと ModuleNotFoundError になります)。社内プロキシで pip が通らない場合の対処は 学習ロードマップ STEP 2 を参照してください。
3.2 動作確認用の仮想シリアルポート(Windows)
実機が手元になくても、仮想シリアルポートを 2 つ作って互いにつなげば動作確認できます。Windows なら com0com(オープンソース)が定番です。ただし注意が 2 つあります。第一に、ドライバのインストールには管理者権限が必要で、権限のない会社 PC では情シスへの依頼が要ります。第二に、com0com はドライバ署名の問題が 2026 年現在も未解決で、Windows 11(特に Secure Boot 有効の環境)ではそのままインストールできない場合があります。テスト署名モードは Secure Boot の無効化が必要なため業務 PC では非推奨——確実に動かしたい場合は、署名済みドライバを持つ有償ツール(Virtual Serial Port Driver など)を検討してください。インストールできた場合は、付属の setupc コマンド(管理者として実行)で install PortName=COM10 PortName=COM11 のようにペアを作成し、片方をシミュレータ・もう片方をクライアントが開きます。Linux なら socat で同等のことができます。
# Linux: 仮想シリアルペアを作る
socat -d -d pty,raw,echo=0 pty,raw,echo=0
# 出力例:
# 2026/05/03 10:00:00 N PTY is /dev/pts/3
# 2026/05/03 10:00:00 N PTY is /dev/pts/4
このペアの片方をシミュレータが、もう片方をクライアントが開けば、PC 単体で送受信を確認できます。
4. 最小実装
コードに入る前に、自分の環境の COM ポート番号を確認しておきます。デバイスマネージャーを開き、「ポート (COM と LPT)」を展開すると USB Serial Port (COM3) のように表示されます。USB-シリアル変換ケーブルを挿した状態で確認してください(挿すと項目が増えるので、それが目的のポートです)。
4.1 開く・読む・書く
import serial
ser = serial.Serial(
port="COM3", # Linux なら "/dev/ttyUSB0"
baudrate=9600,
bytesize=serial.EIGHTBITS,
parity=serial.PARITY_NONE,
stopbits=serial.STOPBITS_ONE,
timeout=1.0, # read のタイムアウト(秒)
write_timeout=1.0,
)
try:
ser.write(b"PING\r\n")
response = ser.readline()
print(response.decode("utf-8", errors="replace").rstrip())
finally:
ser.close()
機器側の通信設定(ボーレート、パリティ、ストップビット)と完全一致させる必要があります。ここがズレると無音 or 文字化けで原因不明のまま時間を溶かします。
4.2 with 文での自動 close
with serial.Serial("COM3", 9600, timeout=1.0) as ser:
ser.write(b"PING\r\n")
print(ser.readline())
5. パケット境界の扱い
シリアル通信はストリームベースです——データは小包ではなく、水道の水のように区切りなく流れてきます。送信側で 1 メッセージずつ送っても、受信側で「ちょうど 1 メッセージが 1 回の read で来る」保証はありません。フレーム境界をプロトコル側で明示する必要があります(この性質と対策は TCP/IP 通信の記事の「落とし穴 1・2」と同じ話です)。なお、以降の断片コードの ser は 4.1 で開いたポートを使う前提です。
5.1 終端文字方式
テキスト系プロトコルでは \r\n や \n 終端が一般的。pyserial には終端文字までを読む read_until があります。
frame = ser.read_until(b"\r\n") # キーワードで書くなら expected=b"\r\n"(3.4 以前は terminator= だった)
text = frame.decode("ascii", errors="replace").rstrip()
1 つ注意: read_until はタイムアウトすると、終端が来ていなくてもそこまでに読めた部分データを返します。終端 \r\n が含まれているかを確認してから使ってください。
5.2 長さプレフィックス方式
バイナリプロトコルでは「最初の N バイトに長さが入っている」方式が多用されます。
def recv_exact(ser: serial.Serial, n: int) -> bytes:
"""正確に n バイトを読む。タイムアウト時は TimeoutError を送出する。"""
buf = bytearray()
while len(buf) < n:
chunk = ser.read(n - len(buf))
if not chunk:
raise TimeoutError(f"recv timeout: got {len(buf)} of {n}")
buf.extend(chunk)
return bytes(buf)
def recv_frame(ser: serial.Serial) -> bytes:
header = recv_exact(ser, 2)
length = int.from_bytes(header, "big")
return recv_exact(ser, length)
5.3 タイマ方式
Modbus RTU など一部のプロトコルでは、「3.5 文字時間以上の無通信」をフレーム境界とみなす方式が使われます。pyserial の inter_byte_timeout を設定するか、上位プロトコルライブラリ(pymodbus など)に任せるのが現実的です。
6. CRC(誤り検出)
シリアル通信は外乱に弱いため、ほとんどのバイナリプロトコルで CRC 16 が付加されます。Modbus RTU の CRC-16/Modbus を例に、Python での実装を示します。
def crc16_modbus(data: bytes) -> bytes:
crc = 0xFFFF
for b in data:
crc ^= b
for _ in range(8):
if crc & 1:
crc = (crc >> 1) ^ 0xA001
else:
crc >>= 1
return crc.to_bytes(2, "little") # Modbus は LSB ファースト
# 使用例: スレーブ番号 1, FC03, アドレス 0, 1 ワード
# (ser は 4.1 で開いたポートを使う前提の断片コードです)
frame = b"\x01\x03\x00\x00\x00\x01"
frame_with_crc = frame + crc16_modbus(frame)
ser.write(frame_with_crc)
もちろん、Modbus RTU そのものを書くなら pymodbus を使うほうが早いです。CRC を自前で書く必要があるのは、独自バイナリプロトコルの実装時です。
7. RS-485 半二重の制御
ソフトの話に入る前に——RS-485 の現場トラブルの大半は配線です。通信できないとき、コードを直す前にこの 3 点を疑ってください。
- A / B 線の極性逆接続: メーカーによって A/B(D+/D−)の呼称が逆のことがあり、逆に繋ぐと一切通信できない。入れ替えて試すのが最速
- 終端抵抗(120Ω)の入れ忘れ / 二重挿入: バスの両端にだけ入れる。長距離・高速ほど効く
- スレーブアドレスの重複: マルチドロップで同一アドレスの機器が 2 台いると応答が化ける
📐 マルチドロップ配線の基本形(両端にだけ 120Ω 終端抵抗を入れる):
PC ─── USB-RS485 変換器 ─┬────── 機器 1 ────── 機器 2 ────── ... ────── 機器 N
│ (addr=1) (addr=2) (addr=N)
[120Ω] [120Ω]
(最上流に終端) (最下流に終端)
・A / B(D+ / D−)の 2 本を全機器で共通のバスとして数珠繋ぎ(デイジーチェーン)
・終端抵抗はバスの物理的な両端にだけ 1 本ずつ(中間の機器には入れない)
・スレーブアドレスは全機器で必ず別々に(工場出荷時の既定=1 のまま接続すると衝突)
・最大距離の目安: 9600 bps で約 1.2 km、通信速度と反比例(115200 bps だと数十 m)
スター配線(マスタから複数機器へ枝分かれ)や、終端抵抗をあちこちに入れる配線は、反射・信号品質劣化の原因になります。物理的に一本のバスをデイジーチェーンで通し、両端にだけ 120Ω——これが唯一の正解です。
RS-485 の 2 線式は「半二重」で、送信と受信が同じ線を共有します。送信中は受信できず、受信中は送信できません。多くの USB-RS485 変換器は内部で自動的に方向制御してくれますが、安価な製品ではユーザー側で RTS / DTR を切り替える必要があります。
# 送受信の前後で RTS を切り替える例
ser.rts = True # 送信モード
ser.write(frame)
ser.flush() # 送信完了を待つ
ser.rts = False # 受信モード
response = ser.read(64)
結論を先に言うと、迷ったら方向制御をハードウェアで自動処理する変換器(Auto Direction Control 対応品)を買ってください。数千円の差で、この節の面倒ごとが全部消えます。以下の RTS 手動制御は、安価な変換器しか使えない場合の回避策です。
このコードには 2 つ注意があります。第一に、RTS の極性(True が送信なのか受信なのか)は変換器のハードによって逆のことがあるため、必ず変換器の仕様書で確認してください。第二に、flush() は OS バッファの掃き出しを待つだけで、UART から最終ビットが出きるまでは保証しません。「送信が終わってから受信に切り替える」タイミングがシビアで、フレームの末尾が削れる事故が頻発します——確実にするなら flush() の後に「送信バイト数 × 1 文字あたりの時間」ぶんのウェイトを足します。この面倒さこそ、冒頭で ADC 対応変換器をすすめた理由です——それなら方向制御のコードは一切不要で、通常の serial.Serial だけで書けます。なお pyserial には RTS をソフトで自動切替する serial.rs485.RS485 クラスもありますが、公式ドキュメント自身が「制御信号がデータと同期しない場合がある。OS の遅延精度は数十ミリ秒単位で信頼できないことがある」と警告しており、本番用途にはおすすめしません。Linux の場合は RS485 モードを kernel に通知するインターフェース(ioctl(TIOCSRS485, ...))があるので、ハードウェアが対応していれば任せられます。
8. Windows での COM ポートの扱い
8.1 ポート番号の固定
USB-シリアル変換ケーブルを抜き差しすると、Windows が COM ポート番号を変えてしまうことがあります。本番運用では「デバイスマネージャー → ポートのプロパティ → 詳細設定」で COM 番号を固定するか、シリアル番号で識別するコードにします。
import serial.tools.list_ports
for port in serial.tools.list_ports.comports():
print(f"{port.device}: {port.description} (serial={port.serial_number})")
USB-シリアルのシリアル番号で目的の機器を見つけ、port.device(COM3 などのパス)を取り出してから serial.Serial に渡すと、ポート番号が変わっても動き続けます。
8.2 ドライバ
- FTDI 系(FT232R など): 汎用性が高く Windows / Linux / Mac で安定
- Prolific 系(PL2303): 安価だが要注意。旧チップ(PL2303HXA / TA など)は偽造品対策のため公式ドライバで意図的に無効化されており、Windows 11 では「PHASED OUT SINCE 2012」と表示されて動作しません。新規購入なら現行チップ(PL2303GC 等)搭載品を確認すること
- CH340 系: 中華系の安価製品でよく見る。Windows 11 では公式ドライバの動作確認推奨
業務用なら FTDI 系の選定が無難です。
9. 24 時間運用に耐える接続マネージャ
シリアルポートも切断や読み取りタイムアウトが起こります。TCP 通信のときと同じく、自動再接続できるラッパを用意しておくと安心です。
import logging
import threading
import time
import serial
logger = logging.getLogger(__name__)
class SerialClient:
def __init__(self, port: str, baudrate: int = 9600, **kwargs):
self.port = port
self.baudrate = baudrate
self.kwargs = kwargs
self._ser: serial.Serial | None = None
# RLock(再入可能ロック)にするのが重要。query() がロックを持ったまま
# open() を呼ぶため、通常の Lock だと再接続時にデッドロックする
self._lock = threading.RLock()
def open(self) -> None:
with self._lock:
if self._ser and self._ser.is_open:
return
self._ser = serial.Serial(
port=self.port,
baudrate=self.baudrate,
timeout=1.0,
write_timeout=1.0,
**self.kwargs,
)
logger.info("opened %s @ %d", self.port, self.baudrate)
def close(self) -> None:
with self._lock:
if self._ser and self._ser.is_open:
self._ser.close()
logger.info("closed %s", self.port)
def query(self, request: bytes, response_len: int) -> bytes:
"""要求を送って応答を読む。失敗したら 1 度だけ再接続して再試行。"""
for attempt in range(2):
try:
with self._lock:
if not self._ser or not self._ser.is_open:
self.open()
assert self._ser
self._ser.reset_input_buffer()
self._ser.write(request)
self._ser.flush()
response = self._ser.read(response_len)
if len(response) != response_len:
# 応答不足も再試行の対象にするため、except 節で捕捉できる例外を使う
raise serial.SerialTimeoutException("short read")
return response
except (serial.SerialException, serial.SerialTimeoutException) as e:
logger.warning("query failed (attempt %s): %s", attempt + 1, e)
self.close()
time.sleep(0.5)
raise RuntimeError("query failed after retry")
reset_input_buffer(): 直前のゴミデータを捨ててから送信開始flush(): OS 送信バッファの掃き出しを待つ(厳密な限界は 7 章の注意を参照)_lock: 複数スレッドから同時アクセスがあるとフレームが混ざるので排他制御。RLock(同じスレッドなら重ねて取得できるロック)を使う点が急所で、通常のLockにすると再接続経路で自分自身を待ち続けるデッドロックになる
10. デバッグの定石
10.1 まずは生データをダンプ
raw = ser.read(64)
print(" ".join(f"{b:02X}" for b in raw))
「何バイト来ているか」「終端は何か」「先頭は何か」をまず目で見ます。プロトコル仕様書と突合するときの基本動作です。
10.2 ロジックアナライザで波形を見る
本格的に分からないときは、ロジックアナライザ(Saleae Logic など。安価な互換機なら数千円)でビット波形を見るのが確実です。⚠️ ただし電気的な注意があります——ロジックアナライザの入力は 3.3〜5V のロジックレベル前提なので、RS-232C の ±12V 系信号を直結すると入力段を壊す恐れがあります。観測するのは USB-シリアル変換器の後段(TTL レベル側)にするか、RS-232 レベルにはレベル変換や専用のアナライザを使ってください。
10.3 PuTTY / Tera Term で対話
機器が ASCII 系のコマンドを使う場合、まずは PuTTY や Tera Term で手打ちで通信が成立することを確認すると、Python 実装に進む前に問題が切り分けられます。
11. おわりに
シリアル通信は、表面的にはレガシー技術ですが、現場では決して消えない「枯れた信頼性」を持つ通信手段です。Python と pyserial の組み合わせで、温調器・流量計・古い PLC・計測機器のような機器とも無理なく対話できるようになります。
ボーレートとパリティを正しく合わせ、フレーム境界と CRC を扱い、半二重とポート番号の問題を回避——この 4 点を押さえれば、シリアル通信で詰むことはほぼなくなります。
⚠️ 実機接続時の注意: 本記事のサンプルは検証用(ダミー機器・ループバック・シミュレータ想定)です。実際の産業機器・PLC・温調器・流量計等に接続する場合は、必ず読み出し専用モードで先に疎通確認し、書き込み(設定変更・制御コマンド)は安全インターロック・計画停止のうえで実施してください。RS-485 の配線変更(A/B 極性・終端抵抗・アドレス変更)や機器の電源投入順序は、機器のマニュアル・保全側の手順書に従うのが前提です。24 時間稼働機器の停止・書き込みは、自社の安全基準と保全側の承認を経ることが前提です。本記事のコード・配線例による損害・事故(機器故障・ライン停止・人身事故を含む)について筆者・GenbaPy は責任を負いません。