CAN协议种类与数据帧结构深度解析:从CAN 2.0到CAN FD
2026/9/17 8:53:58 网站建设 项目流程

很多人一开始接触CAN协议,都会直接去翻数据手册、看帧格式图,然后被那一堆“SOF、仲裁场、控制场、数据场、CRC场、ACK场”整得有点懵。我当年做车载项目的时候也是这样,第一次用CANoe抓总线报文,满屏十六进制看了一下午,愣是没搞明白一个0x123到底代表什么。后来踩的坑多了,才意识到一个事:CAN协议看着不难,但如果你不理解它的协议种类差异,也没把数据帧结构吃透,后面做解析、做过滤、做仿真、排查总线故障,每一步都会很难受。

这篇文章我就把自己对CAN协议种类以及CAN数据帧的理解梳理一遍,结合我实际做过的项目,尽量用大白话把底层的原理和操作细节讲清楚。内容适合刚入门嵌入式、汽车电子、工业控制相关的开发者,也适合那些做过一段时间CAN开发但某些细节一直没弄明白的朋友。

1. 先搞懂CAN协议的种类:2.0A、2.0B、CAN FD到底差在哪

在动手写代码之前,必须先搞清楚一个问题:你平时说的“CAN协议”,可能根本不是同一种东西。很多人拿着CAN 2.0B的节点去接一个CAN FD的控制器,结果总线上一帧都发不出去,还以为是硬件坏了,其实是对协议种类不熟。

1.1 从2.0A到2.0B:标准和扩展帧的区别是ID长度

先说两个最常见的老协议:CAN 2.0A和CAN 2.0B。这两个都是博世在1986年之后陆续发布的标准,定义了经典CAN总线的基本通信机制。

CAN 2.0A的标准帧,标识符(ID)只有11位,所以最多能分配2的11次方,也就是2048个不同的ID;而CAN 2.0B的扩展帧,标识符是29位,能分配的ID数量直接到了2的29次方。实际使用中这个数字已经可以当作“无限大”来看待了。

11位和29位可不只是长度差别,它们背后的帧结构也不同。标准帧比扩展帧少了整整12个bit(这里说的是仲裁场部分,更严格说是少了ID扩展位、SRTR位和扩展ID部分),所以标准帧的传输效率要高一点点。但是,如果车上有多个ECU,每个ECU都要上报几十个信号,2048个ID很容易用完,所以整车厂基本都采用29位扩展帧。

很多初学者会混淆一个概念:标准帧和扩展帧,指的是帧格式,不是协议版本。CAN 2.0B的控制器完全兼容CAN 2.0A,它可以收发标准帧,只是标准帧的ID范围只有11位。反过来说,一个只管CAN 2.0A的老控制器,遇到29位扩展帧就无法解析,因为它压根不识别后面那些扩展ID位。

1.2 CAN FD为什么越来越常见

如果你最近接触过稍微新一点的MCU,比如STM32H7系列、英飞凌TC3xx系列,或者一些带CAN收发器的车载域控制器,大概率会看到CAN FD的字样。CAN FD全称是CAN with Flexible Data-rate,翻译过来就是“可变数据速率CAN”。

CAN FD和经典CAN最大的区别,简单说就两点:

第一,数据长度不再是固定8字节的上限,而是可以扩展到64字节。这一下就解决了大数据量传输的痛点。以前一个Bootloader升级文件,几百KB的数据,用8字节一帧地去发,一个ECU升级可能要十几分钟。现在CAN FD一次能带64字节,升级时间直接缩短好几倍。

第二,传输速率可以切换。CAN FD在仲裁段继续使用经典CAN的速率(比如500kbps),但在数据段可以切换到更高的速率(比如2Mbps甚至5Mbps)。这个设计很聪明,因为在仲裁阶段,总线上多节点要通过“线与”机制仲裁输赢,速度太快会造成同步偏差,所以不能随便提速;但到了数据段,只有获胜的那一个节点在发数据,不存在竞争关系,就可以放开手脚跑高速。

