
做测试的第三个月我被同事随口一问问住了“两条报文同时发到CAN总线上会撞吗”我当时支吾了半天说了些有优先级机制之类的片儿汤话。现在想想挺丢人的——仲裁是CAN总线最核心的设计吃这碗饭的人本该张口就来。今天就把这事儿一次讲透顺带附两段能直接跑的代码。先给结论不会撞但有输赢。输的那个自己闭嘴赢的那个一口气发完整个过程一帧报文都不浪费。先看物理层总线上只有两种状态仲裁的全部秘密藏在CAN的物理电平里。CAN总线只有两种状态。显性Dominant逻辑0CAN_H拉到3.5VCAN_L压到1.5V两线差分电压约2V。隐性Recessive逻辑1两根线都待在2.5V左右差分约0V。关键特性来了总线是线与逻辑。只要有一个节点在发显性总线就呈现显性只有所有节点都发隐性总线才是隐性。打个比方像开会举手表决有没有反对意见——只要有一个人举手结果就是有。显性就是那只举起的手隐性就是没举手。仲裁本质上就是一场用电压实现的举手比赛。动手跑一遍30行Python把仲裁打出来光看文字容易晕直接跑代码。下面这段纯Python零依赖模拟的就是节点A发0x18、节点B发0x19同时开口的全过程defarbitrate(id_a,id_b):模拟两个节点同时发送标准帧ID的仲裁过程。 返回 (赢家ID, 分出胜负的位序号)。11位ID高位先发。 bits_a[(id_ai)1foriinrange(10,-1,-1)]# 拆成11位高位在前bits_b[(id_bi)1foriinrange(10,-1,-1)]forpos,(ba,bb)inenumerate(zip(bits_a,bits_b)):busbabb# 线与只要一边发0显性总线就是0print(f第{pos1:2d}位: A发{ba}B发{bb}- 总线{bus},end)ifba!bus:print( - A回读不一致A闭嘴转接收)returnid_b,pos1ifbb!bus:print( - B回读不一致B闭嘴转接收)returnid_a,pos1print( 一致继续)returnid_a,None# ID相同会走到数据段撞车真实总线上不允许ID重复winner,bitarbitrate(0x18,0x19)print(f\n赢家: 0x{winner:02X}在第{bit}位分出胜负)跑出来的结果长这样复制粘贴就能跑建议亲手跑一遍第 1位: A发0 B发0 - 总线0 一致继续 第 2位: A发0 B发0 - 总线0 一致继续 第 3位: A发0 B发0 - 总线0 一致继续 第 4位: A发0 B发0 - 总线0 一致继续 第 5位: A发0 B发0 - 总线0 一致继续 第 6位: A发0 B发0 - 总线0 一致继续 第 7位: A发1 B发1 - 总线1 一致继续 第 8位: A发1 B发1 - 总线1 一致继续 第 9位: A发0 B发0 - 总线0 一致继续 第10位: A发0 B发0 - 总线0 一致继续 第11位: A发0 B发1 - 总线0 - B回读不一致B闭嘴转接收 赢家: 0x18在第11位分出胜负对照着看0x18的二进制是000000110000x19是00000011001。前10位两边发的完全一样总线回读也一致相安无事。第11位A发0显性B发1隐性总线被拉成显性0。B回读发现跟自己发的不一致——说明有个ID更小的家伙也在发立刻停止发送转为接收。A全程每一位跟总线回读都一致毫发无损一口气把整帧发完。记住这句就行ID越小优先级越高。不是谁抢得快是谁比得小。一条真实报文长什么样仲裁赢了之后赢家发出去的一帧在CANoe的Trace里长这样12:04:31.8821 Rx 0x18 8 02 00 6E 00 00 00 00 00逐个字段拆一下12:04:31.8821是时间戳Rx是接收方向0x18是仲裁赢下来的那个ID8是DLC数据长度8字节后面02 00 6E 00 00 00 00 00是8个数据字节的十六进制。注意这一行的含义这就是上面代码里赢家A发完的完整一帧——仲裁在第11位分出胜负之后A没有停顿、没有重发直接把数据段一口气发完。如果B0x19还有数据要发它会在Trace的下一行出现等前一帧发完重新参与下一轮仲裁。真实总线上你永远看不到两帧叠在一起。为什么这样设计输的不白等赢的零延迟这个设计妙在哪对比一下以太网就知道了。以太网用的是CSMA/CD撞上了双方都停随机退避一段时间再重发。延迟不确定撞得多了网络直接堵死。CAN用的是CSMA/CA在仲裁段就分出胜负赢家一气呵成输家安静等待。整个过程没有一次碰撞没有一帧重发赢家的延迟是确定的。这就是CAN能进汽车的原因。刹车信号、气囊信号这种报文发出去就不能等一会儿重试。确定性延迟是车载网络的命根子。我在这上面吃过一次亏。之前一个项目网关周期转发40多条报文总线负载常年65%以上。诊断仪发0x7DF请求服务偶尔超时一次两次还以为是诊断仪的问题。抓Trace细看才发现高负载时ID偏大的舒适类报文0x6xx段在仲裁里老是输给0x1xx、0x2xx的动力报文延迟抖到几十毫秒诊断响应就超时了。后来怎么解的两条路要么诊断会话期间把非必要报文的周期拉长降负载要么在项目前期就给关键报文分配更小的ID。ID分配从来不是拍脑袋填数字它就是优先级设计。500k波特率下一帧8字节标准帧大约130位含填充位约260μs。仲裁输一次等的不只是一帧是排在你前面所有赢家的发送时间总和。实战用CAPL盯住大ID报文的延迟抖动上面那个0x7DF超时的坑事后复盘时我写了个CAPL小脚本专门盯0x6xx这种大ID报文的两帧间隔。标称周期100ms的报文如果间隔突然抖到150ms以上基本就是在仲裁里连输了几轮variables{dword lastTime;// 上一帧到达的时间单位ms}on message0x6xx// 盯住0x6xx的舒适类报文标称周期100ms{dword nowtimeNowNS()/1000000;// 纳秒换算成毫秒if(lastTime!0){dword gapnow-lastTime;// 这一帧和上一帧的间隔if(gap150)// 超过150ms大概率在仲裁里被小ID报文挤掉了几轮{write(0x6xx 延迟抖动: 两帧间隔 %d ms怀疑高负载下仲裁被挤,gap);}}lastTimenow;}用法挂在CANoe仿真节点里跑Write窗口一旦刷出延迟抖动就去Trace里对时间戳看同一时段是不是0x1xx、0x2xx的报文特别密。定位高负载仲裁问题这招比肉眼扫Trace快得多。两个容易搞错的细节第一个标准帧和扩展帧谁优先。仲裁段里有个IDE位标准帧的IDE是显性0扩展帧的IDE是隐性1。所以base ID相同的情况下标准帧永远赢扩展帧。别被扩展俩字骗了29位ID不代表优先级更高。第二个ID不能重复。这是仲裁机制的铁律ID必须全网唯一。我见过一次翻车供应商A和供应商B的DBC里各自定义了一条0x2F1联调时error frame满天飞。两个节点ID相同仲裁段分不出胜负一直发到数据段才撞出错误查了一整天才定位到是ID冲突。这种问题DBC评审阶段就该拦下来。顺带提一句RTR位数据帧RTR0显性远程帧RTR1隐性同ID下数据帧赢。不过远程帧现在基本没人用了知道有这么回事就行。面试被问到仲裁就这么答“仲裁靠的是显性/隐性线与特性ID逐位发送、逐位回读比较小的赢。非破坏性仲裁赢家零延迟继续发输家转接收。实际影响是ID分配就是优先级分配总线高负载时大ID报文会有延迟抖动做测试要关注最坏情况延迟。”再补一句自己的项目经历比如上面那个0x7DF超时的例子基本就稳了。面试官要的不是背书是你真在总线上见过这玩意儿。收个尾仲裁这件事拆到底就是三句话显性压住隐性ID小者赢赢家不停顿。下次再有人问你报文同时发会撞吗你可以直接反问他“你知道0x18和0x19同时发谁会闭嘴吗” 往期推荐第一次打开CANoe先看懂这3个窗口汽车电子测试工程师每天到底在干啥五大质量工具之FMEA失效模式分析刚入行做汽车电子测试先搞懂这5个概念DoIP诊断实战——以太网时代的UDS怎么调视觉通用智能来了一篇论文重新思考AGI未来的AI可能首先要看懂世界啃完这本开源教材大模型的底层逻辑我算是理清了从零开始用ComfyUI跑MiniMaxH3本地安装、云端和视频工作流搞懂UDS诊断从这篇开始——测试应用层工程师实战指南