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_000、250_000、500_000 和 1_000_000 是常见的取值。set_filters() 配置控制器将放行哪些 ID;None 表示接受一切(在搭建链路时有用,一旦总线繁忙起来便用处不大)。
3.26.1. 控制器内部¶
在摄像头的 Python 代码和总线之间有三块硬件——TX 邮箱、验收过滤器以及 RX FIFO。每一块都对应下文所用 CAN API 的一部分。
软件与总线之间的控制器 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 稳步攀升,通常指向接线、终端电阻或配置错误的波特率问题。