打个比方,经典CAN就是一条双向单车道,所有车排队通过,速度上限有限;CAN FD则是到了中间一段路突然变成多车道,已经确定要通行的那辆车可以踩油门加速,从而提高整条路的通过效率。

对比项经典CAN 2.0A/BCAN FD
最大数据长度8字节64字节
数据段最高速率与仲裁段相同,通常不超过1Mbps最高5Mbps(ISO 11898-1:2015)
ID类型11位/29位11位/29位,兼容经典帧
协议开销较低相对更高,但因为数据段提速,整体效率更高
典型应用传统动力CAN网络、OBD诊断整车域控、自动驾驶、Bootloader升级

还有一点需要注意:物理层的收发器也有区别。支持CAN FD的收发器通常具备更高的压摆率,比如TJA1044、TJA1043这些带“FD”字样的产品,而老的TJA1050在高速切换时可能会因为信号振铃导致数据错误。所以如果你要把一个老节点的CAN通信升级成CAN FD,光改MCU里的寄存器配置不够,收发器也得换。

2. 数据帧结构逐段解析:从头到尾看懂一帧CAN报文

前面把协议种类搞清楚了,接下来就是硬核部分——CAN数据帧结构。一帧CAN报文在总线上长什么样、每一位起到什么作用、发送和接收双方又是怎么协作的,只有把这些彻底弄明白,后面你才能手写解析代码、排查总线错误、设计通信矩阵。

2.1 帧格式总览:从SOF到EOF一共多少位

先看整体。一帧经典CAN标准数据帧,完整包含如下各段:

起始位(SOF)+ 仲裁场 + 控制场 + 数据场 + CRC场 + ACK场 + 帧结束(EOF)

用更细的位来数一遍:

  • 起始位(SOF):1位,显性“0”,表示一帧开始。
  • 仲裁场:标准帧是11位ID + 1位RTR;扩展帧是11位基础ID + 1位SRR + 1位IDE + 18位扩展ID + 1位RTR。
  • 控制场:标准帧是1位IDE + 1位r0 + 4位DLC;扩展帧是1位r1 + 1位r0 + 4位DLC。
  • 数据场:0到8字节,也就是0到64位,实际长度由DLC决定。
  • CRC场:15位CRC校验值 + 1位CRC界定符(隐性位)。
  • ACK场:1位ACK槽 + 1位ACK界定符(隐性位)。
  • EOF:7位连续的隐性位“1”,表示帧结束。

算一下最常用的8字节数据帧,比如标准帧,总位数是:

1(SOF)+ 12(仲裁场)+ 6(控制场)+ 64(数据场)+ 16(CRC场)+ 2(ACK场)+ 7(EOF)= 108位

加上3位帧间隔(IFS),实际占用的总线时间是111个位时间。如果有位填充规则的影响,最坏情况下还会增加更多位。这个数字为什么重要?因为计算总线负载率、评估报文周期是否合理的时候都要用到它。

2.2 仲裁场:总线上“谁先说话”由ID决定

仲裁场是很多人理解CAN协议的难点,也是重点。CAN总线为什么能实现多主通信?为什么两个节点同时发报文不会冲突?靠的就是仲裁机制。

CAN总线上的物理电平有两种:显性(Dominant)和隐性(Recessive)。显性电平代表逻辑“0”,隐性电平代表逻辑“1”。总线在没有节点发送时,处于隐性状态;任意一个节点拉出显性电平,总线就是显性。

仲裁的原理特别直观:多个节点同时发送时,从ID的最高位开始逐位比较,如果某个节点发送隐性位“1”,而另一个节点发送显性位“0”,总线电平是显性的,发送隐性位的节点会发现自己发送失败,自动退出,转为接收状态

