4. CAN错误处理机制:错误类型与状态机

CAN总线的错误处理,是我认为整个协议栈里最体现设计功底的部分。说白了,CAN之所以能在工业、汽车领域活这么久,靠的就是这套严密的错误检测和恢复机制。你想想看,一条双绞线上挂着几十个节点,电磁干扰、电压波动、接触不良……不出错才怪。但CAN能保证:只要没断线,数据要么正确到达,要么被明确告知出错。

这一章,我们就来拆解CAN的五大错误类型,以及节点如何在这三个状态间切换——主动错误、被动错误、总线关闭。

4.1 五大错误类型

CAN协议定义了五种错误检测机制。我在项目里见过不少开发者只盯着CRC,忽略了其他四种,结果现场跑起来各种诡异问题。嗯,咱们一个一个说清楚。

错误类型 检测层级 触发条件 我见过的坑
位错误 发送节点 发送位与总线采样位不一致 终端电阻不匹配导致回读电平错误
填充错误 所有节点 连续6个相同电平出现 干扰导致填充位被翻转
CRC错误 接收节点 本地CRC与发送端CRC不匹配 波特率偏差导致采样点偏移
格式错误 所有节点 固定格式位出现非法电平 CAN控制器配置错误
ACK错误 发送节点 ACK时隙未收到显性位 总线上没有其他节点在线

4.1.1 位错误(Bit Error)

这个错误只发生在发送节点身上。发送节点往总线上放一个电平,同时回读总线上的实际电平。如果两者不一致,立刻报位错误。

我曾经调试过一个设备,在实验室跑得好好的,一到现场就疯狂报位错误。查了两天才发现,现场用的线缆太长,终端电阻匹配不对,导致回读信号畸变。说白了,位错误很多时候是物理层的问题,别光盯着协议栈。

关键点:仲裁阶段不会触发位错误。因为仲裁时多个节点同时发送,电平不一致是正常的。CAN控制器在仲裁期间自动屏蔽位错误检测。

4.1.2 填充错误(Stuff Error)

CAN协议规定:连续发送5个相同电平后,必须插入一个相反电平的填充位。如果接收端检测到连续6个相同电平,就判定为填充错误。

为什么要这么设计?为了保证足够的电平跳变,方便接收节点做时钟同步。你想想看,如果一长串全是0,接收端的锁相环早就跑飞了。

个人习惯:我在写CAN驱动时,会特意在测试用例里构造连续相同数据,验证填充逻辑是否正常。很多芯片的CAN控制器内部已经处理了填充,但如果你在做FPGA实现,这块必须自己写。

4.1.3 CRC错误

CRC(循环冗余校验)覆盖了从SOF到数据场的所有位。发送节点计算CRC并放在CRC场,接收节点重新计算并比对。不一致就报CRC错误。

我记得有一次,客户反馈说某批设备偶尔丢帧。抓波形发现CRC错误率高达0.1%。后来定位到是晶振精度不够,波特率偏差导致采样点刚好落在跳变沿上。换了个高精度晶振,问题消失。

注意:CRC错误不代表数据一定错了,也可能是采样点位置不对。排查时先看示波器,别急着怀疑算法。

4.1.4 格式错误(Form Error)

CAN帧中有一些固定格式的位段,比如CRC界定符、ACK界定符、EOF等。这些位段必须是显性或者隐性,有严格规定。如果检测到非法电平,就报格式错误。

格式错误通常意味着总线上的节点对协议理解不一致,或者控制器配置有误。我见过最离谱的一次,是有人把CAN控制器的模式配成了“基本CAN”,结果发扩展帧时格式错误满天飞。

4.1.5 ACK错误

发送节点在ACK时隙发送隐性位,然后等待总线上的显性位。如果有任何一个接收节点正确收到了帧,就会在ACK时隙拉显性。如果发送节点没收到显性,就报ACK错误。

这个错误最常见的场景就是:总线上只有一个节点在发,没有其他节点接收。或者接收节点处于总线关闭状态。

实战经验:调试初期,我习惯在发送节点上监控ACK错误计数。如果这个计数持续增长,先别查协议栈,去确认总线上有没有其他节点在线。

4.2 错误状态机

每个CAN节点内部维护两个计数器:发送错误计数器(TEC)和接收错误计数器(REC)。根据这两个计数器的值,节点在三个状态间切换。

下面这张图展示了完整的错误状态迁移逻辑:

