見出し画像

​【CAN】J1939 数千円のPC環境で始める実践・検証入門(Raspberry Pi または Linux PC 編)

​CAN(Controller Area Network)および上位プロトコルのJ1939は、トラックやバス、建設機械、農業機械などの大型商用車で標準的に使われている非常に重要な通信規格です。

​「実機に触れる前に、まずは手元でJ1939の動きを体験したい」というエンジニアに向けて、高価なモーターや特殊な実機ハードウェアを一切使わず、PCと数千円の安価なツールだけで完結する実践的な学習・検証環境の構築手順を徹底解説します。

​概念的な理解から、ハードウェアの準備、オープンソースを活用したシミュレータ環境の構築、そして実際に生のJ1939フレームを送信・解析するまでの全工程を、手を動かしながら学べるステップバイステップの形式でお届けします。


​1. はじめに:高価なハード不要でJ1939を学ぶアプローチ

​J1939の仕様書(SAE J1939)を机上で読むだけでは、プロトコルの本当の挙動(タイミング、アドレス競合、マルチパケット通信など)はなかなか身につきません。しかし、本物の建機用ECUやモーター、高価な車載ネットワークアナライザ(Vector社製Vector CANalyzerなど)を個人や検証初期段階で揃えるのは現実的ではありません。

​そこで本記事では、以下の**「代替アプローチ」**をとります。

  • 制御対象(モーター等)の代替:Pythonスクリプトによる「仮想ECU」

  • 通信解析ツールの代替:オープンソース(Linux SocketCAN / Wireshark)や安価なUSB-CANインターフェース

​この構成であれば、総額数千円(ハードウェア代のみ、PC除く)で、商用ツールと同等の本格的なJ1939フレームの送受信実験が可能です。

​2. J1939クイック基本知識(実践のための必要最小限)

​実践に入る前に、J1939を扱う上で絶対に外せない4つの基本概念を頭に入れておきましょう。

​2.1. CAN(2.0B拡張フレーム)との関係

​J1939は、CANの29ビット識別子(拡張ID)を細分化して上位の通信ルール(アプリケーション層)を定義したものです。データ長は基本的に8バイト(マルチパケット時はそれ以上)です。

​2.2. 29ビットIDの構造

​J1939では、29ビットのCAN IDが以下のようにカプセル化されています。

 28 26 25 24 23         16 15          8 7           0
+------+--+--+------------+------------+--------------+
| Priority|R |DP|    PF      |    PS      |  Source Addr |
+------+--+--+------------+------------+--------------+
             \<------- PGN (Parameter Group Number) ------->
  • Priority(優先度): 3ビット(0が最優先、7が最低優先度。通常は3や6)。

  • R / DP: 予約ビットとデータページ(通常は両方0)。

  • PF(PDU Format): 8ビット。メッセージのタイプを決めます。

    • ​0〜239:PDU 1(宛先指定通信。PSは宛先アドレス)。

    • ​240〜255:PDU 2(ブロードキャスト通信。PSはPGNの拡張部)。

  • PS(PDU Specific): 8ビット。PFによって「宛先アドレス」か「PGNの拡張」になります。

  • Source Address(送信元アドレス, SA): 8ビット。どのECUが送ったかを示します。

​2.3. PGN(Parameter Group Number)とは?

​J1939で最も重要な単位です。データの「グループ名」に相当します。

例えば、エンジン回転数や車速が含まれる有名なPGNは 61444(0xF004)(Electronic Engine Controller 1: EEC1)です。

​2.4. SPN(Suspect Parameter Number)とは?

​PGNの中に含まれる、個々の具体的なデータ項目(信号)です。

例えば、上記PGN 61444(8バイト)の中に、以下のSPNが含まれています。

  • SPN 190: エンジン回転数(2バイト、データ位置:4-5バイト目)

​3. 実践環境の選定:2つのルート

​手元で試す方法として、予算や手持ちの環境に合わせて2つのルートから選べます。本記事では、汎用性が高くLinux環境があれば無料で試せる**【ルートA】**をメインに解説します。

