ラズベリーパイ用のRS485通信HAT基板作ってみた

## 今回設計する基板
![IMG_1994.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3909178/8d0c2c05-1923-47e0-a278-7a80b4181bcf.jpeg)
> RS485通信HAT基板
> https://raspikoubou.theshop.jp/items/103737327


<br>

## RS485通信とは
ネットで検索したらたくさん情報が出てきますので詳細は割愛しますが、
RS485は、産業機器や制御システムでよく使われるシリアル通信規格の一つです。

2本の信号を使った差動通信を使っており、下記のような特徴があります。
・長距離通信が可能（最大約1200m）
・ノイズに強い（差動信号を使用）
・1対多通信が可能（最大32台のデバイス接続）
・Ethernet/IPなど高機能な通信と比較するとシンプル、安価


![無題.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3909178/bbfcd3e9-0057-4eca-98f1-1791ec1f971d.jpeg)
> 産業用ネットワークの種類 (https://emb.macnica.co.jp/articles/4277/)

<br>

RS485の仕様(TIA/EIA-485規格)は電気的な仕様が定義してあるだけです。
例えば、イーサネット通信ではLANケーブル（RJ-45コネクタ）のようなコネクタの規格やプロトコルの規格が定められていますが、RS-485はそれらの規格はありません。
そのためRS485では、オリジナルのプロトコルを作ってLANケーブルを使って通信を行っても、ねじ端子台と適当なケーブルを使って通信してもRS485通信だと言えるのです。(それが適切かどうかは別として)
> EIA-485 (https://ja.wikipedia.org/wiki/EIA-485)

このように物理的な接続やプロトコルに関する共通規格が定められていないため、例えば三菱電機のPLCで用いられる「CC-Link」のように、その名称に「RS-485」という文字が含まれていなくても、内部的にRS-485通信を利用している場合があります。このようにして、様々な機器やシステムにおいてRS-485通信が広く使われています。
> CC-Link (https://www.mitsubishielectric.co.jp/fa/products/cnt/plcq/pmerit/network/cclink.html)

<br>

RS-485通信は長く使われている規格で、(TIA/EIA-485-A規格は1998年策定)、その歴史は長いです。
現在、EtherCATやEtherNet/IPなどRS-485より桁違いに高速な通信規格も登場していますが、それでもなおRS-485は現場で利用されています。

その理由として、まずこれらの新しい高速通信規格と比較してRS-485通信は構成がシンプルであるという点が挙げられます。安価なマイコンではEthernet通信機能の実装が難しい場合でもRS-485であれば比較的容易に実装できます。
また、通信プロトコルもEthernet系の規格と比較して軽量なため、通信速度自体は劣るものの、プロトコルを工夫することである程度のリアルタイム性を確保することも可能です。

このように、通信速度は比較的低速ですが、そのシンプルさ、安価さ、長距離伝送が可能であること、そして実装に必要なリソースが少ない（プロトコルが軽量である）ことなどが現在でも使われ続けている主な理由でしょう。

とはいえ、将来的にはEtherCATなどの高速通信規格がより安価になればRS-485はいずれ無くなってしまうのでしょう。

![無題2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3909178/00606661-c00a-42c7-8953-edbdb9fdefd8.jpeg)
>chatGPTで比較表を作ってもらいました
>規格で定められている範囲が全然違うので、比較するのは適当ではない気はしますが。

<br>

## 基板の回路
いきなりですが、今回は下図のHTC社の「MAX3485ED」というRS485トランシーバICを使い、下図のような回路を作成しました。秋月でも購入できます。(https://akizukidenshi.com/catalog/g/g116211/)
RS485通信するためだけのシンプルな基板になります。通信配線と(必要なら)終端抵抗を接続するためのコネクタ(J2,J3)、ラズベリーパイのUARTをRS485に変換するトランシーバICであるMAX3485ED(IC1)、ICのパスコン(C1)だけです。


![無題3.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3909178/f2383a43-4574-4961-b86f-d84c497c7da8.jpeg)
> 作成した回路図

<br>

トランシーバーICのMAX3485EDはラズベリーパイのUART通信信号をRS485に変換します。
UART通信を送るだけではダメで、受信をイネーブルにするピン(RE_)、送信をイネーブルするピン(DE)もコントロールしてあげる必要があります。
(MAX3485EDに限らず、安価なRS485トランシーバーICの大体は同様な機能になっています)

この基板では、イネーブルピン(RE,DE)を1つにまとめてGPIO4でコントロールする構成としています。
ですので、GPIO4をHIGHにすれば送信モード、LOWにすれば受信モードに切り替わります。

ラズベリーパイのGPIOが3.3Vで動作するのに対して、RS485トランシーバーのIOが5Vで動作するものが多いのですが、このMAX3485EDは3.3Vで動作するICでした。そのため、レベル変換回路なしで直接Raspberry Piと接続でき、シンプルにできました。

![無題4.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3909178/4dee3610-b327-4c5e-9404-d8b36b2bea7b.jpeg)
> MAX3485のデータシートより抜粋





<br>

## RS485トランシーバーICのデータシートの読み解き
RS485トランシーバーICには様々な機器が繋がることになります。
そのため、トランシーバーICを選定する際には、接続する機器との整合性も含めて仕様をしっかり確認する必要があります。不十分な場合、問題につながる可能性があります。

自身の備忘録も兼ねて、RS485トランシーバーICを選定する上で考慮すべきいくつかのポイントについて解説していきます。


### 差動出力電圧
差動出力電圧(Vod)のスペックです。今回重要なのは3行目の「With Load RL=54Ω～」の部分です。

**54Ω**というのはTIA/EIA-485-A規格において、ドライバの性能を保証するためのテスト条件として規定された負荷抵抗値です。
[2つの120Ω終端抵抗]と[並列な32単位負荷(12kΩの入力インピーダンスを持ったRS485通信機器が32個)]の並列合成値に相当します。その場合に、少なくとも0.6V以上の差動電圧が出力されるということです。

RS485は200mV以上の動電圧差がないと0/1を認識できませんから、最低でも0.6Vの差動電圧差を出力できるよ、ということです。最大はVccで3.3Vですね。

![5.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3909178/717e8945-825c-4c9b-82ff-44d47e875add.jpeg)
> MAX3485のデータシートより抜粋

<br>

時系列がごっちゃになってしまいますが、基板を作成し、出力されるRS485信号を測定した波形が下記になります。負荷としてA-B間に10kΩ抵抗を入れています。
HIGH側が約**2.8V**、LOW側が約**0.5V**になっています。
理想ではHIGHが3.3V、LOWが0Vとなってほしい所ですが、そうではありません。これはIC内部の抵抗値によるものです。

さらにその下の波形はRS485信号を受信したICのROピンから出力されている波形です。このように差動信号がシングルエンド信号に変換されるので、ラズベリーパイが処理できるわけです。
送信する場合も受信波形のようなシングルエンド信号をDIピンに入力することで、ICがRS485信号に変換してくれます。
![スクリーンショット 2025-04-28 163004.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3909178/71fb743b-d1ec-485f-ba2c-da25cc357662.jpeg)
> RS485信号波形の測定結果

![スクリーンショット 2025-04-29 220847.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3909178/8eb7457b-f370-4052-8233-b430db926fd9.jpeg)
> ICのROピンから出力される波形

<br>

当然ですが、上記データシートの54Ωでの駆動はあくまでドライバIC単体の基本的な駆動能力を保証するもので、現場のあらゆる条件でOKというわけではありません。

できれば設計前に内部の抵抗値等を計算し、接続されている末端の受信機器での信号電圧が何Vになるかを計算しておきたい所です。

データシートには記載されていませんが、恐らく送信部の回路は下図のようになっていると思われます。ですので、トランジスタ(下図で言うQ1~4)のON抵抗が存在します。
このON抵抗が分かれば、あとはケーブルの抵抗などを足して計算すれば受信端での信号電圧が分かり通信可能な仕様かどうかを調べることができます。
(細かいこと言うと、ケーブルや機器の静電容量による立上り速度の遅れなども考えなればならないです)


![6.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3909178/979f924e-e687-42a9-92af-99275c7ba7ff.jpeg)
> RS-485の基礎 (https://e2e.ti.com/blogs_/japan/b/industrial/posts/rs-485-rs-485-rs-485-basics-the-rs-485-driver)

<br>

このICのデータシートではトランジスタON抵抗が分かりませんので、実測してみるしかありません。安いので文句は言えませんが。

ICによっては、下図のように出力電流ードライバ出力電圧が分かる情報が提供されています。負荷から流れる電流が分かれば、信号電圧が何Vになるかを知ることができます。
しっかりしたものを作りたいのであれば、こういうデータシートの情報が豊富なICを選定すると良いでしょう。

![無題.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3909178/61bd1c48-2237-4d3d-af37-56099dd17927.jpeg)
> ADM4850ARZのデータシートから抜粋(https://www.digikey.jp/ja/products/detail/analog-devices-inc/ADM4850ARZ/997646)

<br>

### 入力インピーダンス
RS485規格では、1単位負荷あたりの最小入力抵抗が**12kΩ**と規定されています。このICの入力インピーダンスもデータシートによると最小12kΩになっています。
入力インピーダンスが12kΩの機器であれば、最大32個繋いでRS485通信することができます。

![スクリーンショット 2025-04-28 210231.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3909178/854cd7dd-7e25-4e65-b0b0-ef948aa82e71.jpeg)
> MAX3485のデータシートより抜粋

<br>

32個以上繋ぎたい場合もあると思います。その場合は1/4ULまたは1/8ULに対応したICを選定すれば可能です。
例えば、先ほども一部紹介したADM4850ARZですと、**1/8UL**に対応しています。
下図のとおり、今回使用するICの入力インピーダンス12kΩの**8倍**の**96kΩ**になっています。
8倍になっているので、32個の8倍の256個繋ぐことができます。

![スクリーンショット 2025-04-28 211206.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3909178/39c892dc-ad16-4577-97c6-43a6f96cc371.jpeg)
> ADM4850ARZのデータシートから抜粋(https://www.digikey.jp/ja/products/detail/analog-devices-inc/ADM4850ARZ/997646)

<br>

### データレート
データレートも重要です。基本的には使用する予定のデータレート以上のスペックの物を選べばOKです。
今回のICですと最小10Mbpsです。


![スクリーンショット 2025-04-29 114249.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3909178/21007d5e-4d45-4f6a-875d-8bcdd6c71b71.jpeg)
> MAX3485のデータシートより抜粋

<br>

通信線からの放射ノイズ低減のために、あえて通信速度を低めの物を選ぶというのもありです。
データレートが高ければ高いほど、スルーレート(信号電圧の立下り立下り速度)が上がりますので、信号の矩形波が高周波成分を含み、それらの高周波成分が回路の配線などをアンテナとして放射されやすくなるため、放射ノイズが大きくなる傾向になります。
ノイズ(EMI)対策として、スルーレートに制限をかけて通信速度を小さくしているICも存在します。

![スクリーンショット 2025-04-29 180629.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3909178/39292fa1-284f-4e08-8851-ad2e91d89448.jpeg)
> ADM4850ARZのデータシートから抜粋(https://www.digikey.jp/ja/products/detail/analog-devices-inc/ADM4850ARZ/997646)

<br>

### 真理値表
真理値表もしっかり確認が必要です。私的に大事だと思うのは、"Receiving"の"A-B"が0.2Vの差が無い場合や機器が接続されていない"Inputs open"の場合です。第三者の機器が繋がっていない場合や、ケーブルが断線したなどの場合に受信信号として0がくるのか1がくるのかは把握しておいた方が良いでしょう。
![スクリーンショット 2025-04-29 221626.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3909178/935844ca-97e4-4b75-afb4-cd0357081836.jpeg)
> MAX3485のデータシートより抜粋


<br>

# 基板のCAD設計
パターン設計した物が下記になります。
特に特別な設計はしてないです。本当に回路図通りに繋いだだけの基板です。

WindowsのKicadで設計しています。下記のgithubにCADデータを無償公開しています。
ご使用にあたっては、ご利用者ご自身の責任においてご判断いただきますようお願いします。

![スクリーンショット 2025-04-29 182841.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3909178/f5f10c14-57ec-4766-b93f-ce44139b413d.jpeg)
> Kicadで設計したCAD図面

https://github.com/raspikoubou/rs485HAT.git

<br>

# 完成した基板
完成した基板が下図になります。
試しにラズベリーパイ4とラズベリーパイ5間でRS485通信してみました。
本当は終端抵抗をつけるのですが、面倒くさがってつけずに実験してます。

![IMG_2066.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3909178/05584c85-0d57-434d-96d9-bff88fd94a37.jpeg)
> ラズベリーパイ4とラズベリーパイ5間でRS485通信している画像

<br>

通信するプログラムはENのピンをコントロールしながらserialモジュールで送受信するだけです。
Chat GPTなどの生成AIで大体はできてしまいます。下記でAIで作ったPRGです。

シリアルコンソールで入力された文字列を送信し、受信は別スレッドで動かすというPRGになってます。
市販の機器と通信させるのであれば、modbus RTUなどなんらかのプロトコルに従って通信する必要がありますので、このPRGだとできません。ご注意を。

プロトコル関係なく、送受信切替時のENピンの切替タイミングが重要です。このPRGだと43,54行目のsleepの時間です。リアルタイムOSではなく、ラズベリーパイOSで動かすのであれば処理のばらつきがありますので検証が必要かと思います。
時間を短く設定してしまうと、送信中に受信モードに切り替えてしまい、一部送信できなかったりします。



```
import serial
import RPi.GPIO as GPIO
import time
import threading

# --- 設定 ---
SERIAL_PORT = '/dev/ttyAMA0'  # Pi 5 のプライマリUART (GPIO 14, 15)
BAUD_RATE = 9600             # 通信ボーレート (接続する機器に合わせる)
EN_PIN = 4                   # RS485トランシーバーのEnableピン (GPIO 4)

# --- GPIO設定 ---
GPIO.setmode(GPIO.BCM)       # BCMピン番号を使用
GPIO.setup(EN_PIN, GPIO.OUT) # Enableピンを出力に設定
GPIO.output(EN_PIN, GPIO.LOW) # 初期状態は受信モード (LOW)

# --- グローバル変数 ---
ser = None
stop_thread = False # 受信スレッド停止用フラグ

# --- 送信関数 ---
def send_data(data_to_send):
    """
    指定されたデータをRS485経由で送信する関数
    :param data_to_send: 送信するバイトデータ (例: b'hello') または文字列 (自動でエンコード)
    """
    if not ser or not ser.is_open:
        print("エラー: シリアルポートが開いていません。")
        return

    # 文字列が渡された場合はUTF-8でバイト列にエンコード
    if isinstance(data_to_send, str):
        data_bytes = data_to_send.encode('utf-8')
    elif isinstance(data_to_send, bytes):
        data_bytes = data_to_send
    else:
        print("エラー: 送信データは文字列またはバイト列である必要があります。")
        return

    try:
        # 送信モードに切り替え (ENピンをHIGH)
        GPIO.output(EN_PIN, GPIO.HIGH)
        # トランシーバーが送信モードに切り替わるのを待つ (時間は調整が必要な場合あり)
        time.sleep(0.01)

        # データを送信
        ser.write(data_bytes)
        print(f"送信: {data_bytes}")

        # 送信バッファが空になるのを待つ (重要！)
        ser.flush()

        # 最後のビットが送信されるのを待つ (時間はボーレートとデータ長による)
        # 簡単のため少し待つ。厳密にはボーレートから計算可能。
        time.sleep(0.01)

        # 受信モードに戻す (ENピンをLOW)
        GPIO.output(EN_PIN, GPIO.LOW)

    except serial.SerialException as e:
        print(f"送信エラー: {e}")
    except Exception as e:
        print(f"予期せぬエラー (送信中): {e}")
    finally:
        # エラーが発生しても受信モードに戻す試み
        GPIO.output(EN_PIN, GPIO.LOW)

# --- 受信関数 (スレッドで実行) ---
def receive_data():
    """
    シリアルポートからデータを受信し表示する関数 (別スレッドで実行)
    """
    global stop_thread
    while not stop_thread:
        if ser and ser.is_open and ser.in_waiting > 0:
            try:
                # データがあるだけ読み込む
                received_bytes = ser.read(ser.in_waiting)
                try:
                    # UTF-8でデコード試行
                    received_data = received_bytes.decode('utf-8')
                    print(f"受信 (UTF-8): {received_data}")
                except UnicodeDecodeError:
                    # デコード失敗時はバイト列として表示
                    print(f"受信 (Bytes): {received_bytes}")

            except serial.SerialException as e:
                print(f"受信エラー: {e}")
                # エラー発生時は少し待つ
                time.sleep(0.1)
            except Exception as e:
                print(f"予期せぬエラー (受信中): {e}")
                time.sleep(0.1)
        else:
            # データがない場合は少し待つ
            time.sleep(0.05) # CPU負荷軽減

# --- メイン処理 ---
if __name__ == "__main__":
    try:
        # シリアルポートを開く
        ser = serial.Serial(
            port=SERIAL_PORT,
            baudrate=BAUD_RATE,
            parity=serial.PARITY_NONE,
            stopbits=serial.STOPBITS_ONE,
            bytesize=serial.EIGHTBITS,
            timeout=1 # 受信タイムアウト (秒) - 受信スレッドがあるので短くても良い
        )
        print(f"シリアルポート {SERIAL_PORT} を {BAUD_RATE} bps で開きました。")

        # 受信スレッドを開始
        receive_thread = threading.Thread(target=receive_data, daemon=True)
        receive_thread.start()
        print("受信スレッドを開始しました。")

        # --- メインループ (デモ) ---
        print("メインループ開始。'quit' と入力すると終了します。")
        while True:
            # ユーザーからの入力を待つ
            message = input("送信するメッセージを入力 (または 'quit'): ")

            if message.lower() == 'quit':
                break

            # メッセージを送信
            send_data(message + '\n') # 改行を追加する場合

            # 少し待機 (連続送信を防ぐ)
            time.sleep(0.5)

    except serial.SerialException as e:
        print(f"シリアルポート {SERIAL_PORT} を開けませんでした: {e}")
        print("ヒント: raspi-config でシリアルポートが有効になっているか、")
        print("       'dialout' グループに所属しているか確認してください。")
    except KeyboardInterrupt:
        print("\nプログラムを終了します。")
    except Exception as e:
        print(f"予期せぬエラー: {e}")
    finally:
        # 終了処理
        stop_thread = True # 受信スレッドに停止を指示
        if 'receive_thread' in locals() and receive_thread.is_alive():
            receive_thread.join(timeout=1) # スレッドが終了するのを待つ
            print("受信スレッドを停止しました。")

        if ser and ser.is_open:
            ser.close()
            print("シリアルポートを閉じました。")
        GPIO.cleanup() # GPIO設定をクリーンアップ
        print("GPIOクリーンアップ完了。")
```

# おわりに
今回設計した基板はらずぱい工房ウェブサイトで販売しております。
興味がありましたらご覧ください。
https://raspikoubou.theshop.jp/items/103737327