CAN节点错误状态机 主动错误 TEC ≤ 127 且 REC ≤ 127 被动错误 TEC > 127 或 REC > 127 总线关闭 TEC > 255 TEC/REC超限 TEC/REC恢复 TEC > 255 128个隐性位恢复 状态说明: • 主动错误:正常参与通信,检测到错误时发送主动错误帧(6个显性位) • 被动错误:仍可通信,但只能发送被动错误帧(6个隐性位),且发送前等待8个隐性位 • 总线关闭:完全断开总线,不发送任何数据,不参与任何通信

4.2.1 主动错误状态(Error Active)

这是节点的正常工作状态。TEC和REC都小于等于127。在这个状态下,节点检测到错误时,会发送主动错误帧——6个连续的显性位。这6个显性位会强制打断当前正在传输的帧,让所有节点都知道出错了。

主动错误帧的威力很大,它会覆盖总线上的任何数据。所以正常运行时,错误帧很少出现。一旦出现,说明总线确实有问题。

4.2.2 被动错误状态(Error Passive)

当TEC或REC超过127时,节点进入被动错误状态。这个状态下,节点仍然可以收发数据,但有两个限制:

  • 发送的错误帧是6个隐性位,不会干扰其他节点的通信
  • 发送数据帧前,必须等待8个连续的隐性位(主动错误状态不需要)

说白了,被动错误状态就是让节点“闭嘴”。你还能听,但别大声嚷嚷。我见过一些设计,节点进入被动错误后反复重发,结果TEC越涨越高,最后把自己搞到总线关闭。

避坑指南:我曾经在项目里遇到一个节点,进入被动错误后每秒重发几百次,把总线负载拉到80%以上。后来加了重发次数限制,超过3次就主动挂起,等一段时间再恢复。效果很好。

4.2.3 总线关闭状态(Bus Off)

当TEC超过255时,节点进入总线关闭状态。这个状态下,节点完全断开与总线的连接——不发送、不接收、不参与任何通信。

退出总线关闭的唯一方式:检测到128次11个连续的隐性位(即总线空闲状态)。这通常由硬件自动完成,但有些控制器需要软件触发恢复流程。

重要提醒:总线关闭不是错误,是保护机制。它防止一个坏掉的节点拖垮整条总线。但恢复策略需要谨慎设计——如果所有节点同时恢复,总线可能再次崩溃。我习惯让节点在恢复前随机等待一段时间,错峰上线。

4.3 错误计数器的增减规则

很多开发者以为错误计数器只是简单的“出错加1,成功减1”。其实规则要复杂得多。下面这个表格是我从项目中整理出来的:

事件 TEC变化 REC变化
发送节点检测到位错误 +8 不变
发送节点检测到ACK错误 +8 不变
发送节点检测到格式错误 +8 不变
接收节点检测到CRC错误 不变 +1
接收节点检测到填充错误 不变 +1
节点成功发送一帧 -1(不低于0) 不变
节点成功接收一帧 不变 -1(不低于0)
被动错误节点发送成功 0 不变

注意看,发送错误的惩罚是+8,而接收错误只有+1。为什么?因为发送节点对总线的影响更大。你想想看,一个发送节点如果出了问题,可能把整帧数据都搞乱。而接收节点只是自己没收到,不影响别人。

我的经验:调试时我习惯同时监控TEC和REC。如果REC持续增长但TEC稳定,说明总线干扰严重但节点还能发。如果TEC也在涨,说明节点本身可能有问题。这两个计数器能帮你快速定位故障方向。

4.4 错误处理的实战要点

说了这么多理论,最后分享几个我在项目中踩过的坑:

  1. 不要依赖硬件自动恢复总线关闭。有些控制器恢复后直接发数据,如果总线还有问题,立刻又关闭。我建议软件层做状态监控,恢复后先监听一段时间再发。
  2. 被动错误节点的重发策略要收敛。不加限制的重发会让TEC快速冲到255。我一般设一个最大重发次数,超限后主动等待。
  3. 错误帧也会被其他节点记录。一个节点发错误帧,所有接收节点都会REC+1。所以别小看一次错误,它可能引发连锁反应。
  4. 调试时把错误计数器暴露出来。我在每个CAN节点上都留了一个诊断接口,可以实时读取TEC和REC。现场排查问题时,这两个数比什么日志都管用。

CAN的错误处理机制,说白了就是一套“犯错-记过-关禁闭”的流程。理解了这个逻辑,你就能在设计阶段预判很多问题,而不是等到现场才手忙脚乱。