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 文字時間以上の無通信」をフレーム境界とみなす方式が使われます。pyserialinter_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 は責任を負いません。

関連記事

参考文献・一次情報