3.26. コードでのCANバス

machine.CAN はハードウェアCANコントローラをラップします。バスIDとビットレートを指定して起動します。

from machine import CAN

can = CAN(1, 500_000)
can.set_filters(None)        # accept all incoming IDs

ビットレートはバス上の他のすべてのノードと正確に一致している必要があります。一般的な値は 125_000250_000500_0001_000_000 です。set_filters() は、コントローラがどのIDを通過させるかを設定します。None はすべてを受け入れることを意味します(リンクの立ち上げ時には便利ですが、バスが混雑するとあまり役に立ちません)。

3.26.1. コントローラの内部

カメラのPythonコードとバスの間には、3つのハードウェアが存在します。TXメールボックス受信フィルタ、そしてRX FIFOです。これらはそれぞれ、以下で使用する CAN APIの一部に対応します。

Block diagram of the CAN controller. On the TX side (top), can.send() drops frames into one of three TX mailboxes, and an arbitrate block picks which mailbox goes to the bus next. On the RX side (bottom), incoming frames pass through an acceptance filter; accepted frames land in the RX FIFO and can.recv() reads the FIFO from its oldest end.

ソフトウェアとバスの間にあるコントローラのTXメールボックス、受信フィルタ、RX FIFO。

  • TXメールボックス。 カメラが send() を介して渡したものの、まだバスに到達していない送信フレームを保持する、少数のハードウェアスロット(通常は3つ)です。バスがアイドル状態のとき、コントローラは最も番号の小さいID(最も高い優先度)を持つメールボックスを選び、それに代わってバスのアービトレーションを行います。カメラがメールボックスを選ぶのではなく、コントローラが1つを割り当て、そのインデックスを send() から返します。

  • 受信フィルタ。 入ってくる各フレームのIDをパターンのリストと照合し、一致しないものを破棄する、構成可能なハードウェアです。通過したフレームはRX FIFOへ進み、拒否されたフレームはPythonに届くことなくコントローラによって捨てられます。set_filters() がこれらのパターンを設定します。

  • RX FIFO。 先入れ先出し(first-in, first-out)のキューです。チケットカウンターの行列のように、最初に入ったフレームが最初に出ます。コントローラは受信したフレームを到着順にキューの末尾に追加し、recv() がそれらを同じ順序で先頭から取り出します。このキューが重要なのは、Pythonが他の処理で忙しい間もバスがバックグラウンドでフレームを捕捉するからです。カメラはその後、FIFOがオーバーフローしていない限り、1つずつ失うことなくそれらを取り出していきます。

3.26.2. フレームの送信

send() はフレームを送信用にキューに入れます。

can.send(0x123, b"\x01\x02\x03\x04")

第1引数はID(標準フレームでは11ビットの整数)、第2引数はペイロード(CAN Classicでは0~8バイト)です。この呼び出しは、フレームが入ったハードウェアメールボックスを識別する小さな整数のインデックスを返します。コントローラはバス上の他の送信側とアービトレーションを行い、ソフトウェアからのさらなる介入なしに必要に応じて再送します。

拡張(29ビット)IDの場合は、CAN.FLAG_EXT_ID フラグを第3引数にORで指定します。

can.send(0x18FF1234, b"hello", CAN.FLAG_EXT_ID)

3.26.3. フレームの受信

コントローラは、フィルタが何もインストールされていない状態で起動し、入ってくるすべてのフレームを破棄します。recv() が何かを返すようにするには、まず set_filters() を一度呼び出します。最も単純な形は None で、これはすべてのIDを受け入れます。

can.set_filters(None)   # accept every frame

その後 recv() は受信FIFO内の次のフレームを返します。待機中のものが何もなければ None を返します。

msg = can.recv()
if msg is not None:
    can_id, data, flags, errs = msg
    print("got", hex(can_id), bytes(data))

バスはバックグラウンドでRX FIFOを満たすため、メインループはそれを反復処理する速度で取り出すだけで済みます。FIFOが取り出しの間の最も長い間隔よりも深い限り、フレームは失われません。

3.26.4. フィルタ

実際のバスは通常、カメラが関心を持たないフレームで混雑しています。ハードウェアフィルタを使えば、不要なIDがFIFOに到達する前にコントローラがそれらを破棄できます。set_filters()(id, mask, flags) のタプルのリストを受け取ります。フレームのIDを mask でマスクした結果が、設定された id と一致する場合、そのフレームはフィルタを通過します。

# Accept only IDs 0x100 - 0x10F (mask off the bottom 4 bits)
can.set_filters(((0x100, 0x7F0, 0),))

# Accept IDs 0x300 and 0x700 exactly
can.set_filters(((0x300, 0x7FF, 0),
                 (0x700, 0x7FF, 0)))

一致しないフレームはコントローラによって破棄され、recv() には決して現れません。これによりバッファ領域とCPU時間の両方が節約されます。

3.26.5. エラー状態と復旧

実際のCANバスは送信エラーを拾います。グランドへのショート、欠落したノード、ビットを破損させる電気的ノイズなどです。コントローラはこの挙動を追跡する2つのカウンタを保持します。送信エラーカウンタ(TEC)と受信エラーカウンタ(REC)で、コントローラがエラーを検出するたびに増加し、転送が成功するたびに減少します。カウンタの値により、コントローラは4つの状態のいずれかになります。

  • Error Active(TECとRECが両方とも96未満)。通常動作です。ノードがバスエラーを検出すると、ドミナントのアクティブエラーフレームを送信し、これによって他のすべてのノードに進行中のフレームを破棄させ、送信側が再試行できるようにします。

  • Error Warning(いずれかのカウンタが96に達する)。バス上では依然として完全にアクティブです。Warning状態は、エラーが蓄積していることを示すソフトウェア向けの信号であって、挙動の変化ではありません。

  • Error Passive(いずれかのカウンタが128に達する)。ノードは依然としてバス上にありますが、ドミナントエラーフレームの送信を停止します。エラーはパッシブ(リセッシブ)エラーフレームで通知されるようになり、故障したノードが他のすべてのノードのためにバスを乱し続けることがないようにします。

  • Bus Off(TECが256に達する)。コントローラは、このノードが参加するには信頼性が低すぎると判断しました。バスから切断し、送信とアクノリッジを停止し、ソフトウェアが明示的に再起動するまでバスの外にとどまります。

最初の3つの遷移は完全に自動です。フレームが成功するにつれてカウンタが減少していくと、コントローラは介入なしにError Activeへと自身を戻していきます。

Bus Offは、ソフトウェアによる対処を必要とする唯一の状態です。restart() はコントローラをリセットし、Error Activeへ戻します。典型的なパターンは、メインループから状態を確認し、バスが落ち着く時間を与えるために短い遅延の後で再起動するというものです。

import time
from machine import CAN

can = CAN(1, 500_000)
can.set_filters(None)

while True:
    if can.state() == CAN.STATE_BUS_OFF:
        time.sleep_ms(100)
        can.restart()
    # ... rest of the loop

現在のカウンタの値は、診断のために get_counters() から取得できます。他の点では静かなバスでTECが着実に上昇する場合、通常は配線、終端、または誤って設定されたビットレートが原因です。