3. CAN总线仲裁机制:线与逻辑、逐位仲裁、ID优先级、非破坏性仲裁

CAN总线的仲裁机制,是它最精妙的设计之一。说白了,就是多个节点同时抢着发消息时,谁说了算。

我刚开始接触CAN时,觉得这玩意儿不就是个“谁先发谁赢”吗?后来踩了坑才明白,它用的是“线与逻辑”和“逐位仲裁”,而且整个过程是非破坏性的——赢了的节点继续发,输了的自动退出发送,不丢数据。

3.1 线与逻辑:物理层的基础

先看物理层。CAN总线有两根线:CAN_H和CAN_L。它有两种状态:

  • 显性(Dominant):逻辑0,CAN_H ≈ 3.5V,CAN_L ≈ 1.5V,差分电压约2V
  • 隐性(Recessive):逻辑1,CAN_H ≈ 2.5V,CAN_L ≈ 2.5V,差分电压约0V

关键来了:显性位会覆盖隐性位。只要有一个节点输出显性,总线就是显性。这就是“线与逻辑”——多个输出端直接连在一起,谁拉低谁说了算。

核心规则:显性(0)> 隐性(1)。总线状态 = 所有节点输出状态的“线与”结果。

我在项目中遇到过一个问题:某个节点在隐性状态下把CAN_H拉到了3.3V,结果总线一直显性,其他节点全发不出去。排查了半天,发现是收发器的隐性电平偏了。嗯,物理层的问题,有时候比协议层更隐蔽。

3.2 逐位仲裁:一位一位地比

仲裁过程发生在帧起始(SOF)之后,仲裁场(Identifier + RTR)期间。每个节点在发送每一位时,都会同时监听总线电平。

流程是这样的:

  1. 所有等待发送的节点,在总线空闲时同时开始发送SOF位(显性)。
  2. 从仲裁场的第一位(ID的MSB)开始,每个节点逐位发送自己的ID位。
  3. 发送完一位后,节点立即读取总线电平。
  4. 如果自己发送的是隐性(1),但读到的是显性(0),说明有更高优先级的节点在发送——自己立刻退出仲裁,转为接收模式。
  5. 如果自己发送的是显性(0),或者发送隐性且读到隐性,则继续发送下一位。
  6. 重复直到仲裁场结束,最后剩下的那个节点赢得总线。

关键点:仲裁是“边发边听”的。节点在发送每一位的同时,也在监听总线。这种机制叫“回读(Read-back)”。

举个例子。假设三个节点同时发送:

节点 ID(二进制) 优先级
A 0b1010 0011 00 中
B 0b0110 0101 10 高
C 0b1100 1001 01 低

仲裁过程:

  • 第1位:A发1(隐性),B发0(显性),C发1(隐性)。总线显性。A和C读到显性,知道自己输了,退出。B继续。
  • 后续位:B独占总线,完成发送。

你想想看,整个过程只用了几个位时间就完成了仲裁,效率极高。

3.3 ID优先级:数字越小,优先级越高

CAN的ID优先级规则很简单:ID值越小,优先级越高。因为显性(0)优先于隐性(1),所以ID中高位为0的节点,在仲裁中占优势。

标准帧(11位ID)的优先级范围:0x000(最高)~ 0x7FF(最低)。

扩展帧(29位ID)的优先级范围:0x00000000(最高)~ 0x1FFFFFFF(最低)。

注意:扩展帧的ID虽然长,但仲裁时是从ID的MSB开始逐位比较的。所以扩展帧的优先级并不比标准帧高——实际上,标准帧的RTR位位置更靠前,在某些情况下标准帧反而有优势。

我个人习惯在项目初期就规划好ID分配策略:

  • 紧急消息(如刹车、安全气囊):ID 0x000~0x0FF
  • 实时控制消息(如发动机转速、车速):ID 0x100~0x2FF
  • 状态信息(如温度、故障码):ID 0x300~0x4FF
  • 诊断和配置消息:ID 0x500~0x7FF

我曾经在一个项目中,把刹车消息的ID设成了0x700,结果在总线负载高时,刹车消息被其他消息堵住了。嗯,从那以后我再也不敢把关键消息的ID设大了。

3.4 非破坏性仲裁:赢了继续,输了不丢

这是CAN仲裁最优雅的地方。传统的总线仲裁(比如以太网的CSMA/CD)是“破坏性”的——冲突发生后,所有节点都停止发送,然后随机等待重试。这会导致带宽浪费和延迟不确定。

CAN的非破坏性仲裁不同:

  • 赢了的节点:继续发送完整帧,不受任何影响。
  • 输了的节点:在仲裁失败的位立即停止发送,自动转为接收模式。它不会重发,而是等待下一次总线空闲时重新尝试。

整个过程:

  • 没有数据损坏
  • 没有带宽浪费
  • 延迟可预测(优先级高的消息总能及时发出)

非破坏性仲裁的三大好处:

  1. 实时性:高优先级消息的延迟有上界,适合硬实时系统。
  2. 效率:仲裁过程不浪费带宽,总线利用率高。
  3. 确定性:系统行为可预测,便于分析和测试。

我建议你在设计CAN网络时,一定要利用好这个特性。把时间关键的消息分配小ID,把非关键消息分配大ID。这样即使总线负载达到80%以上,关键消息也能准时到达。

3.5 仲裁过程图解

下面这张图展示了三个节点同时发送时的仲裁过程:

CAN总线逐位仲裁过程 SOF ID.10 ID.9 ID.8 ID.7 ID.6 ID.5 节点A ID=0x28C 0 1 1 0 0 1 0 节点B ID=0x196 0 0 1 1 0 0 1 节点C ID=0x325 0 1 1 0 0 1 0 总线 0 0 0 0 0 0 0 第1位(SOF后):所有节点发0,总线0。第2位:A发1、B发0、C发1 → 总线0 → A和C退出仲裁,B获胜 A退出 C退出

从图中可以看到:节点B的ID最小(0x196),在第二位就赢了仲裁。节点A和C在读到总线显性后,立即退出。整个过程干净利落。

3.6 避坑指南

我曾经踩过的坑:

  • ID分配不合理:把高频率消息的ID设得太大,导致低优先级消息饿死。后来我改用“优先级分组+动态ID调整”策略。
  • 忽略RTR位的影响:远程帧的RTR位是隐性(1),数据帧的RTR位是显性(0)。所以相同ID的数据帧优先级高于远程帧。如果你用远程帧请求数据,要小心这个优先级差异。
  • 扩展帧的误判:有些人以为扩展帧优先级更高,其实不是。仲裁只看位序列,不看帧类型。

我的建议:

  • 项目初期就制定ID分配规范,留出扩展空间。
  • 关键消息的ID尽量用0x000~0x0FF。
  • 用CAN分析仪抓一下总线,看看实际仲裁过程中有没有异常。
  • 如果总线负载超过60%,考虑增加波特率或拆分网络。

3.7 小结

CAN的仲裁机制,核心就四点:

  • 线与逻辑:显性覆盖隐性,物理层保证。
  • 逐位仲裁:边发边听,一位一位比。
  • ID优先级:数字越小,优先级越高。
  • 非破坏性仲裁:赢了继续,输了自动退,不丢数据。