2. CAN数据链路层:帧结构与核心机制

大家好,欢迎来到CAN协议栈开发的第二讲。今天我们要聊的,是CAN通信最核心、最基础的部分——数据链路层。

说实话,我在刚接触CAN的时候,觉得帧结构就是一堆二进制位,背下来就行了。后来在项目中踩过几次坑,才真正理解:帧结构的设计,本身就是一种精妙的通信哲学。你想想看,一个没有主节点的总线,多个节点怎么同时发数据而不乱套?答案就藏在这些帧里。

2.1 CAN帧的五大类型

CAN总线上一共定义了五种帧。我习惯把它们分成两类:正常通信用的(数据帧、远程帧),和异常处理用的(错误帧、过载帧、帧间隔)。

帧类型 用途 谁发起的
数据帧 发送节点把数据放到总线上 发送节点
远程帧 请求其他节点发送数据 接收节点
错误帧 检测到总线错误时通知大家 任何节点
过载帧 接收节点忙不过来,要求延迟 接收节点
帧间隔 帧与帧之间的隔离带 总线空闲

嗯,这里要注意:远程帧和过载帧在实际项目中用得不多。我个人建议,新手先把精力放在数据帧和错误帧上。远程帧我见过很多团队直接禁用,因为容易引发总线风暴。

2.2 数据帧的完整结构

数据帧是CAN通信的主角。它分为标准帧(11位ID)和扩展帧(29位ID)。我们以标准帧为例,看看它长什么样:

SOF | 11位ID | RTR | IDE | r0 | DLC | 0~8字节数据 | CRC | ACK | EOF

我来拆开讲:

  • SOF(Start of Frame):1位显性位。说白了就是「我要开始说话了」的信号。
  • ID(标识符):11位。这个ID决定了帧的优先级——数值越小,优先级越高。我在项目中遇到过一个问题:两个节点同时发数据,ID小的总能抢到总线。后来我们设计协议时,把关键信号都分配了小ID。
  • RTR(Remote Transmission Request):1位。数据帧里是显性(0),远程帧里是隐性(1)。
  • IDE(Identifier Extension):1位。标准帧里是显性(0),扩展帧里是隐性(1)。
  • r0:保留位,必须为显性。
  • DLC(Data Length Code):4位。表示数据字节数,范围0~8。注意:DLC可以填0~8,但超过8的数值会被当作8处理。我曾经见过一个新手把DLC写成9,结果接收方只收到8个字节,丢了一个数据。
  • 数据段:0~8字节。这就是你要传的实际内容。
  • CRC(Cyclic Redundancy Check):15位CRC校验码 + 1位CRC分隔符。用于检测传输错误。
  • ACK(Acknowledge):2位。发送节点发隐性位,接收节点如果正确收到,就在ACK Slot位拉成显性。这就是CAN的「全员确认」机制。
  • EOF(End of Frame):7位隐性位。表示帧结束。

重点记忆:数据帧的SOF到CRC段,使用了位填充(bit stuffing)技术。EOF和ACK段不填充。这一点在实现硬件或软件CAN控制器时特别容易搞错。

2.3 远程帧:请求数据的正确姿势

远程帧的结构和数据帧几乎一样,唯一的区别是RTR位为隐性(1),而且没有数据段。它的作用是:节点A发一个远程帧,告诉节点B「把你的数据发给我」。

举个例子:

// 节点A发送远程帧,请求ID=0x123的数据
// 节点B收到后,发送ID=0x123的数据帧

听起来很方便对吧?但我建议你谨慎使用。为什么?因为远程帧的优先级和数据帧一样,如果多个节点同时请求同一个ID,总线会乱。我曾经在一个项目中,三个传感器都用远程帧请求数据,结果总线利用率飙升到80%,差点崩溃。后来我们改成了定时发送,问题就解决了。

避坑指南:我曾经在调试一个ECU时,发现它不停地发远程帧,导致其他节点无法发送数据。查了半天,原来是接收缓冲区满了,节点用远程帧来「催」数据。这种设计在低负载时没问题,高负载下就是灾难。建议:能用数据帧主动发送,就别用远程帧

2.4 错误帧与过载帧:总线的「急救机制」

错误帧是CAN总线自我修复的关键。当任何节点检测到错误时,就会发送错误帧。它的结构很简单:

6个显性位(错误标志) | 8个隐性位(错误分隔符)

错误帧有两种:

  • 主动错误帧:6个显性位。由「主动错误状态」的节点发出。
  • 被动错误帧:6个隐性位。由「被动错误状态」的节点发出。注意,被动错误帧不会破坏其他节点的正常通信。

过载帧呢?它用于接收节点告诉发送节点「我忙,慢点发」。结构是:6个显性位 + 8个隐性位。但说实话,我在实际项目中几乎没见过过载帧。现在的CAN控制器处理速度都很快,很少出现「忙不过来」的情况。

2.5 仲裁机制:没有裁判的比赛

CAN总线的仲裁机制,是我觉得最巧妙的设计。多个节点同时发送时,它们通过线与逻辑来决定谁赢。

规则很简单:

  1. 每个节点从SOF开始,逐位发送ID。
  2. 如果某个节点发送了隐性位(1),但总线上是显性位(0),它就自动退出,转为接收模式。
  3. 剩下的节点继续发送,直到只剩一个。

你想想看,这个过程就像一群人同时喊话,声音最大的那个(ID最小)会留下来。其他人都自动闭嘴。

关键点:仲裁不会破坏数据。因为退出节点的隐性位被显性位覆盖了,但数据本身没有丢失。这就是CAN的「无损仲裁」——我特别喜欢这个词,它完美描述了CAN的优雅。

我在项目中遇到过一个问题:两个节点ID相同,同时发送数据帧。结果总线一直报错。查了半天,发现是配置错误,两个节点用了同一个ID。记住:CAN总线上每个ID只能有一个发送节点。接收节点可以有多个,但发送节点必须唯一。

2.6 位填充:保证时钟同步的「小把戏」

位填充(Bit Stuffing)是CAN的一个隐藏技能。它的规则是:连续发送5个相同位后,自动插入一个相反位

为什么要这么做?因为CAN节点是靠电平跳变来同步时钟的。如果连续发送太多相同的位(比如111111),接收节点的时钟就会漂移,导致采样错误。

举个例子:

原始数据:11111 00000 11111
填充后:  111110 000001 111110

注意:位填充只覆盖SOF到CRC段。ACK和EOF段不填充。这一点在实现CAN控制器时特别重要——如果你在EOF段也做了位填充,接收节点会认为帧格式错误。

个人经验:我在调试一个高速CAN网络(1Mbps)时,发现偶尔会出现CRC错误。后来用示波器一看,原来是位填充导致的数据位变化,影响了CRC计算。解决方案是:确保发送和接收双方使用相同的位填充规则。嗯,听起来是废话,但真的有人会搞错。

2.7 小结与实战建议

好了,这一章的内容就到这里。我们来总结一下:

  • CAN有五种帧,数据帧是核心,远程帧慎用,错误帧是保命符。
  • 仲裁机制靠ID优先级,数值越小越优先。
  • 位填充保证时钟同步,但要注意范围。

下一章我们会讲CAN的物理层——差分信号、终端电阻、总线拓扑。到时候我会分享一个我在现场调试时,因为终端电阻没接好导致通信失败的案例。嗯,那真是一段难忘的经历。

如果你在项目中遇到CAN帧结构相关的问题,欢迎随时交流。记住:理解帧结构,是掌握CAN协议栈的第一步