2. CAN协议详解:五种帧结构
CAN协议里最核心的,就是这五种帧。我刚开始接触CAN时,总觉得帧类型太多记不住。后来在项目里调了几个月,发现其实就两类:数据帧和远程帧是正常通信用的,剩下三种——错误帧、过载帧、帧间隔——都是总线管理和异常处理用的。
咱们一个一个拆开看。
2.1 数据帧:最常用的帧
数据帧就是用来发数据的。你想想看,一个传感器采集到温度值,要告诉ECU,用的就是数据帧。
数据帧分两种格式:标准帧(11位ID)和扩展帧(29位ID)。我个人习惯在项目初期就定好用哪种,免得后期改协议栈。
数据帧结构(标准帧):
SOF → 仲裁场(11位ID+RTR) → 控制场(IDE+r0+DLC) → 数据场(0~8字节) → CRC场(15位CRC+CRC分隔符) → ACK场(ACK槽+ACK分隔符) → EOF(7位隐性位)
这里有个关键点:数据场最多8字节。为什么是8?因为CAN总线设计时考虑实时性,8字节刚好够一个控制命令或传感器数据。我在做电机控制器时,经常要把电流、转速、温度三个值塞进一帧里,8字节其实挺紧的。
| 字段 | 长度 | 说明 |
|---|---|---|
| SOF | 1 bit | 帧起始,显性位,同步所有节点 |
| 仲裁场 | 12 bit | 11位ID + 1位RTR(数据帧RTR=0) |
| 控制场 | 6 bit | IDE + r0 + DLC(4位) |
| 数据场 | 0~64 bit | 实际数据,最多8字节 |
| CRC场 | 16 bit | 15位CRC + 1位CRC分隔符 |
| ACK场 | 2 bit | ACK槽 + ACK分隔符 |
| EOF | 7 bit | 帧结束,全部隐性位 |
小技巧:DLC(数据长度码)可以设为0~8,但实际数据场长度必须≤DLC。我曾经见过一个bug,DLC设了8但只发了3字节,接收方读到的是垃圾数据。所以发送前一定要把数据场清零。
2.2 远程帧:问别人要数据
远程帧说白了就是“请求帧”。一个节点想从另一个节点拿数据,就发个远程帧。远程帧和数据帧长得几乎一样,唯一的区别是RTR位:数据帧RTR=0,远程帧RTR=1。
远程帧没有数据场。你想想看,既然只是请求数据,带数据干嘛?
注意:远程帧的DLC必须等于期望接收的数据长度。比如你想请求8字节数据,DLC就设8。如果设错了,接收方可能不响应。
我在做BMS(电池管理系统)时用过远程帧。主控发一个远程帧,ID=0x100,DLC=8,从机收到后立刻回复数据帧。这种方式省去了轮询的麻烦。
2.3 错误帧:总线出问题时的信号
错误帧是CAN协议里最“暴力”的帧。一旦某个节点检测到错误,它会立刻发送错误帧,把整个总线上的数据都破坏掉。嗯,听起来很粗暴,但这就是CAN的设计哲学——错误优先。
错误帧分两部分:
- 错误标志:6个连续显性位(主动错误)或6个连续隐性位(被动错误)
- 错误分隔符:8个连续隐性位
我曾经在项目里遇到过一个问题:总线上一有错误帧,整个网络就瘫痪了。后来发现是某个节点的终端电阻没焊好,导致信号反射。所以硬件设计时,终端电阻一定要焊牢。
错误类型:
- 位错误:发送的位和总线上的位不一致
- 填充错误:连续5个相同位后没有填充位
- CRC错误:接收方计算的CRC和发送方的不一致
- 形式错误:固定格式的位(如EOF)值不对
- ACK错误:发送方没收到ACK应答
2.4 过载帧:我忙,别发数据了
过载帧和错误帧长得很像,但用途完全不同。过载帧是节点告诉总线:“我处理不过来了,暂停一下。”
过载帧的结构:
- 过载标志:6个连续显性位
- 过载分隔符:8个连续隐性位
你可能会问:这和错误帧有什么区别?区别在于触发条件。错误帧是检测到错误才发,过载帧是节点内部缓冲区满了才发。我在做高速CAN(1Mbps)时,如果接收中断处理太慢,就会触发过载帧。后来我把中断优先级调高,问题就解决了。
避坑指南:过载帧不能连续发太多。CAN协议规定,两个过载帧之间至少间隔3个位时间。我曾经在测试时连续触发过载帧,结果总线直接挂了。后来加了延时才稳定。
2.5 帧间隔:总线休息时间
帧间隔就是两帧之间的空闲时间。CAN协议规定,每帧结束后必须有至少3个隐性位作为帧间隔。这3个位叫间歇场。
帧间隔的作用:
- 给节点时间处理接收到的数据
- 让总线恢复隐性状态
- 防止连续帧冲突
我记得有一次调试,发现数据丢帧严重。用示波器一看,帧间隔只有1个位时间。原来是软件里把帧间隔设得太短了。改成3个位后,一切正常。
2.6 五种帧的关系图
下面这张图展示了五种帧在总线上的关系。你可以看到,正常通信时只有数据帧和远程帧,错误帧和过载帧是异常情况才出现的。
2.7 位填充机制
CAN协议有个很有意思的机制——位填充。发送方在连续发送5个相同位后,必须插入一个相反位。比如你发了5个1,第6位必须插个0。
为什么要这么做?
- 保证时钟同步:接收方靠跳变沿同步时钟
- 防止误判:连续相同位太多,接收方可能丢失同步
位填充的范围是从SOF到CRC场结束。EOF、ACK场、帧间隔不参与填充。我在做CAN驱动时,位填充逻辑是硬件自动完成的,但调试时得知道这个机制,不然看到波形上的“多余位”会懵。
位填充示例:
原始数据:11111 00000 10101
填充后:111110 000001 10101
注意:填充位用粗体标出
2.8 仲裁机制:谁优先级高谁先发
CAN总线是多主架构,多个节点可以同时发数据。那冲突了怎么办?靠仲裁。
仲裁的原理很简单:ID越小,优先级越高。发送时,每个节点逐位比较自己的ID和总线上的电平。如果某个节点发送了隐性位(1),但总线上是显性位(0),说明有更高优先级的节点在发数据,这个节点就自动退出发送。
我举个例子:
- 节点A:ID = 0x100(二进制:001 0000 0000)
- 节点B:ID = 0x200(二进制:010 0000 0000)
节点A的第2位是0(显性),节点B的第2位是1(隐性)。所以节点A胜出,节点B退出发送。整个过程在硬件层面完成,软件完全无感。
经验之谈:设计CAN网络时,把实时性要求高的节点(如刹车、转向)分配小ID,普通节点(如车窗、灯光)分配大ID。我在做车控项目时,刹车节点的ID是0x001,车窗是0x300,这样刹车永远优先。
2.9 总结
五种帧各有各的用途。数据帧和远程帧是干活用的,错误帧和过载帧是处理异常用的,帧间隔是让总线喘口气用的。理解这些帧结构,是写CAN驱动的基础。