2. CAN数据帧结构:标准帧与扩展帧、ID仲裁机制、数据场与CRC校验

好,咱们今天聊聊CAN数据帧。说实话,搞电机控制这么多年,我见过太多工程师把CAN帧结构背得滚瓜烂熟,可真到调试时却抓瞎。为什么?因为光背没用,你得理解它背后的设计逻辑。

CAN总线上的数据,说白了就是通过一个个“帧”来传递的。就像寄快递,你得有包裹、有地址、有内容、有签收单。CAN帧也一样,它由多个字段组成,每个字段都有特定作用。

2.1 标准帧 vs 扩展帧:11位ID和29位ID的区别

先看最直观的区别——ID长度。标准帧的ID是11位,扩展帧是29位。你可能会问:为什么要有两种?

嗯,这得从CAN协议的历史说起。早期CAN 2.0A只定义了11位ID,够用吗?对于简单系统,比如一辆车的发动机和变速箱通信,11位能提供2048个不同ID,绰绰有余。但后来汽车电子越来越复杂,一个车上可能有几十个ECU,11位ID就不够分了。于是CAN 2.0B引入了29位ID,能支持超过5亿个不同标识符。

我个人习惯是:如果项目节点数少于20个,用标准帧就够了。但如果你在做复杂的多轴伺服系统,或者车规级产品,我建议直接用扩展帧,省得以后扩展时改协议。

来看看两种帧的结构差异:

字段 标准帧(11位ID) 扩展帧(29位ID)
SOF(帧起始) 1位显性位 1位显性位
仲裁场 11位ID + RTR + IDE + r0 29位ID + SRR + IDE + 18位扩展ID + RTR + r1, r0
控制场 DLC(4位) DLC(4位)
数据场 0~8字节 0~8字节
CRC场 15位CRC + 1位CRC界定符 15位CRC + 1位CRC界定符
ACK场 2位 2位
EOF(帧结束) 7位隐性位 7位隐性位

注意看,扩展帧在仲裁场里多了SRR位和IDE位。SRR位是替代远程请求位,它总是隐性位,用来保证在标准帧和扩展帧同时发送时,标准帧优先。IDE位则用来区分是标准帧还是扩展帧——IDE=0是标准帧,IDE=1是扩展帧。

关键点:标准帧和扩展帧可以共存于同一总线!但要注意,标准帧的优先级总是高于扩展帧。因为标准帧的IDE位是显性位(0),而扩展帧的IDE位是隐性位(1),在仲裁时显性位胜出。

2.2 ID仲裁机制:谁优先级更高?

这是CAN协议最巧妙的地方。你想想看,多个节点同时发送数据时,总线怎么决定让谁先发?

答案是:靠ID仲裁。ID值越小,优先级越高。为什么?因为CAN总线是“线与”逻辑——显性位(逻辑0)会覆盖隐性位(逻辑1)。

举个例子:节点A发送ID=0x100(二进制0001 0000 0000),节点B发送ID=0x200(二进制0010 0000 0000)。从最高位开始逐位比较:

  • 第10位:A是0,B是0,相同,继续
  • 第9位:A是0,B是0,相同,继续
  • 第8位:A是0,B是1,A发送显性位0,B发送隐性位1,A胜出

所以ID=0x100的节点赢得仲裁,继续发送剩余数据。B节点检测到总线与自身发送不一致,自动退出,转为接收模式。

我在项目中遇到过一个问题:两个电机驱动器同时发送状态数据,ID分别是0x101和0x102。按理说0x101优先级更高,但实际测试时发现0x102总是先发。查了半天,原来是0x101节点的晶振偏差太大,导致位时序偏移,仲裁时出现了误判。嗯,这里要注意:仲裁机制依赖精确的位时序,晶振精度和CAN控制器配置都很重要。

实战建议:在电机控制系统中,把紧急报文(如故障码、过流保护)分配最小的ID,比如0x001~0x010。常规控制报文用0x100~0x1FF。诊断报文用0x700以上。这样能保证紧急信息总是优先发送。

2.3 数据场:0~8字节的灵活运用

