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

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 标志按位或进第三个参数:

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