首页 / 驱动算法 / CANopen专题

CAN 总线测试:物理层、协议层与故障诊断

"总线时好时坏"是 CAN 工程里最难缠的问题。本文把测试拆成两层:物理层看电压波形(显性/隐性电平、差分幅值、采样点、终端阻抗),协议层看帧与错误(CRC、位填充、五类错误、bus-off、负载率),再用一套故障排查流程把示波器读数翻译成诊断结论,配故障诊断示波器交互演示和 SocketCAN 测试 C 代码。

1. 测试框架:两层看总线

CAN 通信故障分两类:物理层问题(电平、波形、阻抗、干扰)和协议层问题(波特率失配、ID 冲突、错误状态)。排查时先分清现象属于哪一层,能少走一半弯路:

层次看什么用什么典型现象
物理层CANH/CANL 电压、差分幅值、边沿、采样点、终端阻抗示波器(差分探头或双通道相减)、万用表波形畸变、电压超范围、时好时坏、长线误码
协议层帧结构、CRC、位填充、错误帧类型、错误计数、负载率CAN 分析仪、SocketCAN(candump/ip)、逻辑分析仪错误帧风暴、Bus-off、偶发丢帧、周期性重发

先万用表量阻抗、示波器看波形,再做协议层抓帧——因为协议层的"错误帧风暴"往往只是物理层问题的表象。

2. 物理层电平规范:ISO 11898-2

高速 CAN(ISO 11898-2)用 CANH 与 CANL 之间的差分电压表示位:

状态逻辑CANHCANL差分 Vdiff=CANH−CANL
显性(Dominant)0≈3.5V≈1.5V≈2V(1.5~3.0V)
隐性(Recessive)1≈2.5V≈2.5V≈0V(−0.5~+0.05V)

接收器判定阈值:Vdiff > 0.9V 判显性,< 0.5V 判隐性(0.5~0.9V 为不确定区)。共模电压应控制在 −2V~+7V。要点:

3. 示波器测量项目:电平、位时间与采样点

用示波器在 CANH/CANL 差分通道上逐项核对(建议每项都截图存档):

测量项合格判据异常解读
隐性电压≈2.5V(2.0~3.0V)偏置/供电异常
显性差分幅值1.5~3.0V,典型 2V<1.5V:终端过小/线太长;>3V:终端缺失/阻抗失配
位时间 Tbit1/波特率(500k → 2µs)偏差>容差 → 采样错位、错误帧
采样点位置位内 60~90%,全网一致不同节点采样点差异大 → 同步失效
边沿振铃/过冲过冲小、无持续振铃终端缺失/阻抗不连续 → 反射
终端阻抗(断电量)CANH−CANL 静态 ≈60Ω≈120Ω:缺一端终端;≈0Ω:短路;∞:断路

终端电阻的实测判断:断电后在总线任一点量 CANH−CANL 间电阻。两端各 120Ω 并联 = 60Ω;只量到 120Ω 说明少一端终端(或该节点没上电生效);0Ω 是短路(注意别把收发器内部电阻误判),∞ 是断线。

⚠️ 万用表测阻抗的两个坑

一是必须断电测量,上电时收发器有源阻抗会干扰读数;二是量到的 60Ω 是"两端并联"结果,不能据此判断"每个节点都接了"——逐节点断电复测,或直接看节点终端开关状态。

4. 协议层测试:帧、错误与负载率

4.1 五类错误与错误帧

CAN 的可靠性来自"发送自检 + 全节点监督"。协议层测试的核心是识别错误帧类型——它直接告诉你问题在哪:

错误类型检测原理常见诱因
位错误发送节点发出去的位与总线回读不一致(仲裁段/ACK 段除外)电平异常、短路、两节点同时收发器冲突
填充错误SOF~CRC 段出现连续 6 个相同位(违反 5 位插 1 反位规则)控制器同步丢失、干扰造成长串同电平
CRC 错误接收节点重算的 CRC 与帧内 CRC 不符干扰、线缆质量、位错误未被当场发现
形式错误固定格式字段(定界符/EOF/ACK 定界符)出现非法电平时序错乱、干扰
ACK 错误ACK 槽采样到隐性(没有节点应答)总线上只有发送节点、接收节点未同步

