第4章:CAN错误处理与总线状态——别让节点“沉默”了
各位工程师,欢迎来到CAN协议最“硬核”的部分——错误处理。说实话,很多初学者觉得这章枯燥,不就是报错嘛?但我在实际项目中吃过亏,才明白:不懂错误处理,你连总线为什么“死掉”都查不出来。
CAN协议之所以能在汽车这种高噪声环境里活下来,靠的就是一套极其严密的错误检测与恢复机制。今天我们就把它拆开揉碎,讲清楚。
4.1 错误类型:CAN总线上的“五宗罪”
CAN协议定义了五种错误。我习惯把它们分成两类:“自己犯的错”和“别人犯的错”。但不管谁犯的错,每个节点都会去检测。
| 错误类型 | 触发条件 | 检测层级 | 我见过的坑 |
|---|---|---|---|
| 位错误 | 发送节点监控总线,发现发送的电平与总线实际电平不一致 | 发送节点 | 终端电阻没焊好,发送显性位却读到隐性位 |
| 填充错误 | 连续6个相同电平出现(违反位填充规则) | 所有节点 | 干扰导致填充位被翻转,直接报错 |
| CRC错误 | 接收节点计算的CRC与发送的CRC不匹配 | 接收节点 | 波特率偏差导致采样点偏移,CRC算不对 |
| 格式错误 | 帧格式中固定位域出现非法电平 | 所有节点 | CAN控制器配置错误,CRC界定符发成显性 |
| 应答错误 | 发送节点在ACK槽没收到显性位 | 发送节点 | 总线上只有一个节点在发,没人应答 |
关键点:除了位错误和应答错误由发送节点自己检测,其他三种错误是所有节点共同监督的。这就是CAN的“分布式检错”思想。
4.2 位错误——最直接的“打脸”
位错误的逻辑很简单:你发什么,你就得读到什么。
发送节点把位送到总线上,同时回读总线电平。如果读回来的和自己发的不一样,立刻报位错误。但有个例外——仲裁阶段。仲裁时发送隐性位却读到显性位,那是正常的仲裁失败,不视为错误。
我曾经调试过一个项目,CAN总线偶尔丢帧。查了半天,发现是PCB走线太长,信号反射导致发送节点读回的电平畸变。嗯,位错误有时候不是协议问题,是硬件问题。
4.3 填充错误——CAN的“防连续同电平”机制
CAN协议规定:连续发送5个相同电平后,必须插入一个相反电平的填充位。接收节点在解码时会自动移除这个填充位。
如果接收节点发现连续6个相同电平,就判定为填充错误。为什么要有这个规则?
- 保持时钟同步:电平翻转让节点能重新同步采样点
- 避免直流分量累积:长时间同电平会让电容耦合电路出问题
小技巧:调试时可以用示波器看CAN_H和CAN_L的差分信号。如果看到连续6个相同电平的波形,基本可以断定是填充错误。我常用这个方法来快速定位干扰源。
4.4 CRC错误——数据完整性的最后防线
CAN帧的CRC段包含15位CRC校验码(加上1位CRC界定符)。发送节点对起始帧、仲裁场、控制场、数据场计算CRC,接收节点用同样的多项式重新计算。
如果两边结果不一致,接收节点就报CRC错误,并发送错误帧。注意:CRC错误只由接收节点检测,发送节点并不知道自己的数据被破坏了。
我记得有一次,某供应商的ECU在高温下频繁报CRC错误。排查后发现是晶振温漂导致波特率偏移,采样点正好落在信号跳变沿上。换了个温漂系数更低的晶振,问题解决。
4.5 格式错误与应答错误
格式错误:CAN帧中有几个固定位域(如CRC界定符、ACK界定符、EOF),这些位必须发送隐性电平。如果某个节点在这些位置读到显性电平,就报格式错误。说白了,就是帧结构乱了。
应答错误:发送节点在ACK槽发送隐性位,然后等待总线上出现显性位(表示至少有一个节点正确接收)。如果没等到,就报应答错误。这通常意味着总线上没有其他节点在线,或者所有接收节点都检测到了错误。
4.6 错误计数器——CAN节点的“记分牌”
每个CAN节点内部有两个错误计数器:
- TEC(发送错误计数器):记录发送错误次数
- REC(接收错误计数器):记录接收错误次数
计数规则很有意思,不是简单的“错一次加一”:
| 事件 | TEC变化 | REC变化 |
|---|---|---|
| 发送节点检测到位错误/应答错误 | +8 | 不变 |
| 接收节点检测到CRC/填充/格式错误 | 不变 | +1 |
| 节点成功发送一帧 | -1 | 不变 |
| 节点成功接收一帧 | 不变 | -1(若REC>0) |
注意:错误计数器的增减幅度是不对称的。检测到错误时加得多(+8),成功时减得少(-1)。这意味着一个节点如果持续出错,计数器会快速飙升。这是故意设计的——让“坏节点”尽快被隔离。
4.7 总线状态机——从“主动”到“关闭”的三级跳
根据TEC和REC的值,CAN节点会处于三种状态之一:
三种状态的行为差异很大:
- 主动错误状态:节点正常收发。检测到错误时,发送主动错误帧(6个显性位),这个帧会覆盖总线上的其他数据,强制所有节点知道出错了。
- 被动错误状态:节点仍可收发,但发送错误帧时只能发被动错误帧(6个隐性位)。这意味着它的错误通知可能被其他节点的显性位覆盖。另外,被动错误节点在发送数据帧前,需要额外等待8个隐性位(称为“挂起传输”)。
- 总线关闭状态:节点完全与总线隔离,不发送也不接收。只有硬件复位或收到特定恢复序列才能重新上线。
核心思想:CAN的错误处理机制本质上是一个“劣币驱逐良币”的反向过程——频繁出错的节点会被逐步剥夺通信权利,最终被踢出总线,保证其他节点不受干扰。
4.8 错误帧——总线上的“紧急刹车”
当节点检测到错误时,会立即发送错误帧。错误帧由两部分组成:
- 错误标志:6个连续的同电平位(主动错误为显性,被动错误为隐性)
- 错误界定符:8个隐性位
有意思的是,错误标志的6个位会破坏位填充规则,导致其他节点也检测到填充错误,进而发送自己的错误标志。结果就是总线上出现6~12个显性位的“错误叠加区”。
我见过一个案例:某个节点因为软件bug,在被动错误状态下频繁发送错误帧。虽然它发的是隐性错误帧,但叠加了其他节点的显性错误帧后,总线被持续拉低,整个网络瘫痪了十几毫秒。嗯,一个“半坏”的节点比完全坏掉的节点更可怕。
4.9 总线关闭恢复机制
节点进入总线关闭状态后,不会永远沉默。CAN协议规定了一个恢复流程:
- 节点检测到总线空闲(连续11个隐性位)
- 等待128个总线空闲周期(128 × 11位时间)
- 将TEC和REC清零
- 重新进入主动错误状态
这个128次等待的设计,是为了确保节点在恢复前,总线上的错误风暴已经完全平息。我曾经在测试中故意让节点反复进入总线关闭状态,观察它的恢复行为——恢复后如果仍然出错,计数器会再次飙升,形成“错误-关闭-恢复-再错误”的循环。
实战警告:不要依赖硬件自动恢复!如果节点存在硬件故障(如CAN收发器损坏),它会反复进入总线关闭再恢复,导致总线周期性抖动。我建议在应用层增加超时监控,如果节点在短时间内多次进入总线关闭,直接上报故障码并保持关闭状态。
4.10 实战:如何用错误计数器定位问题
在实际调试中,读取节点的TEC和REC值是定位总线问题的利器。大多数CAN控制器都提供寄存器来读取这两个值。
/* 以STM32 bxCAN为例,读取错误计数器 */
uint8_t tec, rec;
/* 读取CAN_ESR寄存器中的TEC和REC */
tec = (CAN1->ESR >> 16) & 0xFF; /* TEC在bit[23:16] */
rec = (CAN1->ESR >> 24) & 0xFF; /* REC在bit[31:24] */
printf("TEC: %d, REC: %d\n", tec, rec);
if (tec > 96 || rec > 96) {
printf("警告:错误计数器偏高,请检查总线质量!\n");
}
if (tec > 127) {
printf("节点已进入被动错误状态!\n");
}
if (tec > 255) {
printf("节点已总线关闭!\n");
}
我个人的调试经验:
- 如果REC持续增长,说明节点收到了大量错误帧,问题可能在其他节点或总线干扰
- 如果TEC持续增长,说明节点自己发送出错,问题可能在本节点的硬件或软件
- 如果TEC和REC同时增长,大概率是总线物理层问题(如终端电阻、共模电压、线缆过长)
4.11 本章小结
CAN的错误处理机制,说白了就是一套“犯错-记分-惩罚-隔离”的闭环系统。五种错误类型覆盖了位级、帧级和协议级的各种异常;错误计数器用不对称的加减规则快速识别“坏节点”;总线状态机则确保坏节点不会拖垮整个网络。
理解这套机制,你就能回答一个经典面试题:“为什么CAN总线在强干扰下不会完全崩溃?”——因为每个节点都在独立检测错误,并且会自动隔离自己。
下次调试CAN总线时,别只盯着示波器看波形。读一读错误计数器,它能告诉你很多波形上看不到的故事。