这个机制在物理上是怎么实现的?控制器发送位时,回读总线电平。节点想发“1”时,它会把输出级切成高阻态,让总线自己被上拉电阻拉到隐性;而想发“0”的节点直接拉低总线,电平就是显性。因此当“0”和“1”同时出现时,总线实际呈现“0”,优先级更高。也就是说,ID值越小,优先级越高。

这就是为什么整车网络里,安全气囊、制动这类高实时性报文,ID通常设得很小;而车窗、空调这种舒适性报文,ID会分配得比较大。如果你在项目里发现某个报文总是发不出去,先别急着怀疑硬件,用总线分析仪看一看它的ID是不是太低了——不对,是太“高”了,被别的报文一直仲裁输掉。

2.3 控制场与数据场:DLC决定实际数据长度

控制场一共6位,标准帧由IDE位、r0位和4位DLC组成。DLC全称Data Length Code,占4位,取值范围是0到8。二进制0000到1000,分别对应0到8字节。

这里有一个很多新手会踩的坑:DLC超过8,在经典CAN里是非法的。虽然DLC是4位,理论上能表示0到15,但经典CAN的最大数据长度是8字节,所以超过8的值会被当作8处理,或者直接被某些协议栈判定为帧格式错误。你在用CANalyzer或者PCAN Viewer看报文的时候,如果看到DLC大于8且没有CAN FD标志,那大概率是你的解析工具配置错了。

数据场就是真正要传输的用户数据,字节数由DLC控制。这里要注意一个通信矩阵里的高频概念:字节序(Byte Order)。汽车上常用的信号定义有两种:Intel格式(小端)和Motorola格式(大端)。同一个车速信号,如果发送方用Intel格式打包,接收方用Motorola格式解析,那看到的数值会完全不一样。后面我会专门开一节讲字节序踩坑,这里先记住控制场里有个DLC决定长度,就够了。

2.4 CRC场与ACK场:这两段保证了CAN的可靠性

CRC场由15位CRC校验值和1位CRC界定符构成。CRC的生成多项式在ISO 11898-1里有明确规定,它覆盖SOF、仲裁场、控制场和数据场的所有位。接收节点收到一帧后,会重新计算CRC,如果和发送节点发来的CRC值不一致,就认为这一帧出错,不进入接收邮箱。

ACK场是我觉得CAN协议里最有意思的一段:发送节点在ACK槽发送隐性位“1”,而所有成功收到并校验通过CRC的接收节点,会在这段时间内主动拉低总线,发送一个显性位“0”。发送节点回读总线后看到电平是显性的,就知道“至少有一个节点正确收到了我这帧数据”。

这个机制的巧妙之处在于,它根本不需要知道总线上一共挂了多少个节点,也不需要接收方发什么复杂的应答帧,只要有一个接收节点确认,“线与”操作就自动完成确认。反之,如果总线上只有发送节点自己,没有其他节点接收,ACK槽就会保持隐性,发送节点报ACK错误。

做单节点自测的时候,这一点特别坑。很多人自己搭了一个CAN节点,用USB-CAN盒子挂在电脑上调试,发一帧数据,工具显示“发送失败”或者“ACK错误”,第一反应是电路坏了,其实只是因为总线上只有这一个节点,没人给它确认。解决方法是把接收端的120欧终端电阻当成最低配置,另外再加一个节点,哪怕用另一个USB-CAN工具把接收打开,也能完成ACK。

2.5 位填充机制:为什么数据流中不该连续出现5个相同位

除了上面那些显式定义的段,CAN协议还有一个隐式规则:位填充(Bit Stuffing)。发送节点在发送过程中,如果发现连续输出了5个相同电平的位,就必须在第5个位之后自动插入1个反相电平的位。接收节点收到后,会把这个多余的位去除。

为什么要这么设计?因为CAN节点靠电平跳变来同步时钟。如果总线上一段时间内一直是同一个电平,没有跳变,接收节点的采样点就会慢慢漂移,时间一长就可能采错位。通过位填充,总线电平最长每5个位就会发生一次跳变,时钟同步就不容易失准。

