3. CAN总线仲裁机制:CSMA/CA与位仲裁原理、优先级判定

说到CAN总线,有个问题我经常被问到:多个节点同时发数据,总线会不会打架?

答案是:会争用,但不会乱。CAN总线靠一套精巧的仲裁机制来解决冲突。说白了,就是谁优先级高谁先说话。

3.1 CSMA/CA:先听再说,边说边听

CAN总线用的是CSMA/CA协议。全称是Carrier Sense Multiple Access with Collision Avoidance,载波监听多路访问/冲突避免。

我习惯把它拆成三步理解:

  • 先听:节点要发数据前,先监听总线是否空闲。
  • 再说:总线空闲了,才开始发送。
  • 边听边说:发送的同时,还在监听总线电平。一旦发现电平跟自己发的不一致,就知道有冲突了。

你想想看,这跟以太网的CSMA/CD(冲突检测)不一样。以太网是撞了再重发,CAN是发之前就避免冲突。为什么?因为CAN是实时总线,不能容忍随机退避带来的延迟。

我的经验: 在车载网络中,如果某个节点总是发不出数据,先别急着查硬件。用示波器抓一下CAN_H和CAN_L的差分信号,看看是不是总线一直被高优先级节点占着。我遇到过好几次这种“软故障”。

3.2 位仲裁:隐性 vs 显性

仲裁的核心,在于CAN总线的两种电平状态:

  • 显性(Dominant):逻辑0,差分电压约2V。能覆盖隐性电平。
  • 隐性(Recessive):逻辑1,差分电压约0V。被显性覆盖。

说白了,显性位就是“老大”,隐性位是“小弟”。多个节点同时发,只要有一个节点发显性位,总线就是显性。

仲裁过程是这样的:

  1. 所有节点同时从仲裁段(Identifier)的第1位开始发送。
  2. 每个节点发送一位,同时读取总线电平。
  3. 如果自己发的是隐性位(1),但读到的是显性位(0),说明有更高优先级的节点在发,自己立即退出。
  4. 退出节点转为接收模式,不再干扰总线。
  5. 最后剩下的那个节点,就是赢家。

整个过程发生在一位的时间内,硬件自动完成,不需要软件干预。

关键点: 仲裁不破坏数据,不浪费带宽。输掉的节点自动重发,赢的节点继续发完。这就是CAN总线实时性的底气。

3.3 优先级判定:ID越小,优先级越高

优先级由标识符(Identifier, ID)决定。ID是一个11位(标准帧)或29位(扩展帧)的二进制数。

规则很简单:ID值越小,优先级越高。

为什么?因为ID的高位先发。ID小的节点,高位显性位多,更容易在仲裁中胜出。

举个例子:

节点 ID(二进制) ID(十进制) 优先级
发动机ECU 000 0000 0001 1 最高
ABS控制器 000 0000 1000 8 高
车窗控制器 000 0011 0010 50 中
空调控制器 000 0111 1011 123 低

发动机ECU的ID是1,空调是123。两者同时发,发动机ECU在第1位就赢了。因为它的高位是0(显性),空调的高位是0(显性),打平。第2位,发动机ECU是0,空调是0,继续打平。直到第8位,发动机ECU是0,空调是1,发动机ECU胜出。

避坑指南: 我曾经在一个项目中,把两个节点的ID设成了相同的值。结果总线一直报错,数据发不出去。后来才发现,相同ID的节点在仲裁段无法区分,会导致位错误。记住:同一总线上,每个节点的ID必须唯一。

3.4 扩展帧仲裁:29位ID的玩法

标准帧11位ID,最多2032个有效ID(0~2031)。扩展帧29位ID,数量多得多。

扩展帧的仲裁过程更复杂一些,因为ID分两部分:

  • Base ID(11位):先发,决定基本优先级。
  • Identifier Extension(18位):后发,进一步细分。

仲裁时,先比Base ID,再比Extension。Base ID小的节点,在第一步就赢了,根本不用比Extension。

我个人的建议是:能用标准帧就别用扩展帧。扩展帧的仲裁段更长,总线利用率低。除非你的节点数量超过2032个,或者需要兼容某些协议(比如CANopen、J1939)。

3.5 仲裁失败后的处理:自动重发

输掉仲裁的节点怎么办?

硬件会自动处理:

  1. 节点检测到自己输掉仲裁,立即停止发送。
  2. 转为接收模式,等待总线空闲。
  3. 总线空闲后,自动重发之前没发完的数据帧。
  4. 重发时,仲裁段不变,优先级不变。

这个过程完全由CAN控制器硬件完成,CPU不需要参与。所以即使总线负载很高,高优先级节点也能保证实时性。

注意: 如果某个节点频繁输掉仲裁,它的发送延迟会累积。在实时性要求高的系统中(比如发动机控制),要确保关键节点的ID足够小。我一般把ID 1~10留给动力系统,11~50留给底盘系统,51以上留给车身系统。

3.6 仲裁机制总结

为了让你更直观地理解,我画了一张流程图:

CAN总线仲裁流程图 节点准备发送 监听总线是否空闲 总线空闲? 等待空闲 发送仲裁段(ID位) 发送位 == 总线电平? 继续发送剩余位 退出仲裁 转为接收模式 仲裁失败后 硬件自动重发 CPU无需干预

这张图把仲裁流程串起来了。你重点关注那个判断框:发送位是否等于总线电平。如果不相等,说明有更高优先级的节点在发,自己乖乖退出。

3.7 实际项目中的ID分配策略

ID分配不是随便写的。我总结了几条经验:

  • 实时性要求高的节点,ID越小越好:比如发动机、刹车、转向,ID建议在1~20之间。
  • 周期性报文和数据流报文分开:周期性报文(比如转速、车速)用固定ID,事件触发报文(比如故障码)用动态ID。
  • 预留扩展空间:别把ID用满,留20%~30%给未来功能升级。
  • 避免ID冲突:同一个网络内,ID必须唯一。我见过一个项目,两个节点用了同一个ID,结果总线一直报错,查了三天才发现。

一句话总结: CAN总线的仲裁机制,靠的是“隐性位被显性位覆盖”的物理特性,加上硬件自动完成的位比较和退出逻辑。ID越小,优先级越高。这个机制保证了总线冲突时,高优先级节点零延迟,低优先级节点自动重发。