2. 车载网络演进

大家好,我是你们这趟车载网络之旅的向导。今天咱们聊聊CAN总线。说实话,在我十几年的车载网络生涯里,CAN总线就像一位老朋友,虽然年纪大了点,但江湖地位稳得很。很多新入行的朋友觉得CAN总线“老”,想跳过它直接学以太网。我的建议是——千万别。你想想看,一辆车上几十个ECU,大部分还在用CAN通信,不懂CAN,你连故障都定位不了。

2.1 CAN协议的起源:为什么会有它?

上世纪80年代,汽车里的电子系统开始变多。空调、ABS、发动机控制……每个系统都自己拉一堆线。结果呢?线束比发动机还重,故障率也高。德国Bosch公司看不下去了,1983年开始研发一种新的串行通信协议,1986年正式发布——这就是CAN(Controller Area Network)。

它的设计目标很明确:

  • 高可靠性:能在电磁干扰严重的车里稳定工作
  • 实时性:关键消息(比如刹车信号)不能有延迟
  • 低成本:两根线就能搞定所有节点通信

我记得第一次接触CAN总线是在一个国产商用车项目上。当时客户抱怨“刹车灯偶尔不亮”,查了三天没找到原因。后来我用示波器抓CAN总线波形,发现是某个节点发送了错误帧,把总线给“堵”住了。嗯,从那以后,我养成了一个习惯——任何CAN项目,第一件事就是看总线负载率和错误帧计数。

2.2 CAN物理层特性:两根线,不简单

CAN总线物理层说白了就是一对差分线——CAN_H和CAN_L。为什么用差分?抗干扰啊!你想想看,发动机点火时电磁干扰多强,单端信号早就被淹没了。差分信号呢?干扰同时作用在两根线上,接收器只看差值,干扰就被抵消了。

关键参数我列个表,大家记一下:

参数 高速CAN(ISO 11898-2) 低速CAN(ISO 11898-3)
最大速率 1 Mbps 125 kbps
总线长度(@最大速率) 40 m 500 m
显性电平(逻辑0) CAN_H=3.5V, CAN_L=1.5V CAN_H=3.6V, CAN_L=1.4V
隐性电平(逻辑1) CAN_H=2.5V, CAN_L=2.5V CAN_H=1.75V, CAN_L=3.25V
终端电阻 120Ω(两端各一个) 不需要
⚠️ 我曾经踩过的坑: 终端电阻一定要加在总线物理两端,而不是ECU内部。有一次我图省事,把两个120Ω电阻都放在了一个节点上,结果总线信号反射严重,通信时好时坏。查了两天才发现是终端电阻位置不对。

2.3 CAN报文帧结构:五种帧,各有各的活

CAN总线上一共五种帧:数据帧、远程帧、错误帧、过载帧、帧间隔。咱们重点说前四种,帧间隔比较简单,就是帧与帧之间的休息时间。

2.3.1 数据帧——最常用的“快递员”

数据帧负责把数据从一个节点送到另一个节点。结构分为标准帧(11位ID)和扩展帧(29位ID)。我个人习惯在CANoe里配置时,能用标准帧就不用扩展帧——少传18位,总线负载率能降不少。

数据帧结构如下:

  • 帧起始(SOF):1位显性,告诉总线“我要发数据了”
  • 仲裁场:11位ID(标准帧)或29位ID(扩展帧)+ RTR位
  • 控制场:IDE位、保留位、DLC(数据长度,0-8字节)
  • 数据场:0-8字节数据,这才是真正的“货”
  • CRC场:15位CRC + 1位CRC界定符,用来检错
  • ACK场:接收节点在这里回应“我收到了”
  • 帧结束(EOF):7位隐性,表示帧结束
// 一个标准CAN数据帧的位序列示例(ID=0x123,数据=0xAA 0xBB)
// SOF + ID(11bit) + RTR + IDE + r0 + DLC(4bit) + Data(16bit) + CRC(15bit) + CRC Del + ACK + ACK Del + EOF(7bit)
// 0     + 0x123      + 0    + 0   + 0  + 0x02      + 0xAABB   + CRC       + 1        + 0   + 1       + 1111111

2.3.2 远程帧——只问不拿

远程帧长得跟数据帧几乎一样,区别在于RTR位是隐性(1)。它的作用是:一个节点请求另一个节点发送数据。比如发动机ECU发一个远程帧,请求“水温传感器,把你的数据发给我”。

