
很多人一开始接触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最高5MbpsISO 11898-1:2015ID类型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用更细的位来数一遍起始位SOF1位显性“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界定符隐性位。EOF7位连续的隐性位“1”表示帧结束。算一下最常用的8字节数据帧比如标准帧总位数是1SOF 12仲裁场 6控制场 64数据场 16CRC场 2ACK场 7EOF 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%的额外位。比如一个DLC8的标准帧实测最长可能需要130位左右。所以在计算带宽负载的时候不要只看理想的108位最好预留15%左右的余量。很多人在设计报文矩阵时把总线负载率算到95%结果一上线就疯狂出错就是没算位填充和帧间隔的这10%到20%的开销。3. 实操抓一帧报文手把手把它拆干净光看理论还是不够我来带你走一遍实际的抓包解析过程。我用的是常见的USB-CAN分析工具和CANoe这个思路换到其他工具也完全适用。3.1 搭建极简抓包环境搭建一个能跑通CAN通信的最小环境最少需要以下东西两个CAN节点可以是两块STM32开发板也可以是STB-CAN分析仪加一个普通节点两端各接一个120欧终端电阻并联在CANH和CANL之间一根双绞线CANH接CANHCANL接CANL这里必须强调终端电阻的重要性。CAN总线两端各需120欧目的是匹配线束阻抗吸收信号反射。如果电阻没接或者接错位置总线上可能出现严重的信号振铃导致通信不稳定。你在台上调试的时候看波形像毛刺一样乱跳十有八九就是终端电阻的问题。接好线后在PC上打开抓包工具配置好通道和波特率常见的有125kbps、250kbps、500kbps、1Mbps。注意总线上所有节点的波特率必须一致否则节点之间会互相报错没有任何报文能正常通信。3.2 用工具抓到一帧报文对照帧结构逐位分析假设我们抓到了一帧标准帧ID是0x123DLC是8数据是11 22 33 44 55 66 77 88。在PCAN Viewer这类工具里它显示得非常简洁0x123 8 11 22 33 44 55 66 77 88很多人看到这行数据就以为自己“会解析了”其实真要分析底层帧结构还得把它展开成二进制ID0x123二进制是001 0010 0011共11位。SOF一个显性位0总线从隐性跳变到显性这就是所有节点检测到“帧开始”的信号。ID0x123。11位逐位发送从最高位开始。RTR位对于数据帧RTR0显性如果是远程帧RTR1隐性。远程帧没有数据场它的作用只是请求某个ID的节点发送数据。实际工程中远程帧用得极少很多初学者会在这里绕半天我的建议是先了解概念不必深挖。IDE位标准帧里IDE0扩展帧里IDE1。DLC8二进制1000。数据场8个字节按顺序发送。CRC由硬件自动计算并发送不需要用户干预。ACK接收节点自动应答。EOF7位隐性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(ID0x%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、相位缓冲段1PS1、相位缓冲段2PS2有关系。以STM32F103的bxCAN为例它挂载在APB1总线上APB1时钟典型配置是36MHz。CAN控制器的每个位时间被分成若干时间份Time QuantumTQ波特率公式是波特率 时钟频率 / (预分频值 * 每个位占用的TQ数)其中每个位占用的TQ数 1SyncSeg 传播时间段 PS1 PS2通常配置成8到25个TQ。举例如果预分频值BRP4位时间配置为19818个TQ这里要特别注意很多STM32参考代码里写的是BS19BS28也就是总共19818个TQ波特率36MHz / (4 * 18) 500kHz。如果你想配1Mbps可以适当缩小TQ数比如BRP4BS16BS21严谨的话是1618个TQ波特率36M / (4 * 8) 1.125Mbps不对这样配不出来。所以实际工程里经常用BRP2TQ总数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协议的怪问题欢迎在评论区留言我们一起讨论排查思路。