位填充规则会显著影响帧的“实际长度”。之前说标准帧8字节数据是108位,这只是不含位填充的理想长度。最坏情况下,每5个相同位插入1个位,数据段和CRC段最多可能增加接近20%的额外位。比如一个DLC=8的标准帧,实测最长可能需要130位左右。所以在计算带宽负载的时候,不要只看理想的108位,最好预留15%左右的余量。很多人在设计报文矩阵时把总线负载率算到95%,结果一上线就疯狂出错,就是没算位填充和帧间隔的这10%到20%的开销。

3. 实操:抓一帧报文,手把手把它拆干净

光看理论还是不够,我来带你走一遍实际的抓包解析过程。我用的是常见的USB-CAN分析工具和CANoe,这个思路换到其他工具也完全适用。

3.1 搭建极简抓包环境

搭建一个能跑通CAN通信的最小环境,最少需要以下东西:

  • 两个CAN节点(可以是两块STM32开发板,也可以是STB-CAN分析仪加一个普通节点)
  • 两端各接一个120欧终端电阻,并联在CANH和CANL之间
  • 一根双绞线,CANH接CANH,CANL接CANL

这里必须强调终端电阻的重要性。CAN总线两端各需120欧,目的是匹配线束阻抗,吸收信号反射。如果电阻没接或者接错位置,总线上可能出现严重的信号振铃,导致通信不稳定。你在台上调试的时候看波形像毛刺一样乱跳,十有八九就是终端电阻的问题。

接好线后,在PC上打开抓包工具,配置好通道和波特率(常见的有125kbps、250kbps、500kbps、1Mbps)。注意总线上所有节点的波特率必须一致,否则节点之间会互相报错,没有任何报文能正常通信。

3.2 用工具抓到一帧报文,对照帧结构逐位分析

假设我们抓到了一帧标准帧,ID是0x123,DLC是8,数据是11 22 33 44 55 66 77 88。在PCAN Viewer这类工具里,它显示得非常简洁:

0x123 8 11 22 33 44 55 66 77 88

很多人看到这行数据就以为自己“会解析了”,其实真要分析底层帧结构,还得把它展开成二进制:ID=0x123,二进制是001 0010 0011,共11位。

  • SOF:一个显性位0,总线从隐性跳变到显性,这就是所有节点检测到“帧开始”的信号。
  • ID:0x123。11位逐位发送,从最高位开始。
  • RTR位:对于数据帧,RTR=0(显性);如果是远程帧,RTR=1(隐性)。远程帧没有数据场,它的作用只是请求某个ID的节点发送数据。实际工程中远程帧用得极少,很多初学者会在这里绕半天,我的建议是先了解概念,不必深挖。
  • IDE位:标准帧里IDE=0;扩展帧里IDE=1。
  • DLC:8,二进制1000。
  • 数据场:8个字节,按顺序发送。
  • CRC:由硬件自动计算并发送,不需要用户干预。
  • ACK:接收节点自动应答。
  • EOF:7位隐性1。

如果你用手头的逻辑分析仪抓原始波形,用硬件协议分析功能打开CAN解码,可以看到波形上被标注了每一段的含义,这样看会直观很多。

3.3 用C++手写一个最小CAN帧解析器

工具抓包能看懂,还要能自己写代码解析。我在这里给一个精简的C++实现思路,适用于把USB-CAN设备裸数据(只含ID、DLC、Data的帧数据)里的ID和字节序信息还原出来的场景。

