CAN错误处理:错误类型、错误计数器、错误状态机、总线关闭恢复
各位同学,咱们今天聊点硬核的——CAN总线错误处理。说实话,很多开发者写CAN驱动时,只关注怎么收发报文,对错误处理这块往往一笔带过。但我在实际项目中吃过亏,有一次现场总线莫名其妙就挂了,查了两天才发现是错误状态机没处理好。嗯,这节课咱们就把这块彻底讲透。
4.1 错误类型:CAN总线到底会出哪些错?
CAN协议定义了5种错误类型。你想想看,这5种错误覆盖了从位电平到报文格式的所有异常情况。我习惯把它们分成两类:物理层错误和协议层错误。
| 错误类型 | 触发条件 | 检测节点 | 个人经验 |
|---|---|---|---|
| 位错误 | 发送节点监控总线电平,发现与自己发送的不一致 | 发送节点 | 最常见,线缆松动时必出 |
| 填充错误 | 连续6个相同位出现,违反位填充规则 | 所有节点 | 时钟漂移严重的节点容易触发 |
| CRC错误 | 接收节点计算的CRC与发送的CRC不匹配 | 接收节点 | 电磁干扰强时频发 |
| 格式错误 | 帧格式中固定位域出现非法电平 | 所有节点 | 波特率配置不一致时必现 |
| 应答错误 | 发送节点在ACK槽未检测到显性位 | 发送节点 | 只有一个节点在线时出现 |
4.2 错误计数器:两个计数器管全局
每个CAN节点内部都有两个硬件计数器:TEC(发送错误计数器)和REC(接收错误计数器)。它们怎么涨怎么跌,协议规定得很细。说白了,就是一套奖惩机制——成功就减,失败就加。
具体规则是这样的:
- 发送成功:TEC减1(前提是TEC > 0)
- 接收成功:REC减1(前提是REC > 0)
- 发送错误:TEC加8
- 接收错误:REC加1
- 主动错误标志发出后:TEC再加8
为什么发送错误加8,接收错误只加1?因为发送节点对总线负有主要责任。你发错了,影响的是整个网络。我在项目中遇到过一种情况:某个节点TEC涨到128以上,直接进入被动错误状态,但它还在不停重发,结果REC也跟着涨。这就是典型的"错误风暴"。
4.3 错误状态机:三个状态决定节点命运
CAN节点根据TEC和REC的值,会处于三种状态之一。我画了一张状态机图,大家一看就明白。
这三个状态的行为差异很大。我重点说一下被动错误状态——这是最容易出坑的地方。被动错误节点发送错误标志时,用的是6个隐性位。你想想看,隐性位会被显性位覆盖,所以其他主动错误节点根本感知不到这个错误标志。这就导致一个问题:被动错误节点发错了,别人不知道,它自己还得继续重发。
4.4 总线关闭恢复:两种策略怎么选?
当TEC超过255,节点就进入总线关闭状态。这时候节点完全脱离总线,不发送也不接收。恢复方式有两种:
- 自动恢复:检测到128次连续11个隐性位(即128个总线空闲序列),自动回到主动错误状态。这是硬件自动完成的,不需要软件干预。
- 手动恢复:通过软件复位CAN控制器,强制退出总线关闭状态。但要注意,复位后错误计数器清零,之前的历史错误信息也丢了。
我个人建议:能用自动恢复就别用手动恢复。为什么?手动恢复相当于"掩耳盗铃"——你把错误计数器清零了,但物理层的问题还在,一恢复马上又关闭。我在一个车载项目中就犯过这个错,手动恢复后节点反复关闭,最后查出来是CAN收发器的共模电压出了问题。
4.5 实战:从站节点的错误处理代码
下面给出一段从站节点的错误处理代码框架。这段代码我在多个项目中用过,核心逻辑就是:监控错误状态,分级处理,避免死循环。
/* CAN错误状态监控任务 */
void CAN_ErrorMonitorTask(void)
{
uint8_t error_state;
uint8_t tec, rec;
while(1)
{
/* 读取当前错误状态 */
error_state = CAN_GetErrorState();
tec = CAN_GetTEC();
rec = CAN_GetREC();
switch(error_state)
{
case CAN_ERROR_ACTIVE:
/* 正常状态,计数器小于128 */
if(tec > 96 || rec > 96)
{
/* 接近被动错误,提前预警 */
CAN_SendWarningMessage(tec, rec);
}
break;
case CAN_ERROR_PASSIVE:
/* 被动错误,需要降级处理 */
/* 停止发送非关键报文 */
CAN_StopNonCriticalMessages();
/* 记录错误日志 */
CAN_LogError("Enter Passive Error", tec, rec);
break;
case CAN_ERROR_BUSOFF:
/* 总线关闭,等待自动恢复 */
CAN_StopAllTransmission();
CAN_LogError("Bus Off Detected", tec, rec);
/* 等待自动恢复,超时则报警 */
for(int i = 0; i < 1000; i++)
{
if(CAN_GetErrorState() == CAN_ERROR_ACTIVE)
{
CAN_LogInfo("Bus Off Recovered");
break;
}
delay_ms(10);
}
/* 超时未恢复,触发硬件报警 */
if(CAN_GetErrorState() != CAN_ERROR_ACTIVE)
{
CAN_TriggerHardwareAlarm();
}
break;
}
delay_ms(100); /* 100ms轮询一次 */
}
}
4.6 调试技巧:用错误计数器定位问题
最后分享几个调试技巧。这些是我这些年踩坑踩出来的:
- 看趋势不看瞬时值:错误计数器偶尔跳一下很正常,但如果持续上涨,说明有问题。
- 区分发送侧和接收侧:TEC涨得快,查发送电路和终端电阻;REC涨得快,查接收电路和干扰源。
- 记录错误发生时的报文ID:如果某个特定ID的报文总是触发错误,可能是那个节点的时钟有问题。
- 用CAN示波器看位时序:错误计数器异常时,用示波器抓一下CAN_H和CAN_L的差分信号,看看位宽度是否一致。
嗯,CAN错误处理这块内容就讲到这里。记住一句话:错误处理不是可有可无的附加功能,它是CAN节点稳定运行的基石。下节课咱们接着聊CANopen协议栈的移植和配置,到时候会用到今天讲的状态机知识。