2. CAN数据帧结构:逐字节解析
CAN总线通信里,数据帧就是核心通信单元。说白了,它就是一段有严格格式的二进制序列,发一方按这个格式打包,收一方按这个格式拆包。我刚开始做CAN开发时,总喜欢直接看逻辑分析仪波形,结果遇到一次总线故障抓瞎了。后来老老实实把帧结构背熟,再抓包分析,效率翻倍。
标准CAN数据帧由7个场组成:帧起始、仲裁场、控制场、数据场、CRC场、ACK场、帧结束。下面我一个一个拆开讲,顺便说说我踩过的坑。
2.1 帧起始(SOF)
SOF只有1个位,显性电平(逻辑0)。它的作用是同步总线上的所有节点。所有节点必须在SOF的下降沿同步采样时钟。
我记得有一次客户反馈说设备间歇性丢帧,我抓了波形发现SOF之后紧接着就出现了错误帧。排查下来,原来是有个节点因为电压不稳,在SOF阶段拉低失败,导致其他节点认为总线空闲。嗯,这个坑让我花了整整一周。
提示: 如果示波器上看到CAN_H/CAN_L在帧起始位置没有明显的显性跳变,大概率是发送节点驱动能力不足或总线负载过大。
2.2 仲裁场
仲裁场包含两部分:标识符(ID)和RTR位(远程发送请求)。标准帧用11位ID,扩展帧用29位ID。IDE位(控制场第一位)来区分标准/扩展。
- 标准帧(11位ID): 仲裁场共12位:11位ID + 1位RTR。ID高位在前,低位在后。RTR在数据帧中为显性,远程帧中为隐性。
- 扩展帧(29位ID): 仲裁场共32位:11位基础ID + SRR位(1位,隐性) + IDE位(1位,显性) + 18位扩展ID + RTR位。
关于仲裁机制:多个节点同时发送时,ID值越小的节点优先级越高(显性位覆盖隐性位)。我曾经在项目里遇到两个节点ID相同,导致总线一直处于冲突状态。查了规格书才发现是某节点固件错误地把ID写成了同一个值。
| 特性 | 标准帧 | 扩展帧 |
|---|---|---|
| ID长度 | 11位 | 29位 |
| 仲裁场总位长 | 12位 | 32位 |
| IDE位位置 | 在控制场首位(显性) | 在仲裁场第15位(显性) |
| RTR位位置 | 仲裁场最后1位 | 仲裁场最后1位 |
| 优先级 | 标识符越小,优先级越高 | 同左,但扩展帧需额外占位 |
2.3 控制场
控制场共6位:IDE位(1位,若为标准帧则显性,扩展帧此处为隐性保留位r1)、保留位r0(1位,显性)、DLC(4位,数据长度码)。
DLC的取值范围是0~8(对应数据场0~8字节),但实际发送的数据字节数不能超过DLC指示的值。如果用CAN FD,DLC还可以编码9~15,但那是后话了。
我之前修过一个bug:某设备发送数据帧时DLC写的是0x08,但数据场只塞了4个字节。结果对端解析时多读4个垃圾数据,导致判断逻辑全部出错。所以:DLC必须跟数据场实际长度严格一致。
// DLC编码(标准CAN)
DLC = 0x00 → 0字节数据
DLC = 0x01 → 1字节数据
...
DLC = 0x08 → 8字节数据
2.4 数据场
数据场长度由DLC决定,范围0~8字节。发送时先发字节的高位(MSB优先)。每个字节内先发高位位,后发低位位。
htonl之类的函数统一处理。
2.5 CRC场(循环冗余校验)
CRC场包含15位CRC序列 + 1位CRC界定符(隐性)。计算范围从SOF到数据场结束(包括填充位)。生成多项式是:x¹⁵ + x¹⁴ + x¹⁰ + x⁸ + x⁷ + x⁴ + x³ + 1。
接收节点计算本地CRC后与接收到CRC序列比对,不一致就报CRC错误。我遇到过一次场景:总线长度超过100米,波特率500kbps,终端电阻匹配合适,但CRC错误率还是很高。最后发现是线缆特性阻抗不匹配,导致信号反射,采样点偏移。换了高质量双绞线后问题解决。
// CRC计算示例(伪代码)
uint16_t calc_crc(uint8_t *data, uint8_t len) {
uint16_t crc = 0x0000;
for (uint8_t i = 0; i < len; i++) {
crc ^= (data[i] << 7);
for (uint8_t j = 0; j < 8; j++) {
if (crc & 0x8000)
crc = (crc << 1) ^ 0x4599;
else
crc = crc << 1;
}
}
return crc & 0x7FFF;
}
2.6 ACK场(应答场)
ACK场有2位:ACK Slot(1位)+ ACK界定符(1位,隐性)。
发送节点在ACK Slot发送显性位(实际它会在该位时间释放总线,置为隐性),所有接收节点如果正确接收帧,则必须在该位时间发送显性位来确认(拉低总线)。如果发送节点检测到ACK Slot为隐性(没有节点确认),它会报ACK错误并重发。
我曾经遇到一个情况:节点A发送数据,节点B明明收到了也确认了,但A还是报ACK错误。查了半天,发现B的CAN控制器确认窗口配置错了,把ACK Slot的采样点设在了位时间的末尾,正好错过。那段时间我们天天加焊线抓波形,总算找出来了。
2.7 帧结束(EOF)
EOF由7位隐性电平组成。连续7个隐性位给总线一个“安静”信号,表示帧结束。此后至少还有3位帧间隔(ITM)才能开始下一帧(被动错误节点的帧间隔会延长)。
如果你的逻辑分析仪上看到EOF只有6位隐性就跳到了新帧起始,那说明总线上出现了帧格式错误。我曾在一款盗版CAN收发器上遇到过,它的EOF检测逻辑有bug,导致总线混乱。
2.8 完整帧结构一览
为了帮你快速记忆,我把所有场的位长度和功能汇总成表:
| 场名称 | 位长度 | 主要内容 | 关键注意 |
|---|---|---|---|
| 帧起始 | 1 | 显性位 | 用于同步 |
| 仲裁场 | 12(标准帧)或32(扩展帧) | ID + RTR | ID越小优先级越高 |
| 控制场 | 6 | IDE + r0 + DLC | DLC与数据长度严格对应 |
| 数据场 | 0~64(0~8字节×8位) | 用户数据 | MSB优先,注意字节序 |
| CRC场 | 16(15位CRC + 界定符) | CRC校验值 | 计算范围从SOF到数据结束 |
| ACK场 | 2(1位ACK + 界定符) | 接收节点确认 | 无ACK会触发错误帧 |
| 帧结束 | 7 | 隐性位 | 连续7个隐性 |
2.9 帧结构可视化
下面这张图把整个CAN数据帧的骨架画出来了。从左到右依次是7个场,颜色区分了不同段。你看一眼就能在脑子里记住帧的布局。
拿到这张图,你再配合上面每个场的详解,就能看懂大部分CAN故障日志了。下次做抓包分析时,把帧结构摆旁边,一行一行对照着看,问题自然就跳出来了。