4. CAN帧结构(上):数据帧格式

各位同学,今天我们来啃CAN协议里最核心的一块——数据帧。说白了,CAN总线上跑的最多的就是这玩意儿。你想想看,ECU之间交换传感器数据、发送控制指令,全得靠它。

我刚开始接触CAN的时候,看着那一堆ID、控制位、CRC,头都大了。后来自己动手抓了几次波形,才真正搞明白每一bit是干嘛用的。今天我就把这点经验分享给你们。

4.1 标准帧 vs 扩展帧:到底差在哪?

CAN 2.0规范定义了两种数据帧格式:标准帧和扩展帧。它们最大的区别,就是ID的长度不一样。

对比项 标准帧 (Base Frame) 扩展帧 (Extended Frame)
ID长度 11位 29位
IDE位 显性 (0) 隐性 (1)
可用的ID范围 0x000 ~ 0x7FF 0x00000000 ~ 0x1FFFFFFF
帧头开销 较小 较大(多出17位)
应用场景 节点少、实时性高的系统 节点多、需要大量ID的系统
我的经验:在乘用车上,动力总成(发动机、变速箱)通常用标准帧,因为节点少、要求响应快。而车身控制(门窗、灯光)节点多,用扩展帧更灵活。选型时别拍脑袋,先数数你的系统需要多少个不同ID。

4.2 ID仲裁域:谁说了算?

仲裁域是CAN帧里最精彩的部分。它决定了当两个节点同时发数据时,谁先发、谁退让。

标准帧的仲裁域包含:11位ID + RTR位。扩展帧则是:11位基础ID + SRR位 + IDE位 + 18位扩展ID + RTR位。

仲裁的规则很简单:ID值越小,优先级越高。为什么?因为CAN总线是“线与”逻辑,显性位(逻辑0)会覆盖隐性位(逻辑1)。

举个例子:

  • 节点A发ID = 0x100(二进制:001 0000 0000)
  • 节点B发ID = 0x200(二进制:010 0000 0000)

从最高位开始比。第1位都是0,没问题。第2位,A是0(显性),B是1(隐性)。总线被A拉成显性,B检测到总线电平跟自己发的不一样,立刻退出。A胜出,继续发后面的数据。

关键点:仲裁过程不会破坏任何数据。输的那一方,会在总线空闲后自动重发。这就是CAN的“非破坏性逐位仲裁”,牛吧?

4.3 控制域:告诉接收方怎么读

控制域紧跟在仲裁域后面。它告诉接收方:这个帧里有多少数据?是数据帧还是远程帧?

标准帧的控制域是6位:

  • IDE位:标准帧里是显性(0),表示后面没有扩展ID
  • 保留位r0:显性(0),留给未来用
  • DLC(4位):数据长度码,范围0~8

扩展帧的控制域也是6位,但结构不同:

  • 保留位r1、r0:都是显性(0)
  • DLC(4位):同上
注意:DLC虽然可以表示0~15,但CAN协议规定数据域最多8字节。所以DLC值超过8时,接收方应该按8字节处理。我曾经见过一个项目,有人把DLC设成12,结果接收方直接崩溃了。嗯,这种坑踩过一次就记住了。

4.4 数据域:真正干活的地方

数据域就是帧里装“货”的地方。最多8个字节,最少0个字节。

为什么是8字节?这是CAN协议设计时的权衡。8字节对于大多数控制信号(转速、温度、位置)来说足够了。如果数据量更大,可以拆成多帧发送,或者用CAN FD(后面章节会讲)。

数据域的排列顺序是MSB优先(最高有效字节在前)。比如你要发送一个32位的转速值0x12345678:

字节0: 0x12
字节1: 0x34
字节2: 0x56
字节3: 0x78
字节4~7: 未使用(通常填0)
我的习惯:定义CAN矩阵(DBC文件)时,我会把同一个信号尽量放在连续的字节里,并且对齐到字节边界。这样解析代码写起来简单,也不容易出错。别小看这个习惯,在项目后期能省不少调试时间。

4.5 CRC校验域:数据完整性最后一道防线

CAN总线上环境很恶劣,电磁干扰、电压波动都可能让数据出错。CRC就是用来检测这些错误的。

CAN数据帧使用15位CRC,生成多项式是:

x^15 + x^14 + x^10 + x^8 + x^7 + x^4 + x^3 + 1

计算范围包括:帧起始 + 仲裁域 + 控制域 + 数据域。发送方算好CRC值,放在CRC域里。接收方用同样的多项式重新算一遍,如果结果不一致,就说明数据被破坏了。

CRC域后面还有1位CRC分隔符(隐性位),用来把CRC和ACK域隔开。

你知道吗?CAN的15位CRC可以检测出:所有单bit错误、所有双bit错误、所有奇数个bit错误、所有长度≤15的突发错误。对于长度超过15的突发错误,漏检率只有1/32768。这个防护能力,在工业现场总线里算是非常强的了。

4.6 一张图看懂数据帧全貌

说了这么多,不如直接看一张完整的结构图。下面这张SVG图,把标准数据帧从头到尾的每个域都画出来了。

CAN标准数据帧结构图 CAN标准数据帧结构(11位ID) SOF 11位 ID 仲裁域 RTR IDE r0 DLC 控制域 数据域 0~8 字节 CRC 15位 DEL ACK DEL EOF 总长度:1 + 11 + 1 + 1 + 1 + 4 + 0~64 + 15 + 1 + 2 + 7 = 44~108 位(不含填充位) 1位 11位 1位 1位 1位 4位 0~64位 15位 1位 2位 7位 SOF: 帧起始 | RTR: 远程帧标志 | IDE: 扩展标志 | r0: 保留位 | DLC: 数据长度码 CRC: 循环冗余校验 | DEL: 分隔符 | ACK: 应答 | EOF: 帧结束

4.7 避坑指南:我踩过的几个坑

讲完理论,来点实战经验。这些坑我当年都踩过,你们注意绕开。

坑一:ID分配太随意
我曾经在一个项目里,把高优先级信号(比如刹车)的ID设成了0x700,低优先级信号(比如车窗)设成了0x001。结果刹车信号经常被延迟。后来才反应过来,ID越小优先级越高。正确的做法是:关键信号用小编号ID,非关键信号用大编号ID。
坑二:DLC和实际数据长度不匹配
有一次调试,发送方DLC设成8,但实际只填了4个字节的数据。接收方把后面4个随机值当成有效数据,导致系统误动作。从那以后,我要求团队必须保证DLC和实际数据长度严格一致。
坑三:忽略CRC校验结果
有些工程师觉得CRC出错概率低,就不处理。我在一个项目里遇到过,总线上一根线接触不良,CRC错误帧暴增。幸好我们做了错误统计和报警,及时发现了问题。记住:CRC出错不是“不可能”,而是“什么时候发生”的问题。

4.8 小结

这一章我们拆解了CAN数据帧的每个域。从SOF到EOF,每一bit都有它的使命。标准帧和扩展帧的选择,取决于你的系统规模。仲裁域决定了谁先说话,控制域告诉你怎么读,数据域装的是真正的信息,CRC域守护着数据的完整性。