​【ルートA】Raspberry Pi または Linux PC + 安価なCANモジュール(推奨)

  • メリット: Linux標準の「SocketCAN」が優秀で、J1939プロトコルスタックがカーネルレベルで標準実装されている。Wiresharkでそのまま解析可能。

  • 費用: 約3,000円〜(CANモジュール代)

​【ルートB】Windows PC + USB-CANコンバータ(Python環境)

  • メリット: 普段使いのWindows PCで完結。

  • 費用: 約5,000円〜(コンバータ代)

​今回は、最もプロトコルの生挙動が見えやすい**「ルートA(Linux / Ubuntu / Raspberry Pi OS)」**をベースに解説を進めます。

​※PCがWindowsしかない場合:Windows上に仮想環境(VirtualBoxやVMware)でUbuntuを構築し、USB-CANモジュールを仮想マシンにパススルー(認識)させれば、ルートAの手順がそのまま使えます。また、WSL2(Windows Subsystem for Linux)でもUSBマウントツール(usbipd)を使えば可能です。

ルートBの Windows PC 環境版はコチラ👇️


​4. ハードウェアの準備(数千円で揃える)

​高価なモーターの代わりに、「2つのノード(送信側と受信側)が相互に通信する環境」を作ります。一番簡単なのは、1台のPC(またはRaspberry Pi)に2つのCANポートを持たせ、それらを物理的に結線することです。これによって、自分で投げたJ1939フレームを自分で受け取る「自己完結型ループバック環境」が作れます。

​必要なパーツ一覧

  1. USB-CANインターフェース(2ポート分、または1ポート×2個)

    • ​おすすめ:gs_usbファームウェア互換のUSB-CANモジュール。Linuxでドライバ不要で認識し、SocketCANでそのまま動くためトラブルが非常に少ないです。

    • ​例:「Canable 2.0」のクローン品や、「Waveshare USB-TO-CAN」など(AmazonやAliExpressで2,000〜4,000円程度で購入可能)。

    • ​※Raspberry Piを使う場合は、SPI接続の「MCP2515 CAN Hat」でも代用可能です。

  2. ジャンパ線(2本)

    • ​CAN_H同士、CAN_L同士を接続するため。

  3. 終端抵抗(120Ω)× 2個

    • ​CANバスの両端に必要な抵抗です。多くのUSB-CANモジュールには、基板上にジャンパピンやスイッチでON/OFFできる120Ω抵抗が内蔵されているため、それをONにすれば外付け抵抗は不要です。

​物理結線の手順

  1. ​モジュールAの CAN_H と、モジュールBの CAN_H をジャンパ線で繋ぐ。

  2. ​モジュールAの CAN_L と、モジュールBの CAN_L をジャンパ線で繋ぐ。

  3. ​両方のモジュールの終端抵抗スイッチ(120Ω)を有効にする。

  4. ​両方のUSB端子をPCに接続する。

+------------------+                    +------------------+
|  USB-CAN (can0)  |                    |  USB-CAN (can1)  |
|                  |   CAN_H (ジャンパ) |                  |
|          [CAN_H] |--------------------| [CAN_H]          |
| [120Ω]   [CAN_L] |--------------------| [CAN_L]   [120Ω] |
| (ON)             |   CAN_L (ジャンパ) |             (ON) |
+------------------+                    +------------------+
        |                                        |
     (USB)                                    (USB)
        +------------------+-----------------+
                           |
                     [ Linux PC ]

5. ソフトウェア環境の構築(Linux / SocketCAN)

​ハードウェアが接続できたら、Linux上で通信ができるように設定を行います。

​5.1. CANインターフェースの認識確認

​ターミナルを開き、以下のコマンドを実行してカーネルがUSB-CANを認識しているか確認します。

dmesg | grep -i usb

gs_usb などのドライバがロードされ、can0、can1 といったネットワークインターフェースが生成されていれば成功です。

​5.2. CANツールのインストール

​CANフレームをパケットレベルで操作するためのユーティリティツール一式(can-utils)と、解析用のWiresharkをインストールします。

