CAN 总线错误帧排查实战:一条”隔几小时报一次总线-off”的产线设备,逼我把 CAN 的错误机制从头到尾啃了一遍
老规矩,先说结论:CAN 上的偶发错误帧,九成以上是物理层的事——位时序、终端电阻、共模电压、布线,而不是软件。软件的唯一责任是:把控制器里的错误计数器读出来,别让设备在 bus-off 里装死。这篇文章就是我排查一台”隔几个小时报一次总线-off”的现场设备时,把 CAN 错误机制啃透的全过程。
一、问题现场:一天报 3~5 次 bus-off,重启后一切正常
事情是这样的:一台部署在变频器柜子旁边的采集设备,STM32 + 收发器 + 12 个节点的 CAN 网络,通信速率 500 kbps。现场反馈:每天随机报 3~5 次通信故障,设备重启后立刻恢复,抓不到任何规律——跟温度没关系、跟负载没关系、跟谁在发数据也没关系。
我们先做了两件事:
- 把故障日志拉出来看,确认固件确实进了 bus-off,而且是固件自己检测到 TE>127 后主动标记的,不是上层软件瞎报;
- 找了台 CAN 分析仪挂到总线上连续监听,想抓错误帧的现场。
结果分析仪抓了两天,一无所获——错误帧只在故障瞬间出现几毫秒,分析仪的实时上传通道根本来不及把完整错误帧记录下来。这就逼着我换个思路:不盯着错误帧本身,而是去读 CAN 控制器内部的错误状态寄存器。
二、先把 CAN 的错误机制补齐:错误帧到底长什么样
排查之前必须把基础补齐,不然寄存器里那个数字你看不懂。CAN 的错误检测靠一整套机制,我按”谁发现了错”分个类:
| 错误类型 | 检测机制 | 典型物理层原因 |
|---|---|---|
| Bit Error | 发送的节点回读总线,发现”我发 0 总线上是 1″ | 干扰、地电位差、驱动能力不足 |
| Stuff Error | 第 5 个连续同电平位后没插入反码位 | 波特率不匹配、采样点偏差 |
| Form Error | 固定格式位(CRC 界定符、ACK 界定符等)电平不对 | 协议异常、总线电平残缺 |
| CRC Error | 接收方算出的 CRC 与收到的不一致 | 干扰导致的位翻转 |
| Ack Error | 发送方在 ACK 槽没等到显性位(0) | 没有节点应答、终端/阻抗问题 |
发现错误的节点会立刻在总线上广播一个错误帧:主动错误状态下是 6 个显性位 + 8 个隐性位(Error Flag + Error Delimiter),这 6 个显性位故意违反填充规则,让所有节点都同步到这次”报错”。
然后是三个错误计数器和三种状态:
- TEC(发送错误计数) / REC(接收错误计数):出错加、成功减,规则很细(比如发送时发现 Bit Error 加 8,成功收发减 1);
- TEC/REC ≤ 127:Error Active——正常状态,可以正常收发,出错发主动错误帧;
- 任一 > 127:Error Passive——还能收发,但出错只发 6 个隐性位的被动错误帧(不会干扰别人),并且发送后要额外等 8 位暂停时间;
- TEC > 255:Bus Off——控制器从总线脱开,只收不发,这是最危险的:不处理就等于设备”通信猝死”。
理解了这个机制,再回头看现场现象就清晰了:“隔几小时报一次 bus-off”意味着 TEC 在短时间内从正常值冲破 255——即短时间内连续发生了大量 Bit Error(每次 +8)。这指向的不是”偶发一位干扰”,而是某种持续数毫秒的物理层异常。
三、第一步:别猜,先读控制器的 LEC 错误码
STM32 的 bxCAN 控制器把”最后一次错误”记录在 CAN_ESR 的 LEC(Last Error Code)位里,这个信息绝大多数固件根本没读——太可惜了。我在故障日志里加了读取逻辑:
/* STM32 bxCAN: 故障时读取 ESR,解析最后错误码 */
static const char *lec_str[8] = {
"No Error", /* 0b000 */
"Stuff Error", /* 0b001 */
"Form Error", /* 0b010 */
"Ack Error", /* 0b011 */
"Bit1 Error", /* 0b100: 发送隐性位时回读到显性 */
"Bit0 Error", /* 0b101: 发送显性位时回读到隐性 */
"CRC Error", /* 0b110 */
"Set by Software", /* 0b111 */
};
uint32_t esr = READ_REG(CAN1->ESR);
uint8_t lec = (esr & CAN_ESR_LEC) >> CAN_ESR_LEC_Pos;
uint8_t tec = (esr & CAN_ESR_TEC) >> CAN_ESR_TEC_Pos;
uint8_t rec = (esr & CAN_ESR_REC) >> CAN_ESR_REC_Pos;
log_printf("CAN FAULT: LEC=%s TEC=%u REC=%u %s",
lec_str[lec], tec, rec,
(esr & CAN_ESR_BOFF) ? "[BUS-OFF]" :
(esr & CAN_ESR_EPVF) ? "[PASSIVE]" : "[ACTIVE]");
/* 记得清 LEC:向 LEC 位写 1 */
SET_BIT(CAN1->ESR, CAN_ESR_LEC);
跑了一晚上,抓到 5 次故障,日志高度一致:
CAN FAULT: LEC=Bit0 Error TEC=248 REC=0 [BUS-OFF]
Bit0 Error(0b101)= 我往总线上发显性电平(0),回读却是隐性的。 而且 TEC 一路冲到 248 才进 bus-off、REC 是 0(说明这一刻只有它在发、别人根本收不到)——这是非常典型的发送方”喊话没人听得见”,或总线在它说话时电平塌了。
如果这里抓到的是 Stuff Error,我会优先查波特率和采样点;抓到 CRC Error 优先查干扰;抓到 Ack Error 优先查终端电阻和应答节点。LEC 就是排查方向的分诊台。
四、第二步:用示波器抓物理层,别用眼睛看逻辑
既然是 Bit0 Error,下一步就是物理层。这里我犯过一个错:用逻辑分析仪看 CAN_H/CAN_L——逻辑分析仪只能看 TTL 门限附近,看不到差分电平的实际幅值和对称性,而 CAN 的问题恰恰藏在幅值和差分里。正确做法是示波器双通道接 CAN_H 和 CAN_L,开启 Math 差分(CAN_H − CAN_L),同时看:
- 显性电平幅值:正常 2 V 左右(CAN_H≈3.5 V,CAN_L≈1.5 V)。低于 1.5 V 说明驱动能力不足或分支太长;
- 隐性电平是否回归:CAN_H、CAN_L 都应回到 2.5 V。如果隐性时电平”漂着回不去”,多半是终端电阻位置不对或总线偏置出了问题;
- 位边沿的上升/下降时间:500 kbps 下位时间 2 μs,边沿应当陡峭。边沿变缓会压缩采样点处的建立时间,边际上就是误码。
正好现场有台四通道示波器,我们在一次故障复现(把设备搬去变频器旁边通电跑大电流)时抓到了现场:
显性电平从 2 V 掉到了 0.4 V,而且下降沿上叠着一串 200 ns 级的振铃。 也就是说发送方往总线上灌显性电平,总线却没拉起来——回读自然是隐性,Bit0 Error 顺理成章。
五、第三步:从”电平塌了”反查硬件,挖出三个叠加的问题
顺着幅值塌陷往下查,最后挖出来是三个问题叠加,单个都不致命,凑一起就是灾难:
- 终端电阻不在总线两端,而在两个最远的节点上。这台采集设备和另一个节点恰好处在总线中段,两个 120 Ω 串在中段相当于 60 Ω 中点端接,两端反而开路——信号在总线两端来回反射。这是施工队”哪方便焊哪”造成的,CAN 的终端电阻必须且只能放在物理拓扑的两个端点;
- 有一条 3 m 的分支线(stub),且没有刻意控制长度。500 kbps 下经验值 stub 不超过 0.3~0.5 m,3 m 的分支就是一个反射腔,振铃的 200 ns 周期和它的往返延时对得上;
- 收发器供电压降过大:量了收发器 VCC,只有 4.6 V(标称 5 V),同一柜子里大电流启动时跌到 4.3 V,输出驱动能力进一步打折。
整改措施和验收标准:
- 终端电阻移到总线物理两端,中段确认无并联端接;
- 3 m 分支改造成总线”串珠”式布线,节点直接从主干就近下线,stub ≤ 0.3 m;
- 收发器单独走一路 LDO 供电,并在 VCC 上加 100 nF + 10 μF 去耦,量测满载下 VCC ≥ 4.85 V;
- 复测显性电平:2.1 V,振铃峰值 < 300 mV,半天压测 0 错误帧。
另外补一句共模电压:CAN 规范要求节点间共模承受范围 −2 V ~ +7 V。这个柜子里变频器和采集设备各自接了不同的大地,实测两地之间有 1.8 V 电位差,叠加共模干扰后接近边界。最终加了双绞屏蔽线 + 屏蔽层单点接地,并把两台设备的电源参考地拉到同一汇流排。如果你的设备要挂进别人的网络,共模和接地一定是进场前先问清楚的事。
六、软件侧必须做对的三件事:读错误、管 bus-off、别裸奔
硬件整改完,软件侧我回头把错误管理补成了完整方案,这里三件事缺一不可:
第一,错误中断里记录 LEC/TEC/REC,第三节那段代码就是。没有这个,现场每次故障都是”黑洞”。
第二,bus-off 恢复不能依赖”重启设备”。 bxCAN 支持自动恢复(ABOM 位),但我的建议是关掉 ABOM,软件延时后手动恢复——立刻自动恢复在持续物理故障时会陷入”恢复→再冲进 bus-off”的死循环,还会把错误帧风暴灌回总线:
/* bus-off 恢复策略:退避后软件恢复,而非自动恢复 */
if (esr & CAN_ESR_BOFF) {
can_offline_hook(); /* 上报离线,停止发送 */
/* 指数退避:1s, 2s, 4s ... 最大 60s,成功通信 N 次后复位退避 */
uint32_t backoff = MIN(60u, 1u << busoff_retries++);
osDelay(backoff * 1000);
/* 硬件要求:先清 INAK 重新进入初始化,再请求退出 bus-off */
CAN1->MCR |= CAN_MCR_INRQ;
while (!(CAN1->MSR & CAN_MSR_INAK)) {}
CLEAR_BIT(CAN1->MCR, CAN_MCR_INRQ); /* 退出初始化,控制器自动重同步 */
can_online_hook();
}
第三,应用层要有通信看门狗。 即使总线一切正常,对端设备也可能死机。我在协议里加了周期心跳,应用层 3 秒收不到任何帧就报”链路异常”,和 CAN 控制器的错误状态互相印证——这次排查里,”应用层收不到帧但控制器没报错”这个矛盾现象,就是靠两边状态对比才定位到收发器供电问题的。
七、常见错误帧场景速查表
把这次和以前踩过的场景汇总成表,排查时按 LEC 对号入座:
| LEC / 现象 | 首要怀疑点 | 验证手段 |
|---|---|---|
| Bit0 Error(发显性读回隐性) | 驱动能力、终端电阻位置、收发器供电 | 示波器看显性幅值 |
| Bit1 Error(发隐性读回显性) | 总线持续被某节点拉显性、节点短路 | 逐个断开节点定位 |
| Stuff Error 风暴 | 波特率不匹配、采样点偏差 | 示波器量位时间,对 1/1000 |
| Ack Error 持续 | 无应答节点在线、终端阻抗异常 | 只留发送方+一个节点测试 |
| CRC Error 偶发 | 干扰、屏蔽与接地、共模电压 | 示波器看边沿振铃,查屏蔽接地 |
| 上电即 Ack Error | 节点没上齐、波特率不一致 | 核对所有节点位时序配置 |
| 偶发错误帧无规律 | 地电位差、变频器等干扰源 | 搬离干扰源对比测试 |
其中”采样点”多说一句:惯例是采样点放在位时间的 75%~87.5%,全网所有节点采样点差值不要超过 2%。很多仪器厂商默认 87.5%,而 MCU 例程常配出 80% 甚至 70%,混网之后就是偶发 Stuff Error。计算公式:
采样点 = (1 + TSEG1) / (1 + TSEG1 + TSEG2)
配置位时序时用这个式子反算,别抄例程的魔法数字。
八、CAN 错误排查自查清单
- ☐ 固件读取并记录 CAN_ESR 的 LEC / TEC / REC(故障瞬间快照)
- ☐ bus-off 有软件恢复策略,指数退避,不依赖自动恢复和重启设备
- ☐ 应用层有独立于控制器的通信看门狗(心跳超时上报)
- ☐ 终端电阻位于总线物理两端(不是”两个远端节点”,是拓扑端点)
- ☐ 分支线(stub)长度 ≤ 0.3~0.5 m(500 kbps),尽量串珠式布线
- ☐ 示波器量测显性电平 ≥ 1.8 V,隐性回归 2.5 V,边沿振铃可接受
- ☐ 收发器 VCC 满载 ≥ 标称值 −2%,并有独立去耦
- ☐ 全网节点采样点统一在 75%~87.5%,节点间差值 < 2%
- ☐ 屏蔽线 + 屏蔽单点接地,确认节点间地电位差在规范内
- ☐ 干扰源(变频器、继电器、大电流回路)与 CAN 走线物理隔离
- ☐ 故障日志包含 LEC 解析后的文字描述,而不是裸数字
- ☐ 高压测试:满负载 + 干扰源开启 + 温度循环下压测 24h 无错误帧
九、写在最后
这次排查给我最大的教训是:CAN 的健壮性是”物理层设计 + 错误管理软件”共同给的,芯片手册里那句”CAN 支持错误检测与自动重传”会让很多人误以为接上就能用。真实世界里,CAN 的可靠性上限取决于你有没有:
- 在故障发生的一瞬间读出 LEC 和错误计数(分诊);
- 拿着示波器看差分电平而不是看逻辑(确诊);
- 把终端电阻、分支线、供电、共模这四件事在设计阶段就锁死(预防);
- bus-off 的软件恢复策略写对,别让一次物理故障升级成设备离线(兜底)。
下一篇预告:把视角从错误排查拉回到 RTOS 内核,聊聊 FreeRTOS 事件组与任务通知的进阶用法——什么时候该用任务通知替代队列和信号量,以及事件组在多任务同步启动场景下的一个很妙的设计模式。有兴趣的老哥可以蹲一下。