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总线的仲裁机制,是我觉得最巧妙的设计。多个节点同时发送时,它们通过线与逻辑来决定谁赢。
规则很简单:
- 每个节点从SOF开始,逐位发送ID。
- 如果某个节点发送了隐性位(1),但总线上是显性位(0),它就自动退出,转为接收模式。
- 剩下的节点继续发送,直到只剩一个。
你想想看,这个过程就像一群人同时喊话,声音最大的那个(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协议栈的第一步。