第3章:CAN协议数据链路层——帧格式、仲裁、位填充与CRC校验
数据链路层,说白了就是CAN协议最核心的“交通规则”。它决定了消息怎么打包、怎么抢总线、怎么保证不出错。我当年刚接触CAN时,觉得帧格式就是一堆位,背下来就行。后来在实车上被坑过几次才明白——每一个位域的设计都有它的道理。
这一章我们就把数据链路层的四个关键点掰开揉碎:帧格式(标准帧 vs 扩展帧)、仲裁机制、位填充、CRC校验。嗯,内容不少,但都是硬货。
3.1 CAN帧格式:标准帧与扩展帧
CAN总线上一共跑四种帧:数据帧、远程帧、错误帧、过载帧。日常工作中打交道最多的就是数据帧。数据帧又分两种格式——标准帧(11位ID)和扩展帧(29位ID)。
我个人习惯把帧结构画成一张图记在脑子里。下面这张SVG图,就是标准帧和扩展帧的对比,你看一眼就能明白差异在哪。
3.2 仲裁机制——谁先说话?
CAN总线是多主总线,所有节点都能发消息。那问题来了:如果两个节点同时发,谁先发?
答案是:ID小的赢。这就是CAN的无损逐位仲裁机制。
你想想看,CAN总线电平是“显性(0)覆盖隐性(1)”。两个节点同时往总线上送位流,只要有一个节点送0,总线就是0。每个节点在发送每个位的同时,会回读总线电平。如果自己送的是1但读到0,就知道自己输了,立刻停止发送,转为接收。
我在项目中遇到过一个问题:某次台架测试,两个ECU同时发消息,其中一个总是发不出去。查了半天,发现它们的ID分别是0x100和0x101——就差一个位。0x100每次都能赢,0x101永远抢不到。后来我们把0x101的优先级调高了才解决。
3.3 位填充——保证同步的“小把戏”
CAN协议里有个很有意思的规则:连续发送5个相同电平后,必须插入一个相反电平。这就是位填充。
为什么要这么做?
说白了,是为了保证接收器的时钟能跟发送器保持同步。CAN用的是NRZ编码,电平不变就没有跳变沿,没有跳变沿接收器就不知道什么时候采样。如果连续发一堆0或一堆1,接收器的时钟漂移就会累积,最后采样错位。
位填充的规则很简单:
- 发送器在发送过程中,每发出5个相同位,就自动插入一个相反位
- 接收器在接收过程中,检测到5个相同位后,自动丢弃第6位(填充位)
- 填充位不参与CRC计算
举个例子:
原始数据: 11111 00000 11111
填充后: 111110 000001 111110
↑ ↑ ↑
填充位 填充位 填充位
3.4 CRC校验——数据完整性的最后防线
CAN帧里有一个15位的CRC字段(标准帧和扩展帧都是15位)。这个CRC覆盖SOF、仲裁场、控制场和数据场。
生成多项式是:
CRC-15-CAN: x¹⁵ + x¹⁴ + x¹⁰ + x⁸ + x⁷ + x⁴ + x³ + 1
换算成二进制就是:0x4599(左对齐,15位有效)。
接收器收到一帧后,会用同样的多项式对收到的数据(包括CRC本身)做除法。如果余数为0,说明数据没出错;否则就报CRC错误。
我个人的经验是:CRC能检测出所有单比特错误、双比特错误、奇数个错误,以及大部分突发错误。但要注意,CRC不是万能的——如果错误模式恰好是多项式的整数倍,CRC就检测不出来。不过这种概率极低,工程上完全够用。
- 所有≤5位的突发错误
- 所有奇数个错误
- 99.997%的更长突发错误
3.5 避坑指南——我踩过的几个坑
做CAN开发这些年,我在数据链路层上栽过几次跟头。分享出来,你们少走弯路。
- ID分配太随意:我曾经在一个项目里把制动和车窗的ID分得很近,结果制动消息偶尔被车窗消息阻塞。后来强制规定:安全相关ID必须小于0x100,舒适相关ID大于0x200。
- 位填充导致的数据长度变化:位填充会让实际发送的位数比原始数据多。如果你在计算总线负载率时忘了考虑位填充,算出来的结果会偏小。我一般按原始数据长度×1.2来估算。
- CRC计算范围搞错:标准帧的CRC不包括填充位,但包括SOF。扩展帧的CRC包括SOF、基ID、SRR、IDE、扩展ID、RTR、r1、r0、DLC和数据域。别把填充位算进去。
3.6 本章小结
数据链路层是CAN协议的“硬核”部分。标准帧和扩展帧的区别,说白了就是ID长度不同;仲裁机制靠的是“显性覆盖隐性”的物理特性;位填充是为了时钟同步;CRC校验是数据完整性的保障。
这四个知识点,你在实际开发中几乎每天都会用到。尤其是仲裁和位填充——很多奇怪的通信问题,追根溯源都能回到这两个机制上。
嗯,这一章就到这。内容不少,但都是干货。你把这些吃透了,CAN协议的数据链路层就算入门了。