任何节点检测到错误,立即发错误帧:主动错误标志(6 个显性位)+ 错误定界符(8 个隐性位)。错误帧会让当前帧作废并被重发,这是"抓不到正常帧、满屏错误帧"的机制根源。

4.2 错误状态机与 Bus-off

每个节点维护收发错误计数器 TEC/REC,按计数分三态:

状态错误计数行为
主动错误(Error Active)TEC<128 且 REC<128正常收发,出错发主动错误帧(6 显性位)
被动错误(Error Passive)128≤TEC≤255只能发被动错误帧(6 隐性位),发送前等待
Bus-offTEC>255节点脱离总线,不参与任何收发

Bus-off 恢复:节点需连续检测到 128 次"11 个隐性位"(总线空闲)后自动回到主动错误状态。排查 bus-off:先看错误类型——TEC 快速上涨通常是位错误(自身发不出去),稳定上涨多是 ACK/CRC(总线问题)。Linux 下用 ip -s -d link show can0 可看内核统计的 error 计数与 bus-off 次数。

4.3 负载率计算

总线负载率(经典 CAN 2.0)

单帧位数 ≈ 47 + 8×DLC(标准帧)/ 67 + 8×DLC(扩展帧),单位:位

负载率 = Σ(帧位数) / 波特率 / 统计窗口时长 × 100%

注:公式不含位填充(实际约 +10~25%)与帧间隔外的总线空闲;重发帧会重复计入

工程参考:负载率 <30% 属于轻载,>50% 就要关注仲裁延迟与抖动,>80% 在实时场合基本不可用。测负载率有两个口径——协议层(分析仪按帧统计,本文 C 代码实现)和物理层(示波器统计总线空闲比例,更真实,含错误帧占用)。

5. 故障排查流程:从现象到结论

拿到一个 CAN 故障,按"量 → 看 → 拆"三步走:

  1. 量阻抗:断电量 CANH−CANL ≈60Ω?不是则修终端/短路/断路。
  2. 看波形:上电后差分波形是否符合 11898-2?显性 2V/隐性 0V、位时间对、边沿干净?
  3. 拆节点:分析仪抓帧,逐节点断开,看错误帧是否消失——定位肇事节点。
现象最可能原因验证手段
完全无波形/无通信断线、未供电、收发器损坏万用表通断、示波器看 VCC
波形幅值偏小终端缺失/过多、线缆过长量 60Ω、看边沿振铃
单节点发报错(ACK)总线上只有自己、对方没同步确认对端节点数、波特率一致
满屏错误帧/CRC波特率不匹配、干扰、线缆质量分析仪看错误类型、示波器看毛刺
时好时坏接触不良、共模干扰、终端松动晃动线缆复现、测共模电压
节点 Bus-off 掉线该节点自身发送路径故障看 TEC 上涨速率与错误类型

6. 交互式演示:故障诊断示波器

选择一种总线场景,示波器视图画出 CANH/CANL 波形(500kbps 下 SOF + 数据位的示意波形),下方给出电平判定与诊断建议。波形为示意,用于把"现象 → 结论"对应起来。

CAN 总线故障诊断示波器演示
正常总线:显性差分≈2V、隐性≈0V,位时间 2µs(500k),波形边沿干净。

观察要点:多数故障最终都表现为错误帧风暴或节点掉线,但根源在物理层——示波器看到的是"因",分析仪看到的是"果"。先治因,错误帧自然消失。

7. 总线测试 C 代码(SocketCAN)

下面的程序在 Linux 上用 SocketCAN 原始套接字统计一个窗口内的数据帧数、错误帧数、负载率与重复帧,并对错误帧比例过高的场景给出 bus-off 风险提示(Linux 下可再用 ip -s -d link show can0 核对内核统计)。

