4. CAN数据链路层(下):位填充机制、CRC校验原理与计算、ACK应答机制、帧间距
好,咱们接着往下聊。上一节我们把CAN数据链路层的帧结构拆了个底朝天,这一节要啃的,是几个真正决定CAN总线“稳不稳”的核心机制。说白了,就是保证数据在嘈杂的汽车环境里,还能一个bit都不差地传过去。
4.1 位填充机制:为什么非填不可?
先问大家一个问题:CAN总线是怎么同步时钟的?靠的是电平的跳变。从显性到隐性,或者隐性到显性,接收节点就靠这个边沿来校准自己的采样点。
那问题来了:如果连续发5个、10个、甚至20个相同的bit呢?比如连续发一串“00000000”。
电平一直不变,接收节点的时钟就会慢慢漂移。漂移一多,采样点就偏了,数据就错了。我在项目里就见过这种坑——某个节点长时间不发报文,突然发一帧全是0的数据,结果接收方死活收不对。
位填充就是干这个用的。
规则很简单:发送方在连续发送5个相同电平的bit之后,必须自动插入一个相反电平的bit。
举个例子:
原始数据:11111 00000 11111
填充之后:111110 000001 111110
注意看,每5个连续的1后面,我强制塞一个0;每5个连续的0后面,强制塞一个1。这样接收方就能保证每隔最多5个bit,就能看到一个电平跳变,时钟同步就不会丢。
关键点:位填充只发生在SOF到CRC序列之间。ACK、EOF、帧间距这些字段不参与填充。为什么?因为这些字段长度固定,而且本身就有足够的跳变。
接收方收到数据后,会把填充的bit自动剔除,还原原始数据。这个过程对上层协议是完全透明的——你写代码的时候根本感觉不到它的存在。
避坑指南:我曾经调试过一个ECU,发现它偶尔会发错帧。查了半天,原来是它的位填充逻辑有bug——在连续5个相同bit之后,它插了一个相同电平的bit,而不是相反电平。这种错误在示波器上很难看出来,但接收方一定会报错。所以,如果你自己实现CAN控制器,位填充逻辑一定要反复验证。
4.2 CRC校验原理与计算:数据完整性的最后防线
位填充解决了时钟同步问题,但数据在传输过程中还是可能被干扰。比如一个显性bit被噪声打成了隐性,或者反过来。怎么办?
CAN用的是CRC-15校验。15位CRC,生成多项式是:
x¹⁵ + x¹⁴ + x¹⁰ + x⁸ + x⁷ + x⁴ + x³ + 1
写成二进制就是:1100010110011001(注意,这是15阶多项式,最高位x¹⁵隐含了)。
计算过程其实不复杂,我简单说一下:
- 把要校验的数据(SOF + 仲裁场 + 控制场 + 数据场)看作一个二进制数。
- 在这个数后面补15个0。
- 用生成多项式做模2除法(就是异或运算)。
- 得到的余数就是CRC序列,共15位。
接收方收到数据后,用同样的多项式对(数据 + CRC)做除法。如果余数为0,说明数据没问题。否则,就报CRC错误。
实际项目中:我建议你不要自己手写CRC计算逻辑。现在的MCU很多都内置了CAN控制器,硬件自动完成CRC计算。但如果你在做FPGA实现或者软件CAN(比如用SPI转CAN的芯片),那就得自己算。网上有现成的查表法代码,效率很高。
这里给一个简单的C语言CRC-15计算示例(仅供理解原理,实际项目建议用查表法):
uint16_t crc15_calc(uint8_t *data, uint16_t len) {
uint16_t crc = 0;
uint16_t poly = 0x4599; // 生成多项式,去掉最高位
uint16_t i, j;
for (i = 0; i < len; i++) {
crc ^= (data[i] << 7); // 左移7位对齐
for (j = 0; j < 8; j++) {
if (crc & 0x4000) { // 检查最高位
crc = (crc << 1) ^ poly;
} else {
crc <<= 1;
}
crc &= 0x7FFF; // 只保留低15位
}
}
return crc;
}
嗯,这里要注意:CRC计算的范围不包括位填充插入的bit。也就是说,发送方先做位填充,然后对填充前的原始数据算CRC。接收方也是先剔除填充bit,再算CRC。顺序不能搞反。
警告:CRC-15的检错能力很强,但不是100%。理论上,每2¹⁵个错误帧中,会有1个漏检。不过在实际汽车应用中,这个概率已经足够低了。如果你做的是安全关键系统(比如刹车、转向),建议在应用层再加一层校验,比如Checksum或生命信号。
4.3 ACK应答机制:确认收到,没商量
数据发出去,接收方到底收到没有?CAN用了一个非常巧妙的方法来确认——ACK Slot。
看帧结构:在CRC后面,有一个ACK场,包含2个bit:ACK Slot和ACK Delimiter。
- ACK Slot:发送方在这个bit上输出隐性电平(1)。所有接收方如果正确收到数据,就把这个bit拉成显性(0)。
- ACK Delimiter:固定为隐性电平(1),作为分隔。
说白了,就是发送方说:“我发完了,你们谁收到了?收到的话,给我个回应。” 所有正常接收的节点一起把ACK Slot拉低。发送方检测到显性电平,就知道至少有一个节点收到了。
如果发送方检测到ACK Slot是隐性电平,说明没有节点正确接收。这时候发送方会触发错误帧,然后重发。
个人经验:我曾经遇到一个奇怪的问题:某个节点发送的报文,其他节点都能收到,但发送方自己总是报ACK错误。查了很久,发现是发送节点的收发器芯片的ACK检测电路坏了,它自己拉低ACK Slot之后,读回来的却是隐性电平。换了个芯片就好了。所以,如果你遇到“发送总报错,但别人都能收到”的情况,先查硬件。
4.4 帧间距:给总线一点喘息的时间
两帧数据之间,不能无缝衔接。必须留出一段空闲时间,叫做帧间距(Interframe Space, IFS)。
帧间距由3个隐性bit组成。为什么需要这个?
- 给接收节点处理时间:接收方收到一帧数据后,需要把数据从CAN控制器搬到内存里,或者做其他处理。如果下一帧马上就来,可能来不及。
- 错误恢复:如果前一帧发生了错误,总线需要时间恢复空闲状态。
- 总线同步:连续的隐性电平可以让所有节点重新同步时钟。
帧间距之后,任何节点都可以开始发送下一帧。如果多个节点同时开始发送,就进入仲裁流程——这个我们上一节讲过了。
注意:帧间距是强制性的,所有CAN控制器都会自动插入。你写应用层代码时不需要关心它。但如果你用示波器抓波形,会发现两帧之间总有3个隐性bit的间隔。这是判断总线是否正常工作的一个标志。
4.5 小结:这些机制如何协同工作?
咱们把这一节的内容串起来,看看一帧数据从发送到接收,经历了什么:
- 发送方组装好数据帧(SOF + 仲裁场 + 控制场 + 数据场)。
- 计算CRC,附加到帧尾。
- 位填充:在SOF到CRC之间,每5个相同bit插入一个相反bit。
- 发送到总线,并在ACK Slot等待回应。
- 接收方收到数据,先剔除填充bit,再算CRC。
- 如果CRC正确,拉低ACK Slot,表示确认。
- 如果CRC错误,不拉低ACK Slot,发送方会检测到ACK错误并重发。
- 帧结束后,总线进入帧间距,等待下一帧。
你看,每一步都环环相扣。位填充保证时钟同步,CRC保证数据完整,ACK保证可靠传输,帧间距保证系统稳定。这四个机制,是CAN总线能在恶劣的汽车环境中存活30年的根本原因。
下一节,我们要讲CAN的物理层——那些电平、终端电阻、总线拓扑之类的东西。嗯,那又是另一个有趣的话题了。