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位
优先级标识符越小,优先级越高同左,但扩展帧需额外占位
注意: IDE位在标准帧中位于控制场第一位,在扩展帧中位于仲裁场。很多初学者在这上面搞混,导致解析帧头出错。

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优先)。每个字节内先发高位位,后发低位位。

实战技巧: 很多工程师喜欢用结构体打包数据。注意字节序需要跟CAN控制器一致。比如你在Cortex-M上用了小端模式,而CAN控制器按大端发送,那抓包看到的数据会跟你直觉相反。我习惯用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的采样点设在了位时间的末尾,正好错过。那段时间我们天天加焊线抓波形,总算找出来了。

要点: 抓包时看到ACK错误帧,第一时间检查接收节点的确认逻辑和总线终端电阻。多数情况不是芯片坏了,而是电气或配置问题。

2.7 帧结束(EOF)

EOF由7位隐性电平组成。连续7个隐性位给总线一个“安静”信号,表示帧结束。此后至少还有3位帧间隔(ITM)才能开始下一帧(被动错误节点的帧间隔会延长)。

如果你的逻辑分析仪上看到EOF只有6位隐性就跳到了新帧起始,那说明总线上出现了帧格式错误。我曾在一款盗版CAN收发器上遇到过,它的EOF检测逻辑有bug,导致总线混乱。


2.8 完整帧结构一览

为了帮你快速记忆,我把所有场的位长度和功能汇总成表:

标准CAN数据帧各场位长汇总
场名称位长度主要内容关键注意
帧起始1显性位用于同步
仲裁场12(标准帧)或32(扩展帧)ID + RTRID越小优先级越高
控制场6IDE + r0 + DLCDLC与数据长度严格对应
数据场0~64(0~8字节×8位)用户数据MSB优先,注意字节序
CRC场16(15位CRC + 界定符)CRC校验值计算范围从SOF到数据结束
ACK场2(1位ACK + 界定符)接收节点确认无ACK会触发错误帧
帧结束7隐性位连续7个隐性

2.9 帧结构可视化

下面这张图把整个CAN数据帧的骨架画出来了。从左到右依次是7个场,颜色区分了不同段。你看一眼就能在脑子里记住帧的布局。

CAN数据帧结构图 CAN标准数据帧结构(逐字节) SOF 1位 仲裁场 ID+RTR 12/32位 控制场 IDE+r0+DLC 6位 数据场 0~8字节 0~64位 CRC场 CRC+界定符 16位 ACK场 Slot+界定符 2位 帧结束 EOF 7位 帧起始 仲裁场 控制场 数据场 CRC场 ACK场 帧结束 整个帧按位流顺序发送:先发SOF,接着仲裁场、控制场、数据场、CRC场、ACK场,最后EOF。 每个场之间无间隔,连续发送。接收节点必须严格按照这个顺序解析。

拿到这张图,你再配合上面每个场的详解,就能看懂大部分CAN故障日志了。下次做抓包分析时,把帧结构摆旁边,一行一行对照着看,问题自然就跳出来了。