/* can_test.c —— SocketCAN 总线测试:帧/错误统计与负载率 * 编译:gcc -o can_test can_test.c * 运行:sudo ./can_test can0 5 (接口 统计窗口秒数) * 依赖:Linux SocketCAN(can-utils 同源接口) */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <sys/ioctl.h> #include <net/if.h> #include <linux/can.h> #include <linux/can/raw.h> #include <time.h> static uint64_t now_us(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return (uint64_t)ts.tv_sec * 1000000 + ts.tv_nsec / 1000; } /* 帧位长估算(不含位填充):标准帧 47+8*DLC,扩展帧 67+8*DLC */ static uint32_t frame_bits(const struct can_frame *f) { uint8_t dlc = f->can_dlc > 8 ? 8 : f->can_dlc; /* 经典 CAN 场景 */ if (f->can_id & CAN_EFF_FLAG) return 67 + 8*dlc; return 47 + 8*dlc; } int main(int argc, char **argv) { const char *ifname = argc > 1 ? argv[1] : "can0"; double window = argc > 2 ? atof(argv[2]) : 5.0; const uint32_t BITRATE = 500000; /* 按实际波特率修改 */ int s = socket(PF_CAN, SOCK_RAW, CAN_RAW); if (s < 0) { perror("socket"); return 1; } struct ifreq ifr; strcpy(ifr.ifr_name, ifname); if (ioctl(s, SIOCGIFINDEX, &ifr) < 0) { perror("ioctl"); return 1; } struct sockaddr_can addr; memset(&addr, 0, sizeof(addr)); addr.can_family = AF_CAN; addr.can_ifindex = ifr.ifr_ifindex; if (bind(s, (struct sockaddr *)&addr, sizeof(addr)) < 0) { perror("bind"); return 1; } uint64_t start = now_us(); uint64_t bus_bits = 0; long frames = 0, errs = 0, dup = 0; uint32_t last_id = 0; while ((now_us() - start) < (uint64_t)(window * 1e6)) { struct can_frame f; ssize_t n = recv(s, &f, sizeof(f), 0); if (n < 0) continue; if (f.can_id & CAN_ERR_FLAG) { /* 错误帧 */ errs++; printf("[ERR] 错误帧 can_id=0x%08X\n", f.can_id); continue; } if (f.can_id == last_id) dup++; /* 连续重复帧粗略计数(重发) */ last_id = f.can_id; frames++; bus_bits += frame_bits(&f); } double load = (double)bus_bits / BITRATE / window * 100.0; printf("窗口 %.1fs:数据帧 %ld,错误帧 %ld,连续重发 %ld\n", window, frames, errs, dup); printf("总线占用约 %.1f 位 → 负载率 ≈ %.1f%%(不含位填充)\n", (double)bus_bits, load); if (frames > 0 && (double)errs / (frames + errs) > 0.05) printf("⚠ 错误帧占比 %.0f%%:先查物理层(终端/波形/干扰),再查波特率与节点同步;用 'ip -s -d link show %s' 看 bus-off 计数\n", 100.0 * errs / (frames + errs), ifname); close(s); return 0; }

8. 测试报告要点

现场测试建议按以下清单留档,故障复现时直接对比"基线 vs 故障":

ℹ️ 工具链衔接

物理层截图配合协议层抓帧是完整证据链:本站 SocketCAN 篇 的 candump/cansend 可直接当分析仪用,从站实现篇 的负载率分析与此处公式一致,双波特率篇 的采样点计算可对照示波器实测。

9. 小结

CAN 总线测试的核心是分层定位:先用万用表量出 60Ω、再用示波器确认电平与波形、最后用分析仪看错误帧与负载率。记住三句口诀:阻抗不对先修终端,波形不对先查线缆与干扰,错误帧风暴先看物理层再怀疑协议。多数"时好时坏"最后都能在示波器上找到答案。