sudo apt-get update
sudo apt-get install can-utils wireshark -y

(※Wiresharkのインストール中に「非ルートユーザーにパケットキャプチャを許可するか?」と聞かれたら「はい」を選択しておくと便利です。)

​5.3. リンクアップ(通信速度の設定)

​J1939規格では、伝統的に通信速度は250 kbps、近年の新しい車両では500 kbps(CAN FDの場合はさらに高速)と規定されています。ここでは250 kbpsでリンクアップさせます。

​以下のコマンドで can0 と can1 を立ち上げます。

sudo ip link set can0 up type can bitrate 250000
sudo ip link set can1 up type can bitrate 250000

確認コマンド:

ip -details link show can0

状態が UP になっており、bitrate 250000 と表示されていれば準備完了です。

​5.4. 疎通確認(生CANレベル)

​まずはJ1939のルールを無視して、単純にCANフレームが物理的に行き来するかテストします。

​ターミナルを2つ開きます。

ターミナル1(受信側: can1):

candump can1

ターミナル2(送信側: can0):

cansend can0 123#AABBCCDD

ターミナル1に can1  123   [4]  AA BB CC DD と表示されれば、物理結線およびCANコントローラの動作は完璧です。動かない場合は、CAN_H/Lの逆接続、終端抵抗の入れ忘れ、ビットレートの不一致を疑ってください。

​6. J1939プロトコルを動かす(SocketCAN J1939)

​ここからが本題です。Linuxカーネルには、標準でJ1939プロトコルスタックが組み込まれています。これを利用すると、面倒な29ビットIDの計算や、長大なデータの分割送信(マルチパケット通信)をカーネルが自動で処理してくれます。

​6.1. J1939カーネルモジュールのロード

​通常は自動でロードされますが、念のため手動でロードします。

sudo modprobe can-j1939

6.2. J1939のアドレス(SA)割り当て

​J1939では、ネットワーク上の各ノード(ECU)が一意のアドレス(0〜253)を持つ必要があります。

今回は以下のように見立てて設定を行います。

  • can0(送信側ノード): アドレス 0x1C(例:作業アタッチメントやサードパーティ製デバイスを想定)

  • can1(受信側ノード): アドレス 0x00(例:エンジンコントロールモジュール: ECMを想定)

​LinuxのSocketCAN J1939では、アドレスのバインドを j1939addr で行います。

# can0 にアドレス 0x1C (28) を割り当て
sudo j1939addr add dev can0 sa 0x1c

# can1 にアドレス 0x00 (0) を割り当て
sudo j1939addr add dev can1 sa 0x00

7. 実践演習1:ブロードキャスト通信(PDU2)の送受信

​まずはJ1939で最も多用される、不特定多数への垂れ流し通信(ブロードキャスト)を体験します。

ターゲットにするのは、先ほど紹介したエンジン回転数を含むPGN 61444(0xF004) です。

​7.1. 受信状態のセットアップ

​ターミナル1で、J1939専用のダンプツール j1939dump を起動します。

# can1ポートで、J1939フレームを監視
j1939dump -v can1

7.2. 送信(擬似データの作成)

​別のターミナル2から、j1939send コマンドを使ってデータを送信します。

  • 送りたいPGN: 61444(十六進数で 0xF004)

  • 送信元アドレス(SA): 0x1C (can0に設定したアドレス)

  • データ(8バイト): 例として 0xFF 0xFF 0xFF 0x10 0x27 0xFF 0xFF 0xFF

    • ​※J1939では、無効な項目や未使用エリアは 0xFF で埋めるルールがあります。

    • ​4〜5バイト目の 0x10 0x27 は、インテル方式(リトルエンディアン)で 0x2710 = 十進数で 10000 を意味します。J1939のエンジン回転数の分解能は 0.125 rpm/bit なので、10000 * 0.125 = 1250 rpm というデータを模しています。

​以下のコマンドを実行します。

j1939send -d can0 -s 0x1c -p 0xf004 ffffffff1027ffff