#include <cstdint> #include <cstdio> #include <cstring> struct CanFrame { uint32_t id; // 实际ID值 uint8_t is_ext; // 1=扩展帧 0=标准帧 uint8_t dlc; // 数据长度 0-8 uint8_t data[8]; // 数据场 }; // 假设从CAN控制器接收寄存器中拿到的原始值是32位ID寄存器 CanFrame DecodeCanFrame(uint32_t id_reg, uint8_t flags, const uint8_t* raw_data, uint8_t data_len) { CanFrame frame; memset(&frame, 0, sizeof(frame)); // bit31表示扩展帧标志,bit30表示远程帧标志(这里不处理远程帧) frame.is_ext = (flags & 0x80) ? 1 : 0; if (frame.is_ext) { // 扩展帧:29位ID直接存在低29位 frame.id = id_reg & 0x1FFFFFFF; } else { // 标准帧:ID存在低11位 frame.id = id_reg & 0x7FF; } frame.dlc = data_len > 8 ? 8 : data_len; memcpy(frame.data, raw_data, frame.dlc); return frame; } // 解析一个小端(Intel)格式的16位信号 // 比如车速信号定义:起始位=8,长度=16 uint16_t ParseIntel16Bit(const uint8_t* data, uint8_t start_byte, uint8_t start_bit) { uint16_t value = 0; // 先把整段数据按小端合并成64位,再做位提取 // 简单做法:直接按字节移位 value = (data[start_byte] >> start_bit) | (data[start_byte + 1] << (8 - start_bit)); return value; } int main() { // 模拟收到的原始帧 uint8_t data[8] = {0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88}; CanFrame frame = DecodeCanFrame(0x123, 0x00, data, 8); printf("ID=0x%X DLC=%d\n", frame.id, frame.dlc); printf("Data:"); for (int i = 0; i < frame.dlc; i++) { printf(" %02X", frame.data[i]); } printf("\n"); return 0; }

这个代码只是一个演示骨架,真实项目中你还要处理帧过滤、时间戳、错误帧标识。C++做CAN解析的常见场景是上位机通过USB-CAN设备读数据,一般设备厂商都会提供SDK,你只需要把SDK回调里的数据转换成上面这个CanFrame结构就行。

3.4 波特率参数的计算逻辑

配置CAN控制器时,波特率不是随便填一个数就能用的,它和时钟分频、同步段(SyncSeg)、传播时间段(PropSeg)、相位缓冲段1(PS1)、相位缓冲段2(PS2)有关系。

以STM32F103的bxCAN为例,它挂载在APB1总线上,APB1时钟典型配置是36MHz。CAN控制器的每个位时间被分成若干时间份(Time Quantum,TQ),波特率公式是:

波特率 = 时钟频率 / (预分频值 * 每个位占用的TQ数)

其中每个位占用的TQ数 = 1(SyncSeg)+ 传播时间段 + PS1 + PS2,通常配置成8到25个TQ。举例:如果预分频值BRP=4,位时间配置为1+9+8=18个TQ?这里要特别注意,很多STM32参考代码里写的是BS1=9,BS2=8,也就是总共1+9+8=18个TQ,波特率=36MHz / (4 * 18) = 500kHz。

如果你想配1Mbps,可以适当缩小TQ数,比如BRP=4,BS1=6,BS2=1?严谨的话是1+6+1=8个TQ,波特率=36M / (4 * 8) = 1.125Mbps,不对,这样配不出来。所以实际工程里经常用BRP=2,TQ总数=18,得到1MHz。

这里不展开具体寄存器配置,但你需要理解一个原则:采样点位置一般推荐放在75%到85%之间,也就是把PS1配置得比PS2长一些。采样点太靠前,抗干扰能力差;太靠后,波特率误差稍大就容易采错位。整车常用500kbps + 80%采样点的组合,工业现场总线则常用250kbps。

4. 常见问题与排查技巧实录

这部分是我在项目里踩过、帮别人优化过、也经常在技术社区看到的问题清单,整理成速查表形式,方便大家遇到问题时直接对照。

4.1 问题速查表

