1. 为什么CAN协议值得你花时间搞明白如果你拆过任何一辆2010年以后出厂的车哪怕只是换个车机或者加装个倒车雷达你大概率会碰到两根绞在一起、颜色通常是黄绿或者橙黑的细线。修车师傅管它叫“CAN线”网上搜出来的资料动不动就是“控制器局域网”“差分信号”“非破坏性仲裁”这些词看着就头大。但说实话CAN协议本身并不复杂它之所以让人觉得难是因为大多数教程一上来就丢帧结构图却不告诉你这些位到底是干什么用的、为什么非得这么设计。我自己第一次接触CAN是在做车载数据采集的时候当时需要从OBD口读车速和转速。一开始以为跟串口差不多接上TX/RX就能收数据结果折腾了一整天才发现CAN根本就不是点对点的通信方式它是一条总线上面挂着一堆节点谁想说话得先抢总线使用权。这个“抢”的机制就是CAN协议最核心的设计之一。这篇文章我打算把CAN协议的种类和CAN数据帧这两块讲透。CAN协议从诞生到现在主要分经典CAN和CAN FD两个大类前者又分标准帧和扩展帧后者在数据段做了提速和扩容。CAN数据帧则是总线上真正跑的东西你得知道每一段是干什么的才能做CAN协议报文解析才能用C CAN协议库去收发数据才能判断CAN协议终端电阻到底该不该加、加在哪儿。适合谁看如果你是嵌入式新手正在调STM32的CAN外设如果你是上位机开发要用C写CAN通信程序如果你是汽车电子测试工程师天天跟CANoe和报文打交道——这篇文章里的内容你都能直接用上。我会从协议种类讲到帧格式再讲到实际抓包解析中间穿插我自己踩过的坑和总结出来的排查技巧。2. CAN协议的两大分支经典CAN与CAN FD到底怎么选2.1 经典CAN的两种帧格式标准帧和扩展帧经典CAN协议最早由博世在1986年发布后来成了ISO 11898标准。它最核心的特点是多主架构、基于报文ID的优先级仲裁、差分信号传输。所谓多主就是总线上没有主从之分每个节点都可以在总线空闲时尝试发送数据。但如果两个节点同时开始发就得靠ID来决定谁先发——ID数值越小优先级越高。这个机制叫“非破坏性仲裁”因为输掉的那个节点不会丢失数据它会自动退让等下一次总线空闲再重发。经典CAN有两种帧格式标准帧和扩展帧。标准帧的ID是11位扩展帧的ID是29位。11位ID能表示2048个不同的报文对于大多数车身控制来说够用了比如车门、车灯、座椅这些。但随着车载功能越来越多11位不够分了就引入了29位扩展帧。扩展帧并不是简单地把ID加长它的帧结构里多了一个SRR位和一个IDE位用来区分这是标准帧还是扩展帧。实际项目中怎么选我的经验是如果现有网络里全是标准帧你就别没事找事上扩展帧。因为标准帧和扩展帧可以在同一条总线上共存但优先级仲裁的时候标准帧的RTR位和扩展帧的SRR位会对齐比较标准帧天然占优。如果你混用扩展帧的实时性会受影响。另外很多低端MCU的CAN控制器对扩展帧的过滤表支持有限用之前先查手册。2.2 CAN FD数据段提速与扩容的代价CAN FD是2012年左右博世推出的升级版FD是Flexible Data-rate的缩写。它解决的是经典CAN的两个痛点一是数据场最多8字节传个长一点的诊断报文就得拆成多帧二是仲裁段和数据段速率一样导致总线负载一高实时性就崩。CAN FD的做法是仲裁段仍然用经典CAN的速率通常500kbps但数据段可以切换到更高的速率2Mbps、5Mbps甚至8Mbps。同时数据场从8字节扩展到64字节。这意味着传同样多的数据总线占用时间大幅缩短。但CAN FD不是白给的。首先你的收发器得支持FD普通CAN收发器在数据段高速率下波形会烂掉。其次终端电阻的匹配更敏感因为速率高了信号反射的影响更大。第三CAN FD的帧格式里多了FDF、BRS、ESI这几个位解析的时候不能按经典CAN的格式硬套。我个人的建议是新项目如果带宽需求大直接上CAN FD如果是维护老平台或者成本极度敏感经典CAN还能再战十年。别为了追新而追新CAN FD的调试工具和IP授权费都比经典CAN贵不少。2.3 一张表看清经典CAN与CAN FD的核心差异特性经典CANCAN FD最大数据场8字节64字节仲裁段速率最高1Mbps最高1Mbps数据段速率与仲裁段相同可切换至2/5/8Mbps帧格式标准帧/扩展帧标准帧/扩展帧均支持CRC校验15位17位或21位终端电阻120欧姆120欧姆但匹配要求更严典型应用车身控制、OBD域控制器、ADAS、电池管理这张表里最容易被忽略的是CRC校验位数。经典CAN的CRC是15位CAN FD因为数据长了CRC加强到17位数据≤16字节或21位数据16字节。这意味着你用经典CAN的解析代码去解CAN FD帧CRC校验那一步就会对不上必须换库或者自己改。3. CAN数据帧的每一段到底在干什么3.1 帧起始到仲裁段总线上谁说了算CAN数据帧的起始是SOFStart of Frame一个显性位逻辑0。总线空闲时是隐性逻辑1第一个显性位出现就代表有人开始发了其他节点必须同步。紧接着是仲裁段标准帧是11位IDRTR位扩展帧是11位基本IDSRR位IDE位18位扩展IDRTR位。仲裁的原理是每个节点一边发ID一边读总线如果自己发的是隐性但读回来是显性说明有更高优先级的节点在发自己立刻退出。这个过程在ID发完之前就结束了所以叫“非破坏性”。这里有个实操细节RTR位用来区分数据帧和远程帧。数据帧的RTR是显性远程帧的RTR是隐性。远程帧就是“我要这个ID的数据谁有谁发”现在用得很少了因为容易造成总线拥堵。我调试的时候基本没见过远程帧但解析代码里得留着这个判断不然遇到老设备发远程帧会解析错。3.2 控制段DLC不是随便填的控制段里最重要的是DLCData Length Code4个位表示数据场有多少字节。经典CAN里DLC从0到8对应0到8字节。但注意DLC9到15在经典CAN里是无效的有些控制器会把它当8处理有些会报错。CAN FD里DLC的含义变了0到8还是对应0到8字节但9到15分别对应12、16、20、24、32、48、64字节。这个映射关系必须记牢不然解析CAN FD报文时数据长度会算错。我见过有人用经典CAN的库去解CAN FDDLC9的时候只取了8字节后面的数据全丢了。控制段里还有IDE位区分标准帧和扩展帧和FDF位区分经典CAN和CAN FD以及BRS位Bit Rate Switch表示数据段是否切换速率。这些位在解析的时候都要判断不能跳过。3.3 数据段与CRC段真正承载信息的部分数据段就是你要传的原始字节0到8字节经典CAN或0到64字节CAN FD。字节序是大端也就是高位在前。比如你要传一个16位的车速值0x1234在总线上就是先发0x12再发0x34。但很多应用层协议会自己定义字节序所以解析的时候得看DBC文件或者通信矩阵。CRC段是发送节点根据前面所有位算出来的校验值接收节点会重新算一遍如果不一致就认为帧出错了会发错误帧。CRC的计算多项式在经典CAN里是固定的CAN FD里根据数据长度有两个多项式。你自己写解析代码的时候如果只是读数据可以跳过CRC校验但如果要做错误统计或者总线质量分析CRC必须算。3.4 ACK段与帧结束确认机制不是你想的那样ACK段有两个位ACK Slot和ACK Delimiter。发送节点在ACK Slot发隐性任何正确接收的节点都会把它拉成显性。所以发送节点只要读到显性就知道至少有一个节点收到了。注意ACK不代表数据被正确处理了只代表有人收到了。应用层的确认得自己在上层协议里做。帧结束是7个隐性位标志着这一帧结束。如果这7个位里出现了显性位会被认为是格式错误节点会发错误帧。4. 用C做CAN报文解析的完整实操4.1 环境准备与SocketCAN基础在Linux下做CAN通信最方便的是SocketCAN。它把CAN设备当成网络设备来管理你可以用ip link命令配置用socket API收发。Windows下可以用PCAN或者Kvaser的SDK但SocketCAN的代码更通用我以它为例。先看硬件连接CAN_H接CAN_HCAN_L接CAN_L终端电阻接在总线两端各120欧姆。如果你只有两个节点两端各一个电阻总线上就是60欧姆。用万用表量CAN_H和CAN_L之间的电阻应该是60欧姆左右。如果量出来是120欧姆说明只接了一个电阻如果是40欧姆说明接了三个。终端电阻的作用是消除信号反射速率越高越重要。500kbps的时候不接电阻可能也能凑合但1Mbps以上不接就会大量出错。配置CAN接口的命令sudo ip link set can0 type can bitrate 500000 sudo ip link set can0 up如果是CAN FD还要加dbitratesudo ip link set can0 type can bitrate 500000 dbitrate 2000000 fd on4.2 用C收发CAN帧的核心代码下面是一个最小的CAN收发示例用SocketCAN的raw socket#include linux/can.h #include linux/can/raw.h #include sys/socket.h #include sys/ioctl.h #include net/if.h #include cstring #include cstdio #include unistd.h int main() { int s socket(PF_CAN, SOCK_RAW, CAN_RAW); struct ifreq ifr; strcpy(ifr.ifr_name, can0); ioctl(s, SIOCGIFINDEX, ifr); struct sockaddr_can addr; addr.can_family AF_CAN; addr.can_ifindex ifr.ifr_ifindex; bind(s, (struct sockaddr*)addr, sizeof(addr)); struct can_frame frame; frame.can_id 0x123; frame.can_dlc 8; for (int i 0; i 8; i) frame.data[i] i; write(s, frame, sizeof(frame)); struct can_frame rx; int nbytes read(s, rx, sizeof(rx)); if (nbytes 0) { printf(ID: 0x%X DLC: %d Data: , rx.can_id, rx.can_dlc); for (int i 0; i rx.can_dlc; i) printf(%02X , rx.data[i]); printf(\n); } close(s); return 0; }这段代码里can_id如果要用扩展帧得或上CAN_EFF_FLAG。如果要发CAN FD帧得用canfd_frame结构体并且设置CAN_RAW_FD_FRAMES选项。很多人第一次用CAN FD的时候忘了设这个选项结果发出去的帧被当成经典CAN处理数据段被截断。4.3 解析一个真实的车速报文假设我从总线上抓到一个ID为0x1A0的标准帧DLC8数据是00 00 1A 2B 00 00 00 00。根据DBC文件车速在字节2和字节3分辨率0.01偏移0。那么车速就是0x1A2B * 0.01 67.63 km/h。解析的时候要注意字节2是高位字节3是低位所以是(data[2] 8) | data[3]。如果DBC里定义的是小端那就反过来。我见过有人把大小端搞反车速算出来是几万还以为是传感器坏了。对于CAN FD的长数据解析逻辑一样只是数据长度从8变成最多64。但要注意CAN FD的DLC到字节数的映射不是线性的得查表DLC经典CAN字节数CAN FD字节数0-80-80-89无效1210无效1611无效2012无效2413无效3214无效4815无效64这个表我建议直接抄进代码里别自己算。5. 调试CAN总线时最常见的五个坑5.1 终端电阻到底该加在哪儿CAN协议终端电阻是新手最容易搞错的地方。规则很简单总线的两个物理端点各加一个120欧姆电阻。如果你的总线是手拉手连的电阻加在最远的两端。如果节点是星型连接的那终端电阻的匹配就很麻烦通常需要在每个分支上加但效果不如手拉手。我见过有人在每个节点上都焊了一个120欧姆电阻结果总线上并了五六个电阻变成20多欧姆CAN收发器直接驱动不动通信全挂。也见过一个电阻都不加的短距离低速还能跑一上1Mbps就疯狂报错。用万用表测断电情况下CAN_H和CAN_L之间的电阻应该是60欧姆左右。如果偏差超过10%就得检查电阻数量和阻值。5.2 采样点设置不对导致随机错误CAN的位时间分成四段同步段、传播段、相位缓冲段1、相位缓冲段2。采样点就在相位缓冲段1和2之间。采样点通常设在75%到80%的位置。如果设得太靠前信号还没稳定就采样容易出错设得太靠后同步能力变差。用ip link配置的时候可以用sample-point参数指定sudo ip link set can0 type can bitrate 500000 sample-point 0.875如果总线上有多个节点所有节点的采样点最好一致不然同步会出问题。我调试的时候遇到过两个节点采样点差了10%低速没事一上500kbps就随机丢帧查了半天才发现是采样点不匹配。5.3 总线负载率超过70%就该警惕了总线负载率是衡量CAN网络健康度的重要指标。计算方法是单位时间内总线上传输的位数除以总线总带宽。比如500kbps的总线一秒钟能传500000位如果实际传了400000位负载率就是80%。负载率超过70%的时候低优先级报文的延迟会明显增大实时性没法保证。超过90%基本就快崩了。我一般会在设计阶段就算好负载率留30%的余量。如果实测发现负载率偏高要么提高波特率要么把一些非实时报文挪到另一条总线上。5.4 错误帧频繁出现怎么排查CAN节点有错误计数器发送错误超过255会进入Bus Off状态也就是主动断开总线。如果你发现节点频繁Bus Off先查硬件终端电阻对不对、线缆有没有断、CAN_H和CAN_L有没有接反。硬件没问题再查软件波特率对不对、采样点一不一致、有没有节点在发错误帧。用candump命令可以实时看总线上的报文candump can0如果看到大量ERRORFRAME说明总线上有错误。可以用ip -details -statistics link show can0看错误计数。5.5 CAN FD和经典CAN混用时的注意事项CAN FD节点和经典CAN节点可以在同一条总线上共存但有个前提经典CAN节点必须能容忍CAN FD帧。因为CAN FD帧在仲裁段之后会切换速率经典CAN节点如果收到CAN FD帧会认为CRC错误然后发错误帧把CAN FD帧打断。解决办法是要么所有节点都支持CAN FD要么用CAN FD的“无速率切换”模式BRS0这样数据段速率和仲裁段一样经典CAN节点虽然不认识FDF位但至少不会报错。不过这样CAN FD的优势就没了。我个人的做法是新老混用的网络干脆把CAN FD节点配置成经典CAN模式等老节点淘汰了再开FD。别为了省事搞混合模式调试起来能烦死人。6. 从抓包到解析一个完整的CAN报文分析实例6.1 抓包工具的选择与配置Linux下用candump最方便Windows下可以用CANoe、PCAN-View或者BUSMASTER。我平时用candump加canplayer做回放用cansniffer做报文变化监测。抓包的时候要注意先确认波特率。如果波特率不对抓出来的全是乱码或者错误帧。如果不确定波特率可以用canbusload或者示波器先测一下位时间。抓到的日志格式是这样的(1234567890.123456) can0 1A0#00001A2B00000000括号里是时间戳can0是接口名1A0是ID#后面是数据。如果是扩展帧ID是8位十六进制如果是CAN FD数据长度可能超过8字节。6.2 手动解析一个多字节信号假设DBC里定义了一个电池电压信号起始位是8长度16位字节序是大端分辨率0.1偏移0。抓到的数据是00 00 0C 1A 00 00 00 00。起始位8意味着从字节1开始字节0是第0到7位。长度16位所以取字节1和字节20x0C1A。分辨率0.1所以电压是0x0C1A * 0.1 309.8V。如果起始位不是字节对齐的比如起始位是4长度12位那就得做位操作。这种时候我建议用现成的DBC解析库比如canmatrix或者libcanard自己写位操作容易出错。6.3 用Python快速验证解析结果虽然标题里提到C但做快速验证的时候Python更方便。用python-can库可以几行代码读CANimport can bus can.interface.Bus(channelcan0, bustypesocketcan) for msg in bus: if msg.arbitration_id 0x1A0: speed (msg.data[2] 8 | msg.data[3]) * 0.01 print(fSpeed: {speed} km/h)验证通过之后再把逻辑移植到C里。这样比直接在C里调试快得多。7. 几个容易被忽略但很重要的细节7.1 报文ID的优先级不是随便定的CAN仲裁的时候ID越小优先级越高。所以安全相关的报文比如刹车、气囊ID要设小舒适性相关的比如空调、音响ID可以设大。我见过一个项目把车窗控制的ID设成0x000结果刹车报文抢不过它这是要出人命的。另外标准帧和扩展帧混用的时候标准帧的优先级天然高于扩展帧因为标准帧的RTR位和扩展帧的SRR位比较时标准帧发的是显性。所以如果你有一个扩展帧的紧急报文最好把它改成标准帧或者确保总线上没有同优先级的标准帧。7.2 数据场的无效字节要填默认值DLC8但实际只用了4个字节的时候剩下的4个字节填什么填0或者填0xFF都行但整个网络要统一。我见过有的节点填0有的填0xFF结果接收方做校验的时候对不上。如果DBC里定义了无效值就按DBC填没定义的话我一般填0。7.3 Bus Off恢复策略要设计好节点进入Bus Off之后什么时候恢复CAN标准里建议是检测到128次连续11个隐性位后可以恢复。但实际项目中我一般会让节点先等一段时间比如100ms再尝试恢复。如果短时间内频繁Bus Off说明总线有严重问题这时候不停重试反而会加重总线负担。用SocketCAN的时候可以设置restart-ms参数sudo ip link set can0 type can restart-ms 100这样Bus Off之后100ms自动恢复。7.4 时间戳的精度影响后续分析做总线分析的时候时间戳很重要。比如你要算两个报文之间的间隔或者做信号同步。SocketCAN的时间戳精度是微秒级够用了。但如果用USB转CAN盒子时间戳精度可能只有毫秒级而且会有累积误差。做高精度分析的时候我建议用带硬件时间戳的CAN卡。7.5 别在中断里做耗时操作如果你在MCU上写CAN接收中断记住中断里只做数据拷贝解析和业务逻辑放到主循环或者任务里。CAN的接收中断频率可能很高如果在中断里做浮点运算或者查表会阻塞其他中断导致丢帧。我一般会在中断里把数据丢进环形缓冲区主循环再慢慢处理。8. 关于CAN协议学习路径的一点个人建议如果你刚开始学CAN别一上来就啃ISO 11898标准文档那玩意儿是给芯片设计者看的。我的建议是先找个带CAN外设的开发板比如STM32或者ESP32把收发跑通。然后找个CAN分析仪抓一段真实的车载报文对着DBC文件手动解析几个信号。等你把解析流程走通了再回头看帧格式会发现那些位段设计得非常合理。C CAN协议编程这块SocketCAN的文档虽然简陋但示例代码足够用了。遇到问题多查linux/can.h头文件里面注释写得很清楚。CAN FD的代码和经典CAN差别不大主要是结构体换成canfd_frame然后注意DLC映射。最后说一个我自己的习惯每次调试CAN之前先用万用表量终端电阻再用示波器看波形。这两个步骤花不了五分钟但能省掉后面几个小时的瞎折腾。终端电阻不对后面所有软件调试都是白费劲。