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

CAN总线的可靠性,说白了就靠这套错误处理机制撑着。我刚开始接触CAN驱动时,总觉得这部分理论性太强,直到在产线上亲眼看到一台设备因为错误累积直接“离线”,才意识到这东西有多重要。

这一章咱们就把CAN的错误处理机制拆开揉碎。先看5种错误类型分别是什么,再看节点怎么在这些错误中“活着”或“死掉”——也就是错误状态机。

4.1 五种错误类型

CAN协议定义了5种错误。任何节点在发送或接收时,只要检测到其中一种,就会立即发出错误帧。注意,是立即,不等当前报文发完。

错误类型 检测时机 检测节点 触发条件
位错误 发送时 发送节点 发送的位电平与总线实际电平不一致
填充错误 接收时 所有接收节点 连续6个相同电平(违反位填充规则)
CRC错误 接收时 所有接收节点 计算出的CRC与发送端CRC不匹配
格式错误 接收时 所有接收节点 帧格式中固定位域出现非法电平
应答错误 发送时 发送节点 ACK槽位未收到显性位(无人应答)

4.1.1 位错误(Bit Error)

这个错误只有发送节点能检测到。发送节点往总线上放一个位,同时回读总线电平。如果读回来的和自己发的不一样,就报位错误。

举个例子:你发的是显性位(0),但总线上读回来是隐性位(1)。那说明有其他节点同时在发,或者总线被拉高了。

避坑指南: 我曾经调试过一个项目,CAN总线偶尔出现位错误。查了半天发现是线缆太长,信号反射导致回读电平畸变。后来加了终端电阻匹配,问题就消失了。位错误不一定是节点打架,物理层问题也会触发。

4.1.2 填充错误(Stuff Error)

CAN协议规定:连续发送5个相同位后,必须插入一个相反位。这叫位填充规则。接收节点如果发现连续6个相同电平,就报填充错误。

为什么会这样?说白了是为了保证时钟同步。没有位填充,长时间没有跳变沿,接收节点的时钟就会漂移。

注意: 位填充规则只适用于SOF到CRC段。ACK、EOF、IFS这些段不参与填充。我见过有人把EOF里的连续隐性位当成填充错误,这是不对的。

4.1.3 CRC错误(CRC Error)

发送节点在CRC段发送15位CRC校验值。接收节点用同样的多项式重新计算,如果结果不一致,就报CRC错误。

CRC错误说明数据在传输过程中被破坏了。可能是电磁干扰、线缆接触不良,或者节点硬件故障。

4.1.4 格式错误(Form Error)

CAN帧中有几个固定位域,比如CRC界定符、ACK界定符、EOF。这些位域的值是协议写死的。如果接收节点读到非法电平,就报格式错误。

举个例子:CRC界定符必须是隐性位(1)。如果读到显性位(0),那就是格式错误。

4.1.5 应答错误(ACK Error)

发送节点在ACK槽位发送隐性位(1),然后等待接收节点拉成显性位(0)。如果没人拉,说明总线上没有其他节点正确收到报文。发送节点就报应答错误。

我个人的习惯是:调试初期如果看到大量应答错误,先检查总线终端电阻和节点数量。有时候只有一个节点在发,没有接收节点,应答错误是正常的。

4.2 错误状态机

每个CAN节点内部都有两个计数器:发送错误计数器(TEC)和接收错误计数器(REC)。节点根据这两个计数器的值,处于三种状态之一。

核心逻辑: 错误计数器的增减规则是“发送错误加8,接收错误加1,成功发送减1,成功接收减1”。这个规则设计得很巧妙——发送节点犯错代价更大,因为它影响整个总线。
CAN节点错误状态机 主动错误状态 TEC ≤ 127 REC ≤ 127 被动错误状态 128 ≤ TEC ≤ 255 或 128 ≤ REC ≤ 255 总线关闭状态 TEC > 255 TEC/REC超限 TEC/REC恢复 TEC > 255 状态说明 • 主动错误:节点正常参与通信,检测到错误时发送主动错误帧(6个显性位) • 被动错误:节点可收发,但发送错误帧时只能发被动错误帧(6个隐性位) • 总线关闭:节点完全脱离总线,不发送任何帧,不参与任何通信 • 恢复条件:总线关闭后,节点需检测到128次11个连续隐性位才能恢复

