4、CAN FD数据场与DLC:长度、编码与CRC

各位好,我是老赵。在CAN FD的报文结构里,数据场和DLC这块我当年刚接触时也绕了不少弯子。说白了,DLC(Data Length Code)就是编码器跟你「商量」帧里到底装了多少字节数据。经典CAN里DLC只有0-8,到了CAN FD时代,你想想看,一个帧最多能塞64字节,DLC的编码方式可就完全不一样了。

4.1 DLC编码规则——从4位码到真实长度

DLC在CAN FD帧里仍然是4位,但它的映射表扩展了。经典CAN里DLC=0到8对应数据长度0到8字节,9到15也用但算做8字节。CAN FD则不一样,我见过不少新手直接拿DLC值当字节数读,结果解析出来的数据全乱套了。

下面这张表是我平时查得最勤的,建议你存下来。

DLC值(4位二进制) 经典CAN数据长度 CAN FD数据长度
0 – 8 (0000 – 1000) 0 – 8 字节 0 – 8 字节
9 (1001) 8 字节(非标) 12 字节
10 (1010) 8 字节 16 字节
11 (1011) 8 字节 20 字节
12 (1100) 8 字节 24 字节
13 (1101) 8 字节 32 字节
14 (1110) 8 字节 48 字节
15 (1111) 8 字节 64 字节

核心记忆口诀:DLC ≤ 8 时,长度 = DLC;DLC > 8 时,查表跳变:9→12,10→16,11→20,12→24,13→32,14→48,15→64。

4.2 可变数据场长度——为什么这么做?

嗯,这里要注意。CAN FD允许数据场长度在0到64字节之间以非2的幂次步进(12、20、48等)变化,设计上其实是为了平衡带宽和填充开销。我参与过一个车载诊断项目,ECU每次上报的DTC数据恰好22字节。经典CAN得拆成3帧发,CAN FD一帧搞定,DLC设成12(对应20字节)不够,只能选13(32字节)浪费10字节,但你想想看,省掉的帧头开销反而赚了。

可变长度的好处很明显:

  • 减少协议开销:帧头、帧尾、CRC等固定开销被更大有效载荷分摊
  • 匹配真实数据量:不用为了凑8字节而填充无意义数据
  • 提高实时性:一次传输更多数据,总线访问次数减少

我曾经踩过的坑:在配置CAN FD控制器时,DLC写的是12(对应24字节),但发送缓冲区的实际数据只有22字节,剩下2字节是随机值。接收方解析时发现不对劲——后来规范里其实允许数据场比DLC指示的短吗?不行。CAN FD要求数据场长度必须严格等于DLC映射的长度,不足的部分你得主动填充(一般填0x00或0x55)。不填的话,控制器会报填充错误,帧根本发不出去。

4.3 CRC计算——变长的校验策略

数据场长了,CRC也得跟着变。经典CAN用一个15位CRC保护整个帧(包括数据场),CAN FD引入了两个CRC段:

  • CRC-17:当数据场长度 ≤ 16字节时,使用17位CRC
  • CRC-21:当数据场长度 > 16字节时,使用21位CRC

为什么这么分?我个人的理解是:CRC的纠错能力与多项式长度正相关,短帧用长CRC浪费带宽,长帧用短CRC风险高。这个切换是自动的,控制器硬件根据DLC的值决定用哪个多项式。你只需要在配置寄存器里开启CAN FD模式,硬件自己处理。

小技巧:调试时可以抓一段CAN FD总线波形,手动验证CRC。把数据场、位填充后的比特流全部提取出来,用多项式x¹⁷ + x¹⁶ + x¹⁴ + x¹¹ + x⁶ + x⁵ + x² + 1(CRC-17)或x²¹ + x¹⁹ + x¹⁷ + x¹⁵ + x¹¹ + x⁵ + x¹ + 1(CRC-21)做除法,余数应该等于帧里的CRC段。

4.3.1 一个CRC计算的简单示例

假设我们要发送一个CAN FD帧,数据场长度为12字节(DLC=9)。此时硬件采用CRC-17。我写了个伪代码帮你理解计算过程:

// 输入:数据场 payload(12字节),以及帧中其他固定位(SOF、ID、控制场等)
// 输出:17位 CRC 校验值

uint32_t crc17_calculate(const uint8_t* data, uint32_t len) {
    uint32_t poly = 0x3685B;   // x^17 + x^16 + x^14 + x^11 + x^6 + x^5 + x^2 + 1
    uint32_t crc = 0;          // 初始值为0
    for (uint32_t i = 0; i < len; i++) {
        crc ^= (data[i] << 9); // 左移17-8=9位
        for (int bit = 0; bit < 8; bit++) {
            if (crc & 0x10000) { // 检查最高位(17位)
                crc = (crc << 1) ^ poly;
            } else {
                crc <<= 1;
            }
            crc &= 0x1FFFF;     // 只保留17位
        }
    }
    return crc & 0x1FFFF;
}

注意:实际CAN FD CRC计算的范围不仅包括数据场,还包括SOF、标识符、控制场(含DLC、ESI、BRS等)以及填充位后的比特流。不同CAN FD控制器在填充位处理上可能有细微差别,我曾经遇到过两个控制器厂商的硬件在CRC段含填充位的算法上不一致,导致跨平台通信失败。建议用官方提供的参考实现做交叉验证。

4.4 知识体系总览

下面这张图把DLC、可变数据场长度和CRC计算串在了一起,你可以把它当作本章的知识地图。

CAN FD 数据场与DLC · 知识结构 CAN FD 数据场 DLC 编码 4位 → 0~64字节 可变数据场长度 12/16/20/24/32/48/64 CRC 计算 CRC-17 / CRC-21 DLC ≤ 8 → 长度=DLC DLC > 8 → 查表 非2的幂次步进 · 减少填充开销 ≤16字节 → CRC-17 >16字节 → CRC-21 硬件自动根据DLC选择CRC多项式,但软件须保证填充位一致性 不同CAN FD控制器在填充位CRC算法上可能有差异,建议交叉验证 —— 数据长度码(DLC) → 映射实际字节数 → 决定CRC位数 ——

你看,整个流程其实很顺:先解析DLC得到实际数据长度,然后根据长度决定用CRC-17还是CRC-21,最后硬件自动完成校验。我当年在一款国产MCU上移植CAN FD协议栈时,发现它的CRC计算引擎在填充位的参与方式上跟标准不一样,折腾了两天才定位到问题。所以,拿到新控制器后,一定先用官方的参考帧做一致性测试。

4.5 配置参数小结

这一章的内容在实际解码配置时,你需要关心的寄存器或配置项如下:

配置项 典型值/范围 说明
DLC 0~15 4位码,映射到0~64字节
数据场长度 0,1,2,...,64 (限DLC映射值) 发送时填充至精确长度
CRC配置 ISO 11898-1:2015 硬件自动选择17/21位
填充位策略 固定填充位(0或1) 各个控制器需统一

我个人习惯:在工程中把所有DLC映射表写成一个常量数组,每次解析时直接查表,避免现场计算。同时写一个单元测试,用已知的CAN FD样本帧验证DLC->长度的映射是否正确。这样能提前发现MCU库函数的潜在bug。嗯,这招救过我两次了。