CAN数据帧详解:标准帧与扩展帧、数据帧结构
大家好,我是你们的车载总线讲师。今天咱们来啃一块硬骨头——CAN数据帧的结构。说实话,很多工程师干了三五年,调CAN通信还是靠复制粘贴,帧格式都搞不清楚。我当年刚入行时也踩过这个坑,有一次调试总线死活不通,最后发现是DLC设错了。嗯,从那以后,我养成了一个习惯:每帧数据都亲手拆一遍。
一、标准帧 vs 扩展帧:到底差在哪?
CAN总线有两种帧格式:标准帧(CAN 2.0A)和扩展帧(CAN 2.0B)。说白了,它们最大的区别就是——ID的长度。
- 标准帧:11位ID,最多2^11=2048个不同ID。
- 扩展帧:29位ID,最多2^29=5亿多个ID。
你想想看,一辆现代汽车上有几十个ECU,每个ECU可能发几十种信号。2048个ID够用吗?早期够,现在肯定不够。所以扩展帧就出来了。
| 对比项 | 标准帧 | 扩展帧 |
|---|---|---|
| ID长度 | 11位 | 29位 |
| 仲裁场结构 | ID + RTR + IDE + r0 | ID基础 + SRR + IDE + ID扩展 + RTR + r1 |
| 帧长度 | 较短(约47~111位) | 较长(约67~131位) |
| 兼容性 | 所有CAN控制器都支持 | 需要支持CAN 2.0B的控制器 |
| 典型应用 | 简单系统、老车型 | 复杂系统、新车型、诊断 |
二、数据帧结构:逐位拆解
一个完整的CAN数据帧,从SOF到EOF,一共7个场。咱们一个一个来看。
三、逐场详解
1. SOF(Start of Frame)
SOF只有1位,必须是显性(逻辑0)。它就像发令枪——所有节点看到SOF后,都要同步自己的时钟。我见过一些新手,以为SOF只是标志位,其实它还有同步功能。在高速CAN中,SOF的下降沿用来重新同步所有节点的采样点。
2. 仲裁场
这是CAN最精彩的部分。仲裁场包含ID和RTR位。多个节点同时发送时,谁ID小谁赢。为什么?因为显性位会覆盖隐性位。你想想看,如果节点A发ID=0x100,节点B发ID=0x200,在ID的第9位,A发0(显性),B发1(隐性),A就赢了。
3. 控制场
控制场有6位:IDE(1位)、r0(1位,保留位)、DLC(4位)。IDE=0表示标准帧,IDE=1表示扩展帧。DLC表示数据场有多少字节,范围0~8。注意:DLC可以设0,就是不发数据,只发ID——这叫远程帧,后面会讲。
4. 数据场
数据场0~8字节,按大端序(Motorola格式)或小端序(Intel格式)排列。具体用哪种,看你的协议。我个人习惯用Motorola,因为CAN协议栈默认就是大端。但很多OEM喜欢Intel,比如大众。嗯,这个没有对错,团队统一就行。
5. CRC场
CRC场包含15位CRC校验码和1位界定符。CRC算法是固定的:x^15 + x^14 + x^10 + x^8 + x^7 + x^4 + x^3 + 1。发送节点计算从SOF到数据场的CRC,接收节点重新计算,不一致就报错。这个机制很可靠,我几乎没见过CRC校验出错的情况——除非硬件坏了。
6. ACK场
ACK场有2位:ACK Slot和ACK界定符。发送节点在ACK Slot发隐性位,接收节点如果正确收到帧,就发显性位覆盖它。发送节点检测到显性位,就知道至少有一个节点收到了。如果没人确认,发送节点会重发。
7. EOF(End of Frame)
EOF是7位隐性位。为什么是7位?因为要防止误判。如果EOF太短,可能被当成下一个帧的SOF。7位隐性位保证了帧间隔的最小长度。EOF之后,总线进入3位ITM(Intermission),然后才能发下一帧。
四、扩展帧的特殊位
扩展帧在仲裁场多了几个位:SRR、IDE、r1。SRR(Substitute Remote Request)替代标准帧的RTR位,IDE=1表示扩展帧,r1是保留位。这些位在标准帧里没有,所以扩展帧的仲裁场更长——29位ID + 这些控制位,总共32位。
| 位名称 | 位置 | 说明 |
|---|---|---|
| SRR | 标准帧RTR位的位置 | 扩展帧中必须为隐性(1),保证标准帧优先级更高 |
| IDE | 控制场第1位 | 标准帧=0,扩展帧=1 |
| r1 | 扩展帧控制场第2位 | 保留位,必须为显性(0) |
这里有个细节:SRR位在扩展帧里是隐性,而标准帧的RTR是显性。所以,如果标准帧和扩展帧的ID相同,标准帧的优先级更高。这是协议设计者故意为之——保证老设备不被新设备抢占。
五、实战:如何用代码构造一个CAN数据帧
下面是一个简单的C代码示例,构造一个标准CAN数据帧。实际项目中,你会用CAN驱动库,但理解底层构造很重要。
// 构造一个标准CAN数据帧
typedef struct {
uint16_t id; // 11位ID,实际只用低11位
uint8_t dlc; // 数据长度(0~8)
uint8_t data[8]; // 数据场
} CAN_Frame_Std;
void CAN_SendFrame(CAN_Frame_Std *frame) {
// 1. 发送SOF(硬件自动处理)
// 2. 发送仲裁场:ID + RTR(这里RTR=0表示数据帧)
uint16_t arbitration = (frame->id & 0x7FF) << 1; // 11位ID左移1位,RTR位为0
// 3. 发送控制场:IDE=0, r0=0, DLC
uint8_t control = (0 << 6) | (0 << 5) | (frame->dlc & 0x0F);
// 4. 发送数据场
for (int i = 0; i < frame->dlc; i++) {
// 逐字节发送frame->data[i]
}
// 5. CRC场由硬件自动计算和发送
// 6. ACK场由硬件处理
// 7. EOF由硬件自动发送
printf("帧已发送:ID=0x%03X, DLC=%d\n", frame->id, frame->dlc);
}