3、数据链路层(CAN):CAN帧结构(标准帧/扩展帧)、仲裁机制、位填充、CRC校验、错误处理
各位同学,咱们今天聊聊CAN总线。说实话,在电池化成设备这个圈子里,CAN协议是绕不开的硬骨头。我做了十几年嵌入式,从最早的485总线一路摸爬滚打过来,最后发现,搞电池化成,CAN才是王道。为什么?因为化成现场环境太恶劣了,强电干扰、大电流冲击,一般的通信协议根本扛不住。
嗯,咱们直接切入正题。数据链路层,说白了就是负责把物理层传过来的0和1,组织成有意义的“帧”。CAN的帧结构设计得非常精巧,我当年第一次看规范时,就觉得这帮工程师是真有水平。
3.1 CAN帧结构:标准帧 vs 扩展帧
CAN总线有两种帧格式:标准帧(2.0A)和扩展帧(2.0B)。它们最大的区别,就是ID的长度不同。
| 特性 | 标准帧 (CAN 2.0A) | 扩展帧 (CAN 2.0B) |
|---|---|---|
| ID长度 | 11位 | 29位 |
| IDE位 | 显性(0) | 隐性(1) |
| 最大节点数 | 2048个 | 5.3亿个 |
| 数据场长度 | 0-8字节 | 0-8字节 |
你可能会问,为什么要有扩展帧?我举个例子。在电池化成设备里,一个机柜可能有上百个通道,每个通道都需要独立的ID来标识。11位ID只有2048个组合,对于大型化成系统来说,根本不够用。29位扩展帧就解决了这个问题。
关键点:标准帧和扩展帧可以共存于同一总线。仲裁时,标准帧的优先级永远高于扩展帧。这是由IDE位决定的——标准帧的IDE是显性(0),扩展帧的IDE是隐性(1)。
来看一个实际的数据帧结构。我习惯用这种格式来记忆:
标准帧:
SOF | 11位ID | RTR | IDE | r0 | DLC | 0-8字节数据 | CRC | ACK | EOF
扩展帧:
SOF | 11位ID | SRR | IDE | 18位扩展ID | RTR | r1 | r0 | DLC | 0-8字节数据 | CRC | ACK | EOF
这里有个坑,我当年踩过。RTR位(远程帧请求位)在数据帧里必须是显性(0),在远程帧里是隐性(1)。有一次我调试设备,发现某个节点老是收不到数据,查了半天,原来是RTR位被误设成了1,变成了远程帧请求。嗯,这种低级错误,犯过一次就再也不会忘了。
3.2 仲裁机制:谁先说话谁有理
CAN总线的仲裁机制,是我见过最优雅的冲突解决方式。它不像以太网那样采用CSMA/CD(碰撞检测后重发),而是采用CSMA/CA(碰撞避免)。说白了,就是“谁先说话谁有理”。
具体怎么实现的?靠的是“线与”逻辑。总线上的电平是“显性”覆盖“隐性”。多个节点同时发送时,谁的ID更小(二进制值更小),谁就能继续发送,其他节点自动退出发送,转为接收。
我的经验:在设计化成设备的CAN网络时,我建议把关键节点(比如主控制器)的ID设得小一些,比如0x001。这样在总线冲突时,主控制器总能优先发送。我曾经在一个项目中,把BMS(电池管理系统)的ID设成了0x7FF,结果每次和主控抢总线都抢不过,导致数据延迟。后来把ID改小,问题就解决了。
仲裁过程是这样的:
- 所有节点同时发送SOF(帧起始)位,同步总线
- 从ID的最高位开始,逐位比较
- 如果某个节点发送的是隐性位(1),但总线上是显性位(0),说明有更高优先级的节点在发送,该节点立即退出
- 胜出的节点继续发送剩余帧内容
你想想看,这个过程完全是无损的。没有数据被破坏,没有时间浪费在重发上。这就是为什么CAN总线在实时性要求高的场合(比如电池化成)这么受欢迎。
3.3 位填充:保证同步的“小把戏”
位填充,听起来挺玄乎,其实原理很简单。CAN协议规定:连续发送5个相同电平的位后,必须插入一个相反电平的位。
为什么要这么做?因为CAN接收器是靠电平跳变来同步时钟的。如果长时间没有跳变(比如连续发送15个0),接收器的时钟就会漂移,导致采样错误。
注意:位填充只发生在SOF到CRC段之间。ACK段、EOF段和帧间隔不进行位填充。我曾经见过一个新手,在计算CRC时把填充位也算进去了,结果校验永远通不过。
举个例子:
原始数据:11111 00000 11111
填充后: 111110 000001 111110
看到没?每5个相同位后面,自动插入一个相反位。接收端在解码时,会自动去掉这些填充位,恢复原始数据。
这里有个细节:如果原始数据正好是5个1后面跟一个0,那填充位会变成什么?答案是:5个1后插入一个0,然后原始数据的0继续发送。所以接收端看到的是6个0?不对,是5个1 + 1个填充0 + 1个原始0 = 5个1 + 2个0。接收端根据规则,去掉填充位后,恢复为5个1 + 1个0。嗯,逻辑要理清楚。
3.4 CRC校验:数据完整性的守护神
CAN的CRC校验用的是15位CRC(标准帧和扩展帧都是)。生成多项式是:
x^15 + x^14 + x^10 + x^8 + x^7 + x^4 + x^3 + 1
这个多项式能检测出:所有单比特错误、所有双比特错误、所有奇数个错误、所有突发长度≤15的突发错误。对于电池化成设备来说,这个检错能力完全够用。
CRC的计算范围包括:SOF、仲裁场、控制场、数据场。注意,填充位不参与CRC计算。我当年做CAN驱动时,就因为这个细节折腾了两天。
实战技巧:在化成设备的CAN通信中,我建议在应用层再加一层CRC校验。虽然CAN的CRC已经很强了,但考虑到化成现场的高干扰环境,双重校验更保险。我曾经在一个项目中,CAN控制器报告的CRC错误率高达0.1%,但应用层CRC校验发现,实际上只有0.01%的数据真正出错了。剩下的0.09%是CAN控制器误报。
3.5 错误处理:CAN的自愈能力
CAN的错误处理机制,是我见过最完善的。它定义了5种错误类型:
- 位错误:发送节点在发送位时,发现总线电平与自己发送的不一致
- 填充错误:接收节点检测到连续6个相同电平(违反位填充规则)
- CRC错误:接收节点计算的CRC与发送节点的不一致
- 格式错误:帧格式不符合规范(比如DLC超过8)
- ACK错误:发送节点没有收到ACK应答
每个CAN节点都有两个计数器:发送错误计数器(TEC)和接收错误计数器(REC)。根据这两个计数器的值,节点会处于三种状态之一:
| 状态 | 条件 | 行为 |
|---|---|---|
| 主动错误 | TEC < 128 且 REC < 128 | 正常通信,发送主动错误标志(6个显性位) |
| 被动错误 | TEC ≥ 128 或 REC ≥ 128 | 只能发送被动错误标志(6个隐性位),且发送前需等待8个隐性位 |
| 总线关闭 | TEC ≥ 256 | 完全脱离总线,不再参与任何通信 |
我曾经在调试化成设备时,遇到过一个诡异的问题:某个从站节点每隔几分钟就掉线一次。查了半天,发现是TEC计数器一直在增长,最终达到256导致总线关闭。原因是什么?是这个节点的CAN收发器接触不良,导致发送的位经常被干扰,接收节点返回的ACK也收不到。嗯,硬件问题往往比软件问题更难排查。
避坑指南:我曾经在项目中遇到过总线关闭后无法自动恢复的问题。后来发现,CAN协议规定总线关闭后,节点需要检测到128次11个连续隐性位(总线空闲)才能重新进入主动错误状态。这个时间大约是:128 × 11位时间。对于500kbps的波特率,大约是2.8ms。如果你的应用需要快速恢复,可以考虑在应用层主动复位CAN控制器。
最后说一句,CAN的错误处理机制是分布式的。每个节点独立判断、独立处理,不需要主节点干预。这种设计让CAN总线具有极强的鲁棒性。在电池化成设备这种高可靠性要求的场景下,CAN几乎是唯一的选择。
好了,数据链路层的内容就讲到这里。下一章咱们聊聊应用层协议,看看怎么在CAN帧的基础上,构建一套完整的化成设备通信协议栈。