3. CAN远程帧与错误帧:远程帧作用、错误帧类型、总线错误处理机制

好,咱们接着聊。前面几章我们把CAN的数据帧和位时序都捋了一遍,今天要聊的两个东西,说实话,平时写代码时不太会天天碰见,但一旦出了问题,往往就是大坑。一个是远程帧,一个是错误帧。

远程帧这东西,我刚开始做电机控制那会儿,其实挺不理解的。明明我发个数据帧过去,对方回一个数据帧不就完了?干嘛还要搞个专门的远程帧?后来在调试一个多电机同步的案子时,才真正体会到它的价值。

3.1 远程帧的作用

远程帧,说白了就是一个「请求」。它告诉总线上的某个节点:嘿,把你那个ID的数据发给我看看。

它的格式跟数据帧很像,但有两个关键区别:

  • RTR位(远程传输请求位):数据帧里RTR=0,远程帧里RTR=1
  • 没有数据场:远程帧的DLC虽然可以写,但实际不发送数据字节

我举个例子你就明白了。假设你有一个电机驱动器,ID是0x201。你想知道它当前的转速,你可以:

// 发送远程帧,请求ID=0x201的节点回复数据
// 标准帧格式
CAN_TxHeaderTypeDef txHeader;
txHeader.StdId = 0x201;
txHeader.RTR = CAN_RTR_REMOTE;  // 远程帧
txHeader.DLC = 0;               // 远程帧DLC通常设为0
txHeader.IDE = CAN_ID_STD;

// 发送
HAL_CAN_AddTxMessage(&hcan1, &txHeader, NULL, &TxMailbox);

当ID=0x201的节点收到这个远程帧后,它就会发送一个同ID的数据帧回来。你想想看,这种一问一答的模式,在查询式通信里特别有用。

关键点:远程帧的ID决定了谁来应答。收到远程帧的节点,如果发现自己ID匹配,就发一个数据帧回去。这个数据帧的ID必须和远程帧的ID一致。

我在项目中遇到过一个问题:远程帧的DLC设置不对,导致对方节点不响应。嗯,这里要注意,有些CAN控制器对远程帧的DLC有严格要求,比如必须和对方数据帧的DLC一致。我那次就是DLC设成了8,但对方实际只发2个字节,结果总线一直报错。

3.2 远程帧在电机控制中的实战用法

电机控制里,远程帧最常见的场景就是参数查询。比如:

应用场景 远程帧ID 期望回复内容
查询电机转速 0x181 当前转速值(4字节)
查询母线电压 0x182 电压值(2字节)
查询故障状态 0x183 故障码(1字节)
查询温度 0x184 温度值(2字节)

我个人习惯是,把远程帧的ID和数据帧的ID分开规划。比如数据帧用0x200-0x2FF,远程帧用0x180-0x18F。这样一看ID就知道是请求还是数据,调试时方便很多。

小技巧:如果你用CANopen协议,远程帧其实用得不多。因为CANopen有专门的SDO和PDO机制来处理数据交换。但在自定义的简单CAN协议里,远程帧还是很香的。

3.3 错误帧的类型

聊完远程帧,咱们来说说错误帧。这东西,说白了就是CAN总线的「交警」。一旦发现谁违规了,立刻站出来喊停。

错误帧有两种:

  • 主动错误帧:由检测到错误的节点主动发出,包含6个连续的显性位
  • 被动错误帧:由处于被动错误状态的节点发出,包含6个连续的隐性位

你可能会问:这两种有什么区别?

主动错误帧的6个显性位,会强制覆盖总线上的任何信号。说白了就是「我不管你是谁,先停下来再说」。而被动错误帧的6个隐性位,就比较温和了——它只是「建议」大家停下来,如果总线上有其他节点在发显性位,那这个被动错误帧就被覆盖掉了。

为什么会这样设计?你想想看,如果一个节点已经频繁出错(进入被动错误状态),它就不能再那么强势地去打断别人了,否则整个总线都会被它搞瘫痪。

3.4 错误帧的触发条件