7.3. 結果の解析

​受信側のターミナル1(j1939dump)に以下のような行が表示されます。

can1  1C  F004  00  8  ff ff ff 10 27 ff ff ff

左から、「受信ポート」「送信元アドレス(1C)」「PGN(F004)」「宛先アドレス(00=ブロードキャスト時は意味を持たない)」「データ長(8)」「生データ」です。

​カーネルが、自動的に29ビットCAN ID(この場合は 0x0CF0041C など)を組み立てて送信し、受信側でパースしてくれたことがわかります。

​8. 実践演習2:長大データの送信(マルチパケット通信:TP)

​J1939の真骨頂であり、自作すると最も面倒なのが**「マルチパケット通信(Transport Protocol: TP)」**です。CANの1フレームには8バイトしか入りませんが、車両の故障コード(DM1文字列)や、ECUの識別情報(PGN 65242など)は8バイトを超えます。J1939では、これを最大1785バイトまで自動分割して送る仕組みがあります。

​これを高価なハードなしで実験してみましょう。

​8.1. 9バイト以上のデータを送信する

​送信元アドレス 0x1C から、宛先アドレス 0x00(エンジンECU)に向けて、20バイトのデータを送ってみます。

使用するPGNは、コンポーネント識別情報を示す 65242(0xFEEA) にします。

​受信側(ターミナル1)はそのまま j1939dump -v can1 を流しておきます。

送信側(ターミナル2)で、20バイト(十六進数で40文字)のデータを指定して送信します。

j1939send -d can0 -s 0x1c -p 0xfeea -a 0x00 0102030405060708090a0b0c0d0e0f1011121314

8.2. 受信側の見え方(J1939レイヤ)

​j1939dump を見ているターミナル1には、以下のように綺麗に結合された「20バイトのデータ」が1行で表示されます。

can1  1C  FEEA  00  20  01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14

裏側で行われた複雑なパケット分割・結合を、Linuxカーネルがすべて自動処理してくれた結果です。

​8.3. Wiresharkによる「生の挙動」の可視化

​ここで、グラフィカルなパケット解析ツール Wireshark を起動して、裏で実際に流れた「生のCANフレーム」を覗いてみましょう。J1939がどのようにしてデータを分割しているかのカラクリが視覚的に理解できます。

1.Linuxのデスクトップ環境で Wireshark を起動します。

sudo wireshark

2.インターフェース一覧から can1 を選択し、キャプチャを開始します。
3.上部のフィルター欄に j1939 と入力して適用します。
4.もう一度、先ほどの20バイト送信コマンド(j1939send)を実行します。

Wiresharkの画面には、以下の4つのCANフレームが連続して流れるのが見えます。

  1. TP.CM (Connection Management) BAM / RTS

    • ​ID: 0x1CEC001C

    • ​データ: 10 14 00 03 ff ea fe 00 ...

    • ​解説:「これからデータを送るぞ」というアナウンス。データ総数が20バイト(0x0014)、パケット数が3つ(0x03)、対象PGNが 0xFEEA であることを通知しています。

  2. TP.DT (Data Transfer) - パケット1

    • ​ID: 0x1CEB001C

    • ​データ: 01 01 02 03 04 05 06 07

    • ​解説:1バイト目がシーケンス番号(01)。続く7バイトが実データ。

  3. TP.DT (Data Transfer) - パケット2

    • ​ID: 0x1CEB001C

    • ​データ: 02 08 09 0a 0b 0c 0d 0e

    • ​解説:シーケンス番号(02)と、続く7バイト。

  4. TP.DT (Data Transfer) - パケット3

    • ​ID: 0x1CEB001C

    • ​データ: 03 0f 10 11 12 13 14 ff

    • ​解説:シーケンス番号(03)。データは6バイトしか残っていないため、最後は 0xFF でパディング(穴埋め)されています。

​高価なモーターやECUがなくても、このWiresharkのログを見るだけで、J1939の仕様書に書いてある「接続管理(TP.CM)とデータ転送(TP.DT)のハンドシェイク」の本質が完全に理解できます。

9. 実践演習3:Pythonによる「仮想建機ECU」シミュレータの作成

​コマンドの送受信だけでは「実務感」が足りないため、次はPythonを使って、**「エンジン回転数を一定周期でブロードキャストしつつ、特定の要求に対してステータスを返答する仮想ECU」**をスクリプトとして作成します。

​これにより、高価なハードウェアを完全にソフトウェアで擬似化(シミュレート)できます。

​9.1. 準備:python-canのインストール

​PythonからSocketCANを扱うためのライブラリをインストールします。

pip install python-can

9.2. シミュレータコードの実装(virtual_ecu.py)

​以下のコードをファイル名 virtual_ecu.py として保存します。このスクリプトは can0 側に常駐し、実物のエンジンECUのように振る舞います。

import time
import threading
import can

# CAN設定 (SocketCANを使用)
bus = can.interface.Bus(channel='can0', bustype='socketcan')

def make_j1939_id(priority, pgn, source_address):
    """J1939の各要素から29ビットのCAN IDを合成する"""
    pf = (pgn >> 8) & 0xFF
    ps = pgn & 0xFF
    
    can_id = (priority & 0x07) << 26
    can_id |= pf << 16
    can_id |= ps << 8
    can_id |= source_address & 0xFF
    return can_id

def broadcast_engine_status():
    """エンジン状態(PGN 61444)を100ms周期で送信するループ"""
    print("[ECU] エンジン状態の定周期送信を開始しました。 (100ms)")
    rpm = 800.0  # 初期アイドリング回転数
    
    while True:
        # 擬似的に回転数を変動させる
        rpm += 0.5
        if rpm > 1200: rpm = 800.0
        
        # J1939の回転数変換ルール: 1 bit = 0.125 rpm
        raw_rpm = int(rpm / 0.125)
        
        # 8バイトのデータバッファ作成
        data = [0xFF] * 8
        # SPN 190 (エンジン回転数) は 4-5バイト目 (リトルエンディアン)
        data[3] = raw_rpm & 0xFF
        data[4] = (raw_rpm >> 8) & 0xFF
        
        # PGN 61444 (0xF004), 優先度 3, 送信元アドレス 0x00 (Engine)
        can_id = make_j1939_id(priority=3, pgn=0xF004, source_address=0x00)
        
        msg = can.Message(arbitration_id=can_id, data=data, is_extended_id=True)
        try:
            bus.send(msg)
        except can.CanError:
            print("送信エラー")
            
        time.sleep(0.1) # 100ms待機

def listen_requests():
    """外部からのリクエスト(PGN 59904)を待ち受けるループ"""
    print("[ECU] 要求フレーム(PGN 59904)の監視を開始しました。")
    while True:
        msg = bus.recv()
        if msg is None:
            continue
            
        arbitration_id = msg.arbitration_id
        pf = (arbitration_id >> 16) & 0xFF
        ps = (arbitration_id >> 8) & 0xFF
        sa = arbitration_id & 0xFF
        
        if pf < 240:
            pgn = pf << 8
        else:
            pgn = (pf << 8) | ps
            
        # PGN 59904 (0xEA00) は「Request PGN」
        if pgn == 0xEA00:
            # 要求されたPGNはデータフィールドの3バイト(リトルエンディアン)
            requested_pgn = msg.data[0] | (msg.data[1] << 8) | (msg.data[2] << 16)
            print(f"[ECU] アドレス 0x{sa:02X} から PGN {requested_pgn} の要求を受信しました。")
            
            # 例:コンポーネント識別情報(PGN 65242)を要求されたらレスポンスを返す
            if requested_pgn == 65242:
                response_data = list("VIRTUAL-ENGINE-V1.0".encode('utf-8'))
                response_data = (response_data[:8] + [0xFF]*8)[:8]
                
                # PGN 65242 (0xFEEA), 送信元 0x00
                res_id = make_j1939_id(priority=6, pgn=0xFEEA, source_address=0x00)
                res_msg = can.Message(arbitration_id=res_id, data=response_data, is_extended_id=True)
                bus.send(res_msg)
                print("[ECU] 要求されたコンポーネント識別情報を返答しました。")

if __name__ == "__main__":
    t1 = threading.Thread(target=broadcast_engine_status, daemon=True)
    t2 = threading.Thread(target=listen_requests, daemon=True)
    
    t1.start()
    t2.start()
    
    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        print("\nシミュレータを終了します。")

9.3. シミュレータの実行と対向ノード(can1)での検証

1.ターミナル1で、このシミュレータを実行します。

python3 virtual_ecu.py

2.ターミナル2(受信側:can1)で、j1939dump を使って確認します。

j1939dump -v can1

100msごとに、エンジン回転数が少しずつ変化しながらデータが送られてくる様子がリアルタイムで観察できます。

3.ターミナル3から、この仮想ECUに対して「要求フレーム」を投げてみます。J1939の「要求(Request)」は PGN 59904(0xEA00)です。データに、要求したいPGN(65242 = 0x00FEEA)をリトルエンディアン(ea fe 00)で入れます。

# can1(アドレス 0x1C) から、全ノードに向けて PGN 65242 を要求
cansend can1 18EAFF1C#EAFE00

4.シミュレータ側のターミナルを見ると、リクエストを検知してコンポーネント情報を即座に応答(レスポンス)するログが出力されます。

10. ステップアップ:さらに理解を深めるための応用課題

​ここまでの環境ができれば、J1939の基本骨格はすべて手元で実験可能です。実製品に取り組む前の仕上げとして、以下の応用をコードを改造しながら試してみることをお勧めします。

​応用A:アドレス競合とアドレスクレーム(Address Claim)の再現

​J1939ネットワークに新しいECUが参加するとき、すでに同じアドレスを使っているノードがいたらどうなるでしょうか?J1939には、自身の「NAME(64ビットの固有識別子)」を主張し合ってアドレスを奪い合う、または譲る **「アドレスクレーム(PGN 60928)」**という重要な手続きがあります。

  • ​Pythonコードをもう1つ作り、同じアドレス 0x00 で異なるNAMEを持たせて同時にメッセージを投げさせ、Wiresharkでアドレスクレームのパケットがどう飛ぶかを観察してみてください。

​応用B:DM1(アクティブな故障コード)のシミュレート

​商用車メンテナンスにおいて最重要となるのが、故障診断(Diagnostics)です。J1939では PGN 65226(DM1)を使って、現在発生しているエラーコード(SPNおよびFMI)を毎秒ブロードキャストします。

  • ​先ほどのPythonコードを拡張し、特定の条件(例:擬似的なオーバーヒート)になったら、DM1フレームを発生させて宛先に通知するロジックを作ってみましょう。

​11. まとめ:この練習が実製品開発にどう活きるか

​今回構築した環境は、安価でコンパクトですが、「本物の大型トラックの車内ネットワークで起きていること」と全く同じ現象を再現しています。

​実製品開発(例えば、J1939対応のインジケータや、ゲートウェイ機器、制御コントローラの開発など)に移行した際、必ず以下のようなトラブルに直面します。

  • ​「データが正しくパースできない(エンディアンや分解能の計算ミス)」

  • ​「マルチパケットの取りこぼしが発生する(タイミング問題)」

  • ​「バスが輻輳してプライオリティの低いメッセージが遅延する」

​これらを、実際の重機や高価なモーターが回っている危険な「実機環境」でデバッグするのは非常に困難であり、時間もかかります。

しかし、手元に今回のような**「PC内で完結するSocketCANシミュレータ環境」**があれば、自作のプログラムや機器がJ1939の仕様に準拠しているかを、安全に、何度でも、無料で検証することができます。この「ツールを自作してシミュレートする力」こそが、車載・建機ネットワーク開発を成功させる強力な武器になります。ぜひ最初の第一歩として、試してみてください!

次は実際に制御してみる👇️

ハードウェア無しでバソコンのみで体験したい👇️


いいなと思ったら応援しよう!