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 的一部分。

CAN 控制器的方塊圖。在 TX 端 (上方),can.send() 會把訊框放入三個 TX 信箱之一,而一個仲裁方塊會挑選哪個信箱 接下來送上匯流排。在 RX 端(下方), 進入的訊框會通過接受過濾器; 被接受的訊框會落入 RX FIFO,而 can.recv() 會從 FIFO 最舊的一端讀取。

控制器位於軟體與匯流排之間的 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 若持續穩定攀升,通常指向布線、終端電阻或設定錯誤的位元速率。