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种错误类型,任何一种都能触发错误帧:
- 位错误:发送节点在发送位时,发现总线上的电平跟自己发的不一致。比如你发了个显性位,但总线上读回来是隐性位,那就出问题了。
- 填充错误:连续6个相同电平出现。CAN用的是NRZ编码,每5个相同位后必须插入一个相反位。如果接收方发现连续6个相同位,那就是填充错误。
- CRC错误:接收方计算的CRC校验和与发送方的不一致。这个最常见,往往是数据被干扰了。
- 格式错误:帧格式不对。比如CRC界定符应该是隐性位,结果读到了显性位。
- 应答错误:发送方在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 实战中的错误处理策略
在电机控制项目里,我一般会这样做:
- 监控错误计数器:定期读取CAN控制器的TEC和REC寄存器,一旦发现异常增长,立刻记录日志
- 设置错误中断:配置CAN控制器的错误中断,比如被动错误中断、总线关闭中断
- 自动恢复机制:如果进入总线关闭,等待一段时间后尝试重新初始化CAN控制器
- 冗余通信:对于关键信号(比如急停),同时使用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总线在恶劣环境下的可靠性。你想想看,在电机驱动器旁边,大电流、强干扰,数据出错是难免的。但有了错误帧和错误处理机制,系统能自动检测并重发,这才是工业通信该有的样子。