这里有个细节:远程帧没有数据场,但DLC字段必须填上期望接收的数据长度。我见过不少新手在这里栽跟头——DLC填0,结果对方节点不知道要发多少字节。

2.3.3 错误帧——总线的“警报器”

错误帧由6个连续的显性位(错误标志)和8个隐性位(错误界定符)组成。任何节点检测到错误,就会主动发送错误帧,通知所有节点“刚才那帧有问题,别用了”。

错误类型分五种:

  • 位错误:发送节点发现总线上的电平跟自己发的不一样
  • 填充错误:连续5个相同位后没有插入反相位
  • CRC错误:接收节点计算的CRC跟发送的不一致
  • 形式错误:帧格式不对(比如EOF不是7个隐性位)
  • ACK错误:没有节点在ACK槽发送显性位确认
💡 实战技巧: 在调试CAN总线时,如果发现错误帧频繁出现,先别急着怀疑硬件。我曾经遇到一个案例,错误帧全是“填充错误”,最后发现是某个节点的晶振频率偏差太大,导致位时序错位。换个晶振就好了。

2.3.4 过载帧——总线的“请稍等”

过载帧跟错误帧结构类似,但用途不同。当接收节点来不及处理数据时,它会发送过载帧,告诉发送节点“慢一点,我消化不了”。过载帧由6个显性位(过载标志)和8个隐性位(过载界定符)组成。

说实话,我在实际项目中很少见到过载帧。因为现在的MCU处理速度都很快,除非总线负载率超过80%或者节点中断响应太慢,否则不会触发过载。如果你在项目中频繁看到过载帧,先检查一下软件的中断优先级配置。

2.4 CAN总线仲裁机制:谁先说话,谁说了算

这是CAN总线最巧妙的设计之一。多个节点同时发送时,怎么决定谁先发?答案是:ID越小,优先级越高。

仲裁过程是这样的:

  1. 所有想发送的节点同时发送SOF(显性位)
  2. 然后逐位发送ID。如果某个节点发送隐性位(1),但总线上是显性位(0),说明有更高优先级的节点在发,这个节点立即停止发送,转为接收
  3. 直到只剩下一个节点,它独占总线,完成数据发送

说白了,这就是一个“非破坏性逐位仲裁”机制。高优先级的节点不会被打断,低优先级的节点自动退让。

我画了一张图,帮大家理解这个过程:

CAN总线仲裁机制示意图 节点A (ID=0x100) 节点B (ID=0x200) 节点C (ID=0x300) CAN总线(CAN_H / CAN_L) 仲裁过程(逐位比较ID) 位10=0 位10=0 位10=0 → 全部显性,继续 位9=0 位9=0 位9=0 → 全部显性,继续 位8=0 位8=1 位8=1 → 节点A显性,B和C退让! 🏆 节点A赢得仲裁,发送数据 ⏳ 节点B、C等待下次机会

你看,节点A的ID是0x100(二进制0001 0000 0000),节点B是0x200(0010 0000 0000),节点C是0x300(0011 0000 0000)。在ID的第8位(从高位算起),节点A发送0(显性),节点B和C发送1(隐性)。总线被节点A拉成显性,B和C检测到“自己发的跟总线不一样”,立刻退出。这就是仲裁的整个过程。

🔑 核心要点:
  • ID越小,优先级越高。0x000是最高优先级,0x7FF是最低(标准帧)
  • 仲裁过程不破坏任何数据,高优先级节点“无痛”获胜
  • 低优先级节点自动转为接收,不会丢失数据
  • 远程帧的RTR位是隐性,所以数据帧(RTR=0)优先级高于远程帧(RTR=1)

最后说一个我自己的经验:设计CAN网络时,一定要把关键信号(刹车、油门、安全气囊)分配最小的ID。我曾经在一个项目中,把车窗控制信号分配了0x100,刹车信号分配了0x700。结果呢?刹车信号经常被车窗信号“堵”在后面。虽然CAN仲裁机制保证了高优先级先发,但ID分配不合理,关键信号的实时性就保证不了。后来我重新规划了ID分配表,把安全相关信号全部放在0x000-0x1FF区间,舒适性信号放在0x200-0x3FF,诊断信号放在0x700以上。从此再没出过问题。