CAN协议定义了5种错误类型,任何一种都能触发错误帧:

  1. 位错误:发送节点在发送位时,发现总线上的电平跟自己发的不一致。比如你发了个显性位,但总线上读回来是隐性位,那就出问题了。
  2. 填充错误:连续6个相同电平出现。CAN用的是NRZ编码,每5个相同位后必须插入一个相反位。如果接收方发现连续6个相同位,那就是填充错误。
  3. CRC错误:接收方计算的CRC校验和与发送方的不一致。这个最常见,往往是数据被干扰了。
  4. 格式错误:帧格式不对。比如CRC界定符应该是隐性位,结果读到了显性位。
  5. 应答错误:发送方在ACK槽没检测到显性位。说白了就是没人应答。

我记得有一次调试电机驱动器,总线老是莫名其妙报错。用CAN分析仪抓了半天,发现是应答错误。后来一查,原来是接收节点的CAN控制器初始化时,过滤寄存器设错了,把该收的帧全滤掉了。嗯,这种坑,你不踩一次真的想不到。

3.5 总线错误处理机制

CAN总线有个很聪明的设计——错误计数器。每个节点都有两个计数器:

  • TEC(发送错误计数器):发送出错时增加
  • REC(接收错误计数器):接收出错时增加

根据这两个计数器的值,节点会进入不同的状态:

状态 条件 行为
主动错误 TEC < 128 且 REC < 128 正常通信,出错时发主动错误帧
被动错误 TEC > 127 或 REC > 127 只能发被动错误帧,且发送前要等待8个隐性位
总线关闭 TEC > 255 完全断开与总线的连接,不再参与任何通信

警告:在电机控制中,如果某个节点进入总线关闭状态,意味着它彻底失联了。这对实时性要求高的系统来说可能是灾难性的。我曾经遇到过一台伺服驱动器,因为CAN线接触不良,反复进入总线关闭状态,导致电机突然停止。后来我们在软件里加了状态监控,一旦检测到总线关闭就立刻报警并尝试恢复。

这里有个细节要注意:错误计数器不是无限增加的。当TEC超过255时,节点就进入总线关闭状态。要退出这个状态,需要软件主动复位,或者等待128个11个隐性位的总线空闲序列。

3.6 实战中的错误处理策略

在电机控制项目里,我一般会这样做:

  1. 监控错误计数器:定期读取CAN控制器的TEC和REC寄存器,一旦发现异常增长,立刻记录日志
  2. 设置错误中断:配置CAN控制器的错误中断,比如被动错误中断、总线关闭中断
  3. 自动恢复机制:如果进入总线关闭,等待一段时间后尝试重新初始化CAN控制器
  4. 冗余通信:对于关键信号(比如急停),同时使用CAN和硬线IO,确保可靠性
// 示例:检测总线关闭并自动恢复
void CAN_ErrorHandler(CAN_HandleTypeDef *hcan)
{
    // 读取错误状态
    uint32_t errorCode = HAL_CAN_GetError(hcan);
    
    if (errorCode & HAL_CAN_ERROR_BUSOFF)
    {
        // 总线关闭!记录日志
        printf("CAN总线关闭!TEC=%d, REC=%d\r\n", 
               hcan->Instance->TEC, hcan->Instance->REC);
        
        // 等待500ms后尝试恢复
        HAL_Delay(500);
        
        // 重新初始化CAN
        HAL_CAN_Stop(hcan);
        HAL_CAN_Start(hcan);
        
        // 重新加入总线
        HAL_CAN_ActivateNotification(hcan, CAN_IT_ERROR);
    }
}

避坑指南:我曾经在批量生产时发现,有些板子的CAN通信特别不稳定。排查了半天,发现是CAN终端电阻焊错了位置。记住,CAN总线两端必须各有一个120欧姆的终端电阻,而且只能有两个。多一个少一个都会出问题。

最后说一句,错误帧其实是个好东西。它保证了CAN总线在恶劣环境下的可靠性。你想想看,在电机驱动器旁边,大电流、强干扰,数据出错是难免的。但有了错误帧和错误处理机制,系统能自动检测并重发,这才是工业通信该有的样子。