3、CAN数据链路层:帧结构(SOF、ID、RTR、IDE、DLC、Data、CRC、ACK、EOF)、位填充机制
各位工程师朋友,咱们今天聊聊CAN总线最核心的部分——数据链路层。说白了,就是CAN报文在线上到底长什么样,每个比特位是干什么用的。
我记得刚入行那会儿,拿着逻辑分析仪抓CAN波形,看着一堆高低电平完全懵圈。后来师傅丢给我一本CAN协议规范,说:“你把帧结构背下来,就能看懂一半。” 嗯,这话不假。今天我就把这块掰开揉碎了讲给你听。
3.1 CAN帧的骨架:从SOF到EOF
一个标准的CAN数据帧,就像一列火车。车头是起始位,车厢是数据,车尾是结束标志。咱们一节一节拆开看。
| 字段 | 位数 | 作用 |
|---|---|---|
| SOF | 1 bit | 帧起始,同步所有节点 |
| ID | 11 bit (标准帧) / 29 bit (扩展帧) | 标识符,决定优先级 |
| RTR | 1 bit | 远程帧标志,0=数据帧,1=远程帧 |
| IDE | 1 bit | 扩展标志,0=标准帧,1=扩展帧 |
| DLC | 4 bit | 数据长度,0~8字节 |
| Data | 0~64 bit | 实际数据 |
| CRC | 15 bit | 循环冗余校验 |
| ACK | 2 bit | 应答槽 + 应答界定符 |
| EOF | 7 bit | 帧结束 |
3.2 逐个字段深度解析
SOF(Start of Frame)
一个显性位(逻辑0)。所有节点看到这个下降沿,就知道“要开始收报文了”。我在项目中遇到过一个问题:总线干扰导致SOF误触发,整个网络乱发报文。后来加了硬件滤波才解决。
ID(Identifier)
这是CAN总线的灵魂。ID越小,优先级越高。你想想看,如果刹车报文和车窗报文同时发送,谁先走?当然是刹车。ID就是用来干这个的。
标准帧11位ID,扩展帧29位ID。我个人习惯在TPMS(胎压监测)项目里用标准帧就够了,毕竟传感器数据没那么复杂。但如果你在做车载诊断(UDS),扩展帧更灵活。
RTR(Remote Transmission Request)
这个位很有意思。数据帧里RTR=0,远程帧里RTR=1。远程帧说白了就是“请求帧”——一个节点问另一个节点:“嘿,把你的数据发给我”。
我曾经在调试时犯过一个低级错误:把RTR写反了,结果对方一直不发数据,我还以为是硬件坏了。嗯,这种坑踩过一次就记住了。
IDE(Identifier Extension)
IDE=0表示标准帧,IDE=1表示扩展帧。这个位决定了后面跟的是11位ID还是29位ID。注意:标准帧和扩展帧可以在同一条总线上共存,但优先级比较时,IDE位会影响仲裁结果。
DLC(Data Length Code)
4位二进制,表示数据场有多少个字节。范围0~8。注意:DLC可以写0,表示不传数据,这在某些控制命令里很常见。
Data Field
0~8字节的实际数据。在TPMS里,我通常用前2个字节放胎压值,中间2个字节放温度,后面4个字节留作备用或校验。数据排列顺序要注意:CAN协议是大端模式(Big-Endian),高位字节在前。
CRC(Cyclic Redundancy Check)
15位CRC + 1位CRC界定符。CAN的CRC算法是固定的,硬件自动计算。但我要提醒你:CRC只能检测错误,不能纠正。所以如果CRC校验失败,接收节点会直接丢弃这帧报文。
ACK(Acknowledge)
2位:ACK Slot + ACK Delimiter。发送节点在ACK Slot输出隐性位(1),接收节点如果正确收到,就拉成显性位(0)。这就是“应答”的过程。
我见过一个案例:总线终端电阻没接好,导致ACK信号反射,发送节点总以为自己发失败了,疯狂重发。最后查出来是终端电阻虚焊。
EOF(End of Frame)
7个隐性位(1)。表示帧结束。之后总线进入空闲状态(隐性),等待下一帧SOF。
3.3 位填充机制:为什么CAN要“插队”
你可能会问:为什么CAN协议要搞个位填充?直接传数据不行吗?
原因很简单:同步。CAN总线没有单独的时钟线,所有节点靠电平跳变来同步时钟。如果连续出现5个以上的相同电平(全是0或全是1),节点就会失去同步。
位填充规则:发送节点在连续发送5个相同位后,自动插入一个相反电平的位。接收节点收到后,自动把这个填充位去掉。
举个例子:
原始数据:11111 00000 111
填充后:111110 000001 111
注意:填充位用粗体标出。接收端看到连续5个1后跟一个0,就知道这个0是填充位,直接丢弃。
个人经验:位填充会影响实际传输速率。比如你要发8字节数据,理论上64位就够了,但因为填充机制,实际线上可能多出10~20位。我在做TPMS实时性分析时,必须把这个开销算进去,否则会低估总线负载率。
3.4 避坑指南:我曾经踩过的雷
- ID冲突:两个节点用同一个ID发送数据,总线会乱套。我在一个项目里没仔细核对ID分配表,结果左前轮和右前轮的胎压数据混在一起,排查了两天才发现。
- DLC与实际数据长度不匹配:DLC写4,但数据场只填了3个字节,接收端会读到垃圾数据。一定要保证DLC和实际数据长度一致。
- CRC计算错误:虽然硬件自动算CRC,但如果你用软件模拟CAN控制器(比如用SPI转CAN的芯片),CRC必须自己算。我建议直接用查表法,别手算,容易出错。
- ACK没收到:如果总线上只有一个节点在发,没有接收节点,ACK Slot会保持隐性位,发送节点会认为发送失败。这在单节点测试时经常遇到,别慌,加个回环模式就行。
3.5 实战:用代码解析CAN帧
下面是我在TPMS项目里写的一段解析代码,用C语言实现。你可以直接拿去用。
// CAN帧解析函数
// 输入:原始CAN帧数据(从CAN控制器读取)
// 输出:解析后的帧结构
typedef struct {
uint16_t id; // 11位ID
uint8_t rtr; // 0=数据帧, 1=远程帧
uint8_t dlc; // 数据长度
uint8_t data[8]; // 数据场
uint8_t crc_ok; // CRC校验结果
} CAN_Frame_t;
void ParseCANFrame(uint32_t raw_id, uint8_t* raw_data, uint8_t dlc, CAN_Frame_t* frame) {
// 提取ID(标准帧)
frame->id = raw_id & 0x7FF;
// 提取RTR
frame->rtr = (raw_id >> 4) & 0x01;
// 提取DLC
frame->dlc = dlc & 0x0F;
// 复制数据
for (int i = 0; i < frame->dlc; i++) {
frame->data[i] = raw_data[i];
}
// CRC校验(假设硬件已经完成)
frame->crc_ok = 1; // 实际项目中从硬件寄存器读取
}
注意:上面的代码只适用于标准帧。如果你用扩展帧,ID提取要改成29位,IDE位也要单独处理。另外,实际项目中CRC校验结果通常由CAN控制器硬件提供,不需要软件算。
3.6 小结
CAN数据链路层的帧结构,说白了就是一套“通信协议”。SOF是发令枪,ID是优先级,Data是快递包裹,CRC是防伪标签,ACK是回执单,EOF是结束语。
位填充机制虽然增加了开销,但保证了总线同步的可靠性。没有它,CAN总线在长距离、高干扰的环境下根本跑不起来。
下一章咱们聊聊CAN的物理层——差分信号和终端电阻。到时候我会分享一个因为终端电阻没接好导致整车通信瘫痪的案例,保证让你印象深刻。