4.2.1 主动错误状态(Error Active)

这是正常状态。TEC和REC都小于等于127。节点检测到错误时,发送主动错误帧——6个显性位。这6个显性位会覆盖总线上的其他信号,强制所有节点知道出错了。

4.2.2 被动错误状态(Error Passive)

当TEC或REC超过127,但TEC还没到256时,节点进入被动错误状态。这时候节点还能收发报文,但发送错误帧时只能发被动错误帧——6个隐性位。

隐性位不会覆盖总线。所以被动错误节点发错误帧时,其他节点可能根本不知道。说白了,这个节点被“降级”了,不能再干扰总线。

经验之谈: 我在调试一个多节点系统时,发现某个节点频繁进入被动错误状态。排查后发现是它的晶振精度不够,导致位时序偏差。换了个高精度晶振,问题解决。被动错误不一定是通信问题,也可能是节点自身硬件问题。

4.2.3 总线关闭状态(Bus Off)

当TEC超过255时,节点进入总线关闭状态。这是最严重的状态——节点完全脱离总线,不发送任何帧,也不参与任何通信。

总线关闭的恢复有两种方式:

  • 自动恢复: 节点检测到128次11个连续隐性位(相当于总线空闲128次)后,自动回到主动错误状态
  • 手动恢复: 应用层通过复位或重新初始化CAN控制器来强制恢复
重要提醒: 总线关闭后,节点不能立即恢复。必须等待128个总线空闲序列。这是为了防止频繁出错的节点反复“捣乱”。我曾经见过一个项目,节点总线关闭后立即复位,结果陷入“关闭-复位-再关闭”的死循环。正确的做法是:总线关闭后,先排查根本原因,再考虑恢复。

4.3 错误处理的实际应用

了解了错误类型和状态机,咱们看看在驱动开发中怎么用。

4.3.1 错误中断处理

大多数CAN控制器都提供错误中断。我建议在驱动中注册以下中断:

// 伪代码:CAN错误中断处理
void CAN_Error_IRQHandler(void) {
    uint32_t err_flags = CAN_GetErrorFlags();
    
    if (err_flags & CAN_ERR_BIT) {
        // 位错误:检查物理层和终端电阻
        ErrorCounters.bit_err++;
    }
    if (err_flags & CAN_ERR_STUFF) {
        // 填充错误:可能是干扰或节点故障
        ErrorCounters.stuff_err++;
    }
    if (err_flags & CAN_ERR_CRC) {
        // CRC错误:数据被破坏
        ErrorCounters.crc_err++;
    }
    if (err_flags & CAN_ERR_FORM) {
        // 格式错误:协议违规
        ErrorCounters.form_err++;
    }
    if (err_flags & CAN_ERR_ACK) {
        // 应答错误:没有接收节点
        ErrorCounters.ack_err++;
    }
    
    // 检查状态变化
    if (CAN_GetErrorState() == CAN_STATE_BUS_OFF) {
        // 总线关闭处理
        CAN_HandleBusOff();
    }
}

4.3.2 错误计数器监控

我个人的习惯是:在驱动中定期读取TEC和REC,并记录到日志中。这样在调试时,可以回看错误计数器的变化趋势。

// 读取错误计数器
uint8_t tec = CAN_GetTEC();
uint8_t rec = CAN_GetREC();

// 记录到调试日志
DEBUG_LOG("CAN Error Counters - TEC: %d, REC: %d", tec, rec);

// 如果TEC快速上升,说明发送有问题
if (tec > 100) {
    // 提前预警,避免进入总线关闭
    CAN_Warning("TEC too high, check bus!");
}

4.4 本章小结

CAN的错误处理机制,说白了就是一套“犯错-记过-处罚”的体系。5种错误类型覆盖了通信中可能出现的各种问题,错误状态机则保证了单个节点的故障不会拖垮整个总线。

在驱动开发中,我的建议是:

  • 不要只处理成功发送和接收,一定要处理错误中断
  • 监控TEC和REC的变化趋势,提前预警
  • 总线关闭后,先排查原因再恢复,不要盲目复位

这套机制是CAN总线高可靠性的基石。理解了它,你才算真正入门了CAN驱动开发。