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 程式碼與匯流排之間,有三個硬體部件——TX 信箱(TX mailboxes)、一個接受過濾器(acceptance filter)以及 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() 交付、但尚未送上匯流排的外送訊框。當匯流排閒置時,控制器會挑選 ID 數值最低(優先權最高)的信箱,並代其在匯流排上進行仲裁。相機並不挑選信箱;控制器會指派一個,並從 send() 回傳其索引。

  • 接受過濾器。 可設定的硬體,會把每個進入訊框的 ID 與一份樣式清單比對,並捨棄任何不相符者。通過的訊框會繼續進入 RX FIFO;被拒絕的訊框則由控制器丟棄,永遠不會到達 Python。set_filters() 用來設定這些樣式。

  • RX FIFO。 一個先進先出(first-in, first-out)的佇列——最先進入的訊框也是最先出去的,就像售票櫃檯前排隊一樣。控制器會在收到訊框時把它們附加到佇列的尾端,而 recv() 會以相同的順序從前端把它們取出。這個佇列之所以重要,是因為當 Python 正忙於其他事情時,匯流排會在背景中接住訊框;相機隨後再一次一個地把它們抽出而不會遺失任何一個,只要 FIFO 尚未溢位即可。

3.26.2. 送出一個訊框

send() 會把一個訊框排入佇列以待傳輸:

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

第一個引數是 ID(標準訊框為一個 11 位元整數);第二個是酬載(CAN Classic 為 0 至 8 位元組)。此呼叫會回傳一個小整數索引,標識該訊框所進入的硬體信箱。控制器會與匯流排上任何其他傳輸端進行仲裁,並視需要自動重新傳輸,無需軟體進一步協助。

對於擴充(29 位元)ID,請把 CAN.FLAG_EXT_ID 旗標以 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 匯流排會遭遇傳輸錯誤——對地短路、節點遺失、電氣雜訊損毀位元。控制器會維護兩個追蹤此行為的計數器:傳送錯誤計數器(Transmit Error Counter,TEC)與接收錯誤計數器(Receive Error Counter,REC),各自在控制器偵測到錯誤時遞增,並在成功傳輸後遞減。這些計數器的值會讓控制器進入以下四種狀態之一:

  • Error Active(TEC 與 REC 皆低於 96)。正常運作。當節點偵測到匯流排錯誤時,它會傳送一個顯性的主動錯誤訊框(active error frame),迫使每個其他節點捨棄進行中的訊框,好讓傳送端能重試。

  • Error Warning(任一計數器達到 96)。在匯流排上仍完全主動——警告狀態是一個軟體訊號,表示錯誤正在累積,並非行為上的改變。

  • Error Passive(任一計數器達到 128)。節點仍在匯流排上,但停止送出顯性錯誤訊框;錯誤現在改以被動(隱性)錯誤訊框來告知,如此一來有故障的節點便無法持續為其他所有人擾亂匯流排。

  • Bus Off(TEC 達到 256)。控制器已判定此節點太不可靠而不宜參與。它會自匯流排斷開、停止傳輸與確認,並保持脫離,直到軟體明確地重新啟動它為止。

前三個轉換完全是自動的——當計數器在成功的訊框之後遞減時,控制器會在無需介入的情況下自行往 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 若持續穩定攀升,通常指向布線、終端電阻或設定錯誤的位元速率。