现象可能原因排查思路
总线上完全没有任何报文终端电阻缺失/接错、波特率不一致、CANH/CANL接反先确认终端电阻,再用示波器抓CANH与CANL差分波形
能发出去但抓包工具看不到工具通道配置错误、报文被硬件过滤器拦截检查工具的验收过滤器和屏蔽寄存器,先全通测试
长时间通信后偶发丢帧波特率长期漂移、总线负载过高、线束过长导致反射用总线分析仪统计错误帧,重点看CRC错误数量
单节点发送报ACK错误总线上只有发节点,无接收节点增加一个接收节点,或临时用另一个工具监听
两个节点互相通信正常,三个节点以上出现异常终端电阻位置不对,线缆分支不合理把120欧电阻放在总线物理两端,中间节点用短支线接入
数据解析出来的数值明显偏大/偏小字节序定义不一致(Intel/Motorola)核对通信矩阵里的起始位和字节顺序定义

4.2 字节序的大坑:Intel和Motorola格式

这两个词在CAN通信矩阵里出现频率极高,也是整车厂和零部件供应商之间最容易扯皮的地方。

简单讲,Intel格式就是小端模式,低字节在低地址、低编号位在前;Motorola格式就是大端模式,高字节在低地址。对于一个跨字节的16位信号,比如0x1234:

  • 用Intel格式发送:数据场第一个字节是0x34,第二个字节是0x12;
  • 用Motorola格式发送:数据场第一个字节是0x12,第二个字节是0x34。

如果你的上位机解析工具默认按Intel格式解析,但ECU按Motorola格式打包,那么看到的数据就会是0x3412,数值完全错乱。遇到这种问题,不能只盯着代码看,先回去核对通信矩阵的“Byte Order”一栏。再强调一下,很多国际大厂的诊断报文也是Motorola字节序,踩坑概率极高。

4.3 位填充和误差帧:为什么波形会变长

前面提到,位填充会导致一帧报文的实际位长不确定。在调试现场,如果示波器测出某个ID的报文周期忽长忽短,先不要怀疑CPU主频不稳,优先级更高的是去检查数据场里是不是连续出现了大量的0x00或0xFF。这种极端数据会触发连续的位填充,把帧长拉长;而如果数据是交替的0x55、0xAA,位填充就少很多。

还有一种情况,数据段连续出现5个相同位时,位填充机制会强制插入反相位,但如果这时候总线上存在干扰,填充位被干扰吞掉,接收节点就会检测到位填充错误,从而触发错误帧。错误帧的优先级比数据帧高,错误节点一旦报错,会立刻打断当前总线通信。所以在电磁环境恶劣的场合,除了软件重试机制,更需要关注线束双绞质量和屏蔽层接地。

4.4 CAN FD的过渡期踩坑

最后聊一个CAN FD相关的新坑。CAN FD向下兼容经典帧,所以很多开发者在同一个网络上同时允许经典CAN节点和CAN FD节点。但这里有一个隐藏的矛盾:经典CAN节点收到CAN FD帧时,会把它判定为格式错误并发出错误帧。因为CAN FD在控制场里用了FDF位来标志自己是FD帧,老节点的控制器不认这个位,它按经典CAN的格式去解析,结果就乱了。

所以如果你在一个混合网络里挂老节点,必须在报文的发送策略上做限制,不能直接让所有节点都开始发CAN FD帧。更稳妥的做法是:同一条物理总线上的设备,要么全部支持CAN FD,要么在配置工具里把每条报文的“FD使能”关掉,保持经典帧通信。

最后再分享一点我的体会

CAN协议这个东西,看起来是个老掉牙的通信标准,但直到今天仍然是汽车电子和工业控制里最可靠的骨干网络之一。我从最早用MCP2515实现SPI转CAN,到后来调STM32的bxCAN寄存器,再到现在用CAN FD做域控通信,每一次对帧结构的理解加深,都能在调试速度上来一次明显提升。有一句话可能有点夸但很真实:你把CAN数据帧的每一位都吃透了,再去学CANopen、J1939、UDS这些上层协议,会发现它们都建立在同一个基础上,学起来快得多。希望这篇文章能帮你省掉一些我在总线上熬过的夜。

如果你在实际操作中也遇到过什么CAN协议的怪问题,欢迎在评论区留言,我们一起讨论排查思路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询