数据场是CAN帧里真正装“货”的地方。标准CAN 2.0允许0~8字节,CAN FD可以到64字节(这个后面章节会细讲)。

对于电机控制,8字节怎么分配?我一般这样用:

  • 字节0~1:电机转速(16位有符号整数,单位rpm)
  • 字节2~3:电机电流(16位有符号整数,单位0.1A)
  • 字节4~5:母线电压(16位无符号整数,单位0.1V)
  • 字节6:状态标志位(故障、运行、待机等)
  • 字节7:温度(8位无符号整数,单位℃)

当然,这只是我的习惯。你也可以用不同的映射方式,但要注意字节序问题——是大端(Motorola)还是小端(Intel)?

我曾经在一个项目中吃过这个亏:上位机用大端解析,下位机用小端发送,结果转速值完全不对。从那以后,我都在协议文档里明确标注字节序,并且在代码里加注释。

// 示例:电机状态报文打包(小端模式)
// ID = 0x181,数据场8字节
void pack_motor_status(uint16_t speed, int16_t current, uint16_t voltage, uint8_t flags, uint8_t temp, uint8_t *buffer)
{
    buffer[0] = speed & 0xFF;          // 转速低字节
    buffer[1] = (speed >> 8) & 0xFF;   // 转速高字节
    buffer[2] = current & 0xFF;        // 电流低字节
    buffer[3] = (current >> 8) & 0xFF; // 电流高字节
    buffer[4] = voltage & 0xFF;        // 电压低字节
    buffer[5] = (voltage >> 8) & 0xFF; // 电压高字节
    buffer[6] = flags;                 // 状态标志
    buffer[7] = temp;                  // 温度
}

注意:DLC(数据长度码)必须与实际数据字节数一致。如果你只发送4字节数据,DLC就设为4。接收方会根据DLC判断有效数据长度。我曾经见过有人DLC设为8但只填充了前3字节,结果接收方把后面5字节的随机值当成了有效数据——这会导致严重的控制错误。

2.4 CRC校验:15位CRC的可靠性

CRC(循环冗余校验)是CAN帧的“防伪标签”。发送方计算整个帧的CRC值,接收方重新计算并比对。如果不一致,说明传输过程中有错误,接收方会发送错误帧要求重传。

CAN 2.0使用15位CRC,生成多项式是:

x¹⁵ + x¹⁴ + x¹⁰ + x⁸ + x⁷ + x⁴ + x³ + 1

这个多项式能检测出:

  • 所有单比特错误
  • 所有双比特错误
  • 所有奇数个错误
  • 所有长度≤15的突发错误
  • 99.997%的长度为16的突发错误
  • 99.998%的长度≥17的突发错误

说白了,CRC校验的可靠性非常高。在电机控制这种对实时性和可靠性要求都很高的场景,CRC能有效防止数据被篡改或损坏。

不过要注意,CRC只覆盖SOF、仲裁场、控制场和数据场,不包括CRC场本身和ACK场。另外,CRC界定符是固定的隐性位,用来分隔CRC场和ACK场。

避坑指南:我曾经在一个项目中,因为CAN控制器配置错误,导致CRC计算时包含了填充位(stuff bits)。结果明明数据正确,接收方却一直报CRC错误。查了两天才发现是控制器寄存器配置问题。所以,如果你遇到莫名其妙的CRC错误,先检查CAN控制器的配置,特别是位时序和填充位相关的寄存器。

2.5 总结一下

CAN数据帧的结构,说白了就是一套精心设计的通信协议。标准帧和扩展帧解决的是ID容量问题,ID仲裁机制解决的是总线冲突问题,数据场解决的是内容传输问题,CRC校验解决的是可靠性问题。

在电机控制中,我建议你:

  • 节点少用标准帧,节点多用扩展帧
  • 紧急报文分配小ID,常规报文分配大ID
  • 数据场按功能划分字节,注意字节序
  • CRC校验是最后一道防线,别忽视它

下一章我们会讲CAN FD——那个能传64字节数据、速度能到8Mbps的“升级版”CAN。到时候你会发现,很多在CAN 2.0里需要拆包发送的数据,在CAN FD里一帧就能搞定。