4. CAN错误处理:错误类型、错误计数器、错误状态机与总线关闭

说实话,很多工程师做CAN节点,功能调通了就以为万事大吉。结果一上现场,总线动不动就掉线,节点莫名其妙就“死”了。我早年吃过这个亏——一个电机驱动节点,在实验室跑得好好的,到客户产线上每隔两小时就离线一次。查了三天,最后发现是错误处理机制没配好,节点把自己关总线外面了。

这一章,咱们就把CAN错误处理这块硬骨头啃下来。你搞懂了错误类型、计数器、状态机,才算真正入了CAN的门。

4.1 错误类型:CAN总线上的“五宗罪”

CAN协议定义了五种错误。任何节点检测到其中一种,就会立刻发送错误帧。我习惯把这五种错误分成两类:格式错误和内容错误。

错误类型 检测位置 触发条件 分类
位错误(Bit Error) 发送节点 发送位与总线电平不一致(仲裁场和应答间隙除外) 内容错误
填充错误(Stuff Error) 所有节点 连续6个相同电平位出现 格式错误
CRC错误(CRC Error) 接收节点 计算CRC与接收CRC不匹配 内容错误
格式错误(Form Error) 所有节点 固定格式位(CRC界定符、ACK界定符、EOF)电平错误 格式错误
应答错误(ACK Error) 发送节点 发送节点在ACK槽未检测到显性位 内容错误
我的经验:位错误和填充错误是最常见的。位错误多半是硬件问题——终端电阻没配好、线缆过长、共模干扰。填充错误则往往是时钟精度不够,或者总线负载太高导致位同步丢失。排查时先看这两种,能省一半时间。

4.2 错误计数器:两个“记分牌”

每个CAN节点内部有两个硬件计数器:发送错误计数器(TEC)和接收错误计数器(REC)。它们不是软件变量,是CAN控制器硬件自动维护的。你读寄存器就能拿到值。

计数的规则,我总结成一句话:谁犯错谁加分,别人犯错你减分。具体规则如下:

  • 发送节点检测到位错误、ACK错误、格式错误 → TEC加8
  • 发送节点检测到填充错误、CRC错误 → TEC加8(但如果是发送节点自己填充或CRC算错,也是加8)
  • 接收节点检测到错误 → REC加1
  • 节点主动发送错误标志 → TEC加8
  • 节点作为接收者,成功收到一帧无错误报文 → REC减1(前提是REC > 0)
  • 节点作为发送者,成功发送一帧无错误报文 → TEC减1(前提是TEC > 0)

你想想看,这个机制其实很巧妙。发送节点犯错扣分重,接收节点犯错扣分轻。因为发送节点对总线的影响更大。我曾经在调试一个多主站系统时,发现某个节点的TEC一直在128附近徘徊,但REC几乎为0。这说明它一直在尝试发送,但总线上有其他节点在干扰它的报文。

关键点:TEC和REC的最大值是255。一旦达到255,节点就会进入总线关闭状态。而且TEC的惩罚力度是REC的8倍——发送节点犯一次错,等于接收节点犯8次错。设计高可靠性节点时,要特别注意发送逻辑的健壮性。

4.3 错误状态机:三个“等级”

CAN控制器根据TEC和REC的值,把节点划分为三种状态。这就是CAN错误状态机。我习惯把它画成一个三级台阶:

CAN节点错误状态机 错误主动 TEC ≤ 127 REC ≤ 127 错误被动 TEC 128~255 REC 128~255 总线关闭 TEC ≥ 256 REC 不影响 TEC或REC 超过127 TEC超过255 128个总线空闲位后自动恢复 TEC和REC都降到127以下 状态行为差异 • 错误主动:发送主动错误标志(6个显性位),参与总线竞争 • 错误被动:发送被动错误标志(6个隐性位),不干扰总线 • 总线关闭:完全断开总线,不发送任何帧,不参与任何通信

三种状态的行为差异,我用一个表格给你说清楚:

状态 错误标志 对总线的影响 恢复条件
错误主动 主动错误标志(6个显性位) 会破坏其他节点的报文,强制终止当前通信 TEC和REC都降到127以下
错误被动 被动错误标志(6个隐性位) 不干扰总线,但发送的报文可能被其他节点忽略 TEC和REC都降到127以下
总线关闭 无 完全离线,不参与任何总线活动 检测到128个连续总线空闲位(11个隐性位)
注意:错误被动状态是个“灰色地带”。节点还能收发报文,但发送的被动错误标志不会强制终止错误帧。这意味着如果总线上只有一个错误被动的节点,它检测到错误后发送的隐性错误标志,其他节点可能收不到。我曾经在一个只有两个节点的系统中遇到过这种情况——一个节点进入错误被动后,整个总线就陷入了“发错→收不到错误标志→重发→再错”的死循环。

4.4 总线关闭:最严重的惩罚

总线关闭是CAN节点能遇到的最严重状态。TEC达到256(注意,不是255,是超过255)时触发。一旦进入这个状态,节点就像被踢出群聊一样,完全无法参与总线通信。

恢复机制很有意思:节点必须检测到128次连续11个隐性位(即总线空闲状态)。这128次不需要连续出现,但每次检测到11个隐性位算一次,累计128次后自动恢复。恢复后,TEC和REC都清零,节点回到错误主动状态。

我建议你在设计节点时,一定要考虑总线关闭后的处理策略:

  • 自动恢复:默认行为,节点自己恢复。适合大多数场景。
  • 软件干预恢复:通过复位CAN控制器或写寄存器手动恢复。适合需要记录故障日志的场景。
  • 硬件复位恢复:通过外部看门狗或复位引脚强制恢复。适合无人值守的系统。
我的建议:在CANopen协议栈中,总线关闭通常触发NMT心跳停止或紧急报文。我习惯在应用层加一个“总线关闭计数”变量,每次恢复后记录一次。如果一小时内总线关闭超过3次,就主动拉高一个GPIO,通知上层系统“这个节点快不行了”。这个做法帮我提前预警过好几次线缆老化问题。

4.5 实战:如何读取错误计数器

以STM32的bxCAN为例,读取TEC和REC很简单:

// 读取发送错误计数器
uint8_t tec = CAN->ESR & CAN_ESR_TEC_MASK;

// 读取接收错误计数器
uint8_t rec = (CAN->ESR >> 16) & CAN_ESR_REC_MASK;

// 读取错误状态
uint8_t state = (CAN->ESR >> 4) & 0x03;
// 0: 错误主动
// 1: 错误被动
// 2: 总线关闭

我一般在主循环里每100ms读一次这两个计数器,通过串口打印出来。调试时特别有用——你能直观看到节点在“犯错”和“恢复”之间来回摆动。

调试技巧:如果REC持续增长但TEC为0,说明节点在被动接收错误帧。问题可能出在其他发送节点上。如果TEC持续增长,说明这个节点自己发送有问题——检查它的时钟精度、CAN收发器供电、或者是不是有另一个节点在用相同的ID发报文。

嗯,CAN错误处理这块,说白了就是一套“犯错→记分→惩罚→恢复”的闭环机制。你理解了计数器的加减规则,看懂了状态机的跳转条件,就能在节点出问题时快速定位。别等到现场总线崩了才去翻手册——调试阶段就把错误计数器打印出来,养成习惯。