4. CAN错误处理机制:5种错误类型与错误状态机
CAN总线的可靠性,说白了就靠这套错误处理机制撑着。我刚开始接触CAN驱动时,总觉得这部分理论性太强,直到在产线上亲眼看到一台设备因为错误累积直接“离线”,才意识到这东西有多重要。
这一章咱们就把CAN的错误处理机制拆开揉碎。先看5种错误类型分别是什么,再看节点怎么在这些错误中“活着”或“死掉”——也就是错误状态机。
4.1 五种错误类型
CAN协议定义了5种错误。任何节点在发送或接收时,只要检测到其中一种,就会立即发出错误帧。注意,是立即,不等当前报文发完。
| 错误类型 | 检测时机 | 检测节点 | 触发条件 |
|---|---|---|---|
| 位错误 | 发送时 | 发送节点 | 发送的位电平与总线实际电平不一致 |
| 填充错误 | 接收时 | 所有接收节点 | 连续6个相同电平(违反位填充规则) |
| CRC错误 | 接收时 | 所有接收节点 | 计算出的CRC与发送端CRC不匹配 |
| 格式错误 | 接收时 | 所有接收节点 | 帧格式中固定位域出现非法电平 |
| 应答错误 | 发送时 | 发送节点 | ACK槽位未收到显性位(无人应答) |
4.1.1 位错误(Bit Error)
这个错误只有发送节点能检测到。发送节点往总线上放一个位,同时回读总线电平。如果读回来的和自己发的不一样,就报位错误。
举个例子:你发的是显性位(0),但总线上读回来是隐性位(1)。那说明有其他节点同时在发,或者总线被拉高了。
4.1.2 填充错误(Stuff Error)
CAN协议规定:连续发送5个相同位后,必须插入一个相反位。这叫位填充规则。接收节点如果发现连续6个相同电平,就报填充错误。
为什么会这样?说白了是为了保证时钟同步。没有位填充,长时间没有跳变沿,接收节点的时钟就会漂移。
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)。节点根据这两个计数器的值,处于三种状态之一。
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控制器来强制恢复
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驱动开发。