見出し画像

【CAN】J1939 制御ロジック実装における課題と、実務を安定させる現場ノウハウ

はじめに:なぜ、J1939の規格書通りに構築しても実機が思わぬ挙動を示すのか?

​SAE J1939は、建設機械、農業機械、大型商用車、あるいは近年のハイブリッド・電動パワートレインにおけるデファクトスタンダードとなる上位プロトコルです。

​しかし、ベクターなどのCAN解析ツールをPCに接続し、J1939規格書(J1939/21, /71, /81など)の通りにPGN(Parameter Group Number)やSPN(Suspect Parameter Number)を定義してシミュレータに流し、検証を終えた段階であっても、実機開発においては予期せぬ挙動に直面することがあります。

​実際の開発現場では、以下のような事態への対応を迫られるケースが少なくありません。

  • ​「ベンチテストでは問題なかったが、実機に搭載すると始動直後にエンジンが停止、またはセーフモードに入る」

  • ​「他社製の油圧バルブコントローラを接続したタイミングで、既存のECU群の通信タイムアウトが多発する」

  • ​「アクセル操作に対して、モーターのトルク応答が一瞬遅れる、あるいは細かな振動が発生する」

​ネットで「J1939 制御 方法」と検索しても、出てくるのは「29bit IDの構造(Priority, EDP, DP, PF, PS, SA)」や「3本のCANライン(CAN_H, CAN_L, CAN_SHLD)のトポロジー」といった仕様書の解説ばかりです。

​本記事では、Google検索や生成AIの一般論では辿り着きにくい、**「J1939の仕様と、実機の物理的な特性が組み合わさったときに発生する通信課題」**を整理し、その具体的な回避策をまとめました。

​制御ロジックを実装するチーフエンジニア、システムインテグレーター、そして組み込みファームウェアエンジニア向けに、実戦的なC言語ライクなコード例を交えて解説します。


​第1章:アドレス自動競合解決(J1939/81)のメカニズムと初期化の課題

​〜起動タイムラグが引き起こす一瞬の通信不安定〜

​1-1. 規格の目的と、組み込みハードウェアの起動特性

​J1939/81(ネットワークマネジメント)の大きな特徴は、**Address Claimed(アドレスクレーム)**によるソースアドレス(SA)の動的割り当てです。各ECUは、64bitの「NAME」(メーカーコード、ECUインスタンス、機能等を含む固有ID)を持ち、起動時に Address Claimed PGN (0x00EE00) を送信して自身のSAを宣言します。もし他ノードとアドレスが重複した場合、NAMEの優先度(数値が小さい方が高優先)を比較し、優先度の低い側が別のアドレスを再宣言するか、Null Address(0xFE)に移行して通信を停止します。

​この仕組みは、後付けのオプション機器をCANバスにプラグ&プレイで追加する際には非常に有効に機能します。しかし、動力をリアルタイムに制御するクローズドな車両ネットワークにおいては、初期化プロセスの複雑化を招く要因になります。

​現実の車両では、すべてのECUが同時に起動するわけではありません。

メインコントローラは32bitの高性能MCUでOS(RTOSやLinuxベース)の立ち上がりに500msかかる一方、周辺のセンサーノードはシンプルなマイコンで起動後5msでCANにパケットを出力し始める、といった**「起動タイムラグ」**が必ず存在します。

​1-2. 現場で発生しやすい事象:なぜ初期化直後にコマンドが途切れるのか?

​以下のようなタイムラインで通信の不整合が発生することがあります。

  1. t = 0ms: キーON。全ECUに電源が投入される。

  2. t = 10ms: 最速で立ち上がったサードパーティ製の「アシスト油圧バルブECU」が、設定の不備などにより、本来メインコントローラが使うべき SA: 0x20 を名乗って Address Claimed をブロードキャストする。

  3. t = 50ms: アシストバルブECUは、周囲に競合ノードが検知されないため、「SA: 0x20」が確定したものとして定時通信を開始する。

  4. t = 400ms: メインコントローラ(本来のSA: 0x20)のブートローダーが終了し、アプリケーション層が起動。自身が SA: 0x20 であるとして Address Claimed を送信。

  5. t = 401ms: ネットワーク上でアドレス競合が発生。メインコントローラとアシストバルブECUの「NAME」が比較される。

  6. t = 402ms: メインコントローラの優先度が高かった場合、アシストバルブECUは要求を取り下げ、Null Address(0xFE)に移行するか、別のアドレス(例: 0x80)を再要求する。

​規格通りに調停は完了しますが、メインコントローラ側から見ると、**「t=400msからt=402msのわずかな時間、ネットワーク上で自身のSAが不安定になり、通信処理が一瞬乱れる」**という事象が起きます。

​このタイミングで、エンジンECU(SA: 0x00)に対して送信されるはずの最優先コマンド TSC1 (Torque/Speed Control 1) の送信周期(10ms)にわずかでも乱れが生じると、エンジンECUは安全のために「通信異常」と判断し、フェイルセーフモード(出力制限など)へ移行してしまいます。これが、「キーをONにして作業機を動かそうとすると、最初の1秒間だけエンジンが吹けない、またはエラーランプが点滅する」という現象の背景にあるメカニズムです。

​1-3. 対策:実務におけるアドレス固定とガードタイムの設計

​高い信頼性が求められる制御ネットワーク内においては、原則として動的なアドレス変更(Arbitration Capability)を行わない設計が推奨されます。

​仕様書およびソースコードには、以下のガードロジックを組み込みます。

  • 静的アドレス(Static Address)の指定: サプライヤに対して発行する共通インターフェース仕様書に、「J1939/81 Address Claim に対応すること。ただし、アドレス能力は Non-Configurable(固定)とし、競合時に自動でアドレスをシフトする機能は使用しないこと」と明記します。

  • 初期化ガードタイム(Start-up Guard Time)の導入: メインコントローラの制御ロジック内で、起動直後の一定時間、ネットワークの「監視」だけに徹するフェーズを設けます。

// 起動シーケンス制御のステートマシン例
typedef enum {
    SYS_INIT_HARDWARE,
    SYS_INIT_WAIT_NETWORK, // ← ネットワークの安定を待つ
    SYS_CONTROL_PASSIVE,
    SYS_CONTROL_ACTIVE,
    SYS_FAULT
} SystemState_t;

void Task_SystemSequence(void) {
    static uint32_t startup_timer = 0;
    
    switch(current_state) {
        case SYS_INIT_HARDWARE:
            CAN_Init();
            startup_timer = GetTickCount();
            current_state = SYS_INIT_WAIT_NETWORK;
            break;
            
        case SYS_INIT_WAIT_NETWORK:
            // 500msの間は、他ノードのAddress Claimed (PGN 0xEE00) を受信し、
            // 自身のSA(例: 0x20)と重複するノードがないか監視する。
            // また、この間は自身からの能動的な制御指令は送信しない。
            if (Check_Address_Collision(MY_SOURCE_ADDRESS)) {
                current_state = SYS_FAULT; // 競合検知時はシステム異常処理へ
            }
            if ((GetTickCount() - startup_timer) > 500) { // 500msのガードタイム
                Send_My_Address_Claim(); // 満を持して自身の存在を宣言
                current_state = SYS_CONTROL_PASSIVE;
                startup_timer = GetTickCount();
            }
            break;
            
        case SYS_CONTROL_PASSIVE:
            // さらに200msの間、TSC1の「Control Purpose」を "No Command" で送信し、
            // エンジンやインバータとの通信ハンドシェイクが安定するのを待つ
            Send_TSC1(CONTROL_PURPOSE_NO_COMMAND, 0, 0);
            if ((GetTickCount() - startup_timer) > 200) {
                current_state = SYS_CONTROL_ACTIVE; // ここで初めて実制御開始
            }
            break;
            
        case SYS_CONTROL_ACTIVE:
            // 通常のリアルタイム制御ロジックを実行
            Execute_Dynamic_Control();
            break;
            
        case SYS_FAULT:
            Set_Safe_State();
            break;
    }
}

ここから先は

12,825字

¥ 980

この記事が気に入ったらチップで応援してみませんか?