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 邮箱验收过滤器以及 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 标志按位或进第三个参数:

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 总线会遇到传输错误——对地短路、节点缺失、电气噪声破坏位。控制器维护两个计数器来跟踪这种情况:发送错误计数器(TEC)和接收错误计数器(REC),各自在控制器检测到错误时递增,在一次成功传输后递减。计数器的取值将控制器置于以下四种状态之一:

  • 错误激活(TEC 和 REC 都低于 96)。正常运行。当节点检测到总线错误时,它会发送一个显性的激活错误帧,迫使其他每个节点丢弃正在进行中的帧,以便发送方可以重试。

  • 错误警告(任一计数器达到 96)。在总线上仍然完全处于激活状态——警告状态是一个软件信号,表示错误正在累积,而不是行为上的改变。

  • 错误被动(任一计数器达到 128)。节点仍在总线上,但停止发送显性错误帧;现在错误改用被动(隐性)错误帧来发出信号,这样故障节点便无法持续地为所有其他节点扰乱总线。

  • 总线关闭(TEC 达到 256)。控制器已判定该节点过于不可靠而不能参与通信。它会从总线断开,停止发送和应答,并保持脱离状态,直到软件显式地重启它。

前三种转换完全是自动的——随着计数器在成功的帧之后递减,控制器会在无需干预的情况下自行回退到错误激活状态。

总线关闭是唯一需要软件介入的状态。restart() 复位控制器并使其回到错误激活状态。一种典型的做法是在主循环中检查状态,并在短暂延迟后重启,以便给总线留出稳定下来的时间:

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 稳步攀升,通常指向接线、终端电阻或配置错误的波特率问题。