设备通信与协议编程(四):校验和与 CRC 的能力边界

设备协议里常见“校验和不对”“CRC 失败”,但 CRC 不是某个唯一算法,校验通过也不等于消息可信。真正落地时,必须同时确定算法参数、覆盖范围、字节顺序和错误后的恢复行为。

1. 校验到底解决什么

校验字段用于发现传输、存储或组帧过程中的偶发错误。常见方法包括:

方法特点适用边界
XOR极易实现,检测能力有限简单遗留协议
加法和可检测部分错误,容易碰撞资源受限或既有协议
Fletcher / Adler计算便宜,能力取决于参数与报文长度软件数据完整性场景
CRC对特定错误模式有可分析的检测能力串口、总线、文件和网络帧
密码学 MAC同时验证完整性与共享密钥持有者需要抵抗主动攻击的场景

CRC 的检测能力取决于位宽、多项式和受保护消息的最大长度。设计良好的 n 位 CRC 可保证检出长度不超过 n bit 的突发错误;能否保证检出全部单比特、双比特或奇数个 bit 错误,还要检查具体多项式及码字长度。2⁻ⁿ 只能近似描述特定随机错误模型下的残余概率,不能当作所有链路和错误模式的固定漏检率。测试时应先声明目标消息长度和预期检测的错误类别,再选择多项式并注入验证。

CRC 不含秘密。攻击者修改数据后可以重新计算 CRC,因此它不是认证机制。

2. “CRC-16”信息不够

一个 CRC 实例至少由以下参数确定:

  • width(位宽,多项式隐含但单独列出更清晰);
  • 多项式(poly);
  • 初始值(init);
  • 输入、输出是否反射(refin/refout);
  • 输出异或值(xorout);
  • 校验值在线路上的字节顺序;
  • CRC 覆盖哪些字段。

两个协议都写“CRC-16”,结果仍可能完全不同。实现前应从设备协议或标准获得完整参数,并用规范测试向量核对,而不是挑一个搜索结果相似的函数。

3. Modbus RTU 的 CRC 示例

Modbus 串行线路规范定义 RTU 帧使用 CRC-16;CRC 字段在消息中先发送低字节,再发送高字节。下面函数返回数值形式的 CRC,序列化时再明确字节顺序:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public static class ModbusCrc16
{
    public static ushort Compute(ReadOnlySpan<byte> data)
    {
        ushort crc = 0xFFFF;

        foreach (byte value in data)
        {
            crc ^= value;
            for (int bit = 0; bit < 8; bit++)
            {
                bool lsb = (crc & 1) != 0;
                crc >>= 1;
                if (lsb)
                    crc ^= 0xA001;
            }
        }

        return crc;
    }

    public static void WriteLittleEndian(Span<byte> destination, ushort crc)
    {
        if (destination.Length < 2)
            throw new ArgumentException("CRC needs two bytes.", nameof(destination));

        destination[0] = (byte)crc;
        destination[1] = (byte)(crc >> 8);
    }
}

可用标准检查字符串验证核心实现:ASCII 123456789 的该 CRC 结果应为 0x4B37。这只证明参数化核心吻合;仍要用真实协议帧验证覆盖范围和线路字节序。

4. 覆盖范围要写成可执行规则

“对整帧做 CRC”仍然含糊。应明确类似规则:

1
2
3
CRC = Compute(Address || Function || Data)
CRC 字段自身不参与计算
发送顺序 = CRC low byte, CRC high byte

若协议含转义,还要说明 CRC 计算的是转义前原始字节还是线路上的转义后字节。若含长度字段,要说明长度是否参与计算。编码器和解码器应共享同一条规则,避免各自拼切片。

5. 校验失败后怎么办

校验失败意味着当前候选帧不可交付,但不一定说明端口已断开。恢复策略取决于定界方式:

  • 固定长度帧:丢弃当前候选帧,再寻找下一同步点;
  • Magic + 长度:从后续可能的 Magic 重新扫描,同时限制扫描量;
  • 静默间隔:丢弃该时间窗口内的消息;
  • TCP 长度帧:若头部已失去可信度,通常应关闭会话,避免无限错位解析。

不要把坏帧原样交给业务层“看看能不能用”,也不要在每个 CRC 错误后立刻重连。应记录计数、端口、方向、帧长度和有限的十六进制摘要,避免把整段敏感数据写入日志。

6. 表驱动与硬件实现

查表法能减少逐 bit 计算,硬件 CRC 外设则可进一步降低 CPU 占用。但它们必须与协议参数完全一致。不同 CPU 指令或硬件外设支持的多项式、反射和初值集合并不相同。

先以清晰的参考实现和测试向量建立正确性,再替换为表驱动或硬件加速版本;优化后对同一语料做逐帧差分测试。

7. 测试清单

  • 空消息、单字节、全零、全 0xFF
  • 标准检查字符串和规范示例帧;
  • 分别翻转每一个 bit,确认检测结果符合预期;
  • CRC 字节交换、覆盖范围少一个字节等常见错误;
  • 编码后再解析的往返测试;
  • 与设备抓包、厂商工具或另一独立实现交叉验证。

8. 小结

校验算法的名字不是完整契约。只有参数、覆盖范围和字节序全部一致,双方才会得到相同结果;CRC 的职责是检测偶发错误,而不是认证或加密。把参考向量固化为自动化测试,是避免设备现场反复猜算法的最低成本手段。

参考资料

Licensed under CC BY-NC-SA 4.0