前段时间组里上下位机联调。协议文档看起来挺完整:帧头、命令字、数据、帧尾带 CRC16 校验。两边各自实现、各自自测,全都通过;一联调,各种 bug 排着队来。查了好几天,最后发现元凶平平无奇:两边都实现了 CRC16,但从头到尾没人问过一句——是哪个 CRC16。
先说结论:CRC 不是"一个算法",是一个算法族。“加个 CRC 校验"这句话的规格含量,跟"加点辣"差不多——都是辣,川湘黔滇各不一样。
翻车姿势
CRC 变体没对齐时的症状,通常是这三种:
- 全帧校验失败。发什么错什么,怀疑链从串口时序、大小端、接地一路排查到换线换板子,通信工程师的经典受难路线。其实链路好得很,两边算的压根不是同一个数。
- 为联调把校验旁路。有人图快,把"校验失败"改成"打条日志继续跑”,业务先跑通了,恢复和补测永远排在下个迭代。校验从此形同虚设,比没有还危险——它给人安全感。
- 偶发"漏检"玄学。压力测试几百万帧,偶尔一两帧校验通过但数据是错的。于是开始研究 CRC 漏检率、查电磁干扰、上磁环。其实是两套算法在 65536 个值里随缘撞上了——不是漏检,是压根没检。
CRC 是一个算法族
CRC 除了"用哪个多项式做除法"这个自由度,除法前后还有一串参数。业界通用的描述模型叫 Rocksoft 模型,出自 Ross Williams 1993 年那篇 A Painless Guide to CRC Error Detection Algorithms,一共五个参数:
- width:位宽
- poly:多项式
- init:寄存器初值
- refin / refout:输入、输出是否按位反转
- xorout:结果异或值
“CRC16” 只锁定了第一个参数,剩下四个全开放。同一个多项式能配出多少种不同结果?看下表(check 是官方测试向量:9 字节 ASCII 字符串 "123456789" 的校验值):
| 名字 | poly | init | refin/refout | xorout | check |
|---|---|---|---|---|---|
| CRC-16/XMODEM | 0x1021 | 0x0000 | 否/否 | 0x0000 | 0x31C3 |
| CRC-16/CCITT-FALSE | 0x1021 | 0xFFFF | 否/否 | 0x0000 | 0x29B1 |
| CRC-16/KERMIT | 0x1021 | 0x0000 | 是/是 | 0x0000 | 0x2189 |
| CRC-16/ARC | 0x8005 | 0x0000 | 是/是 | 0x0000 | 0xBB3D |
| CRC-16/MODBUS | 0x8005 | 0xFFFF | 是/是 | 0x0000 | 0x4B37 |
| CRC-32/ISO-HDLC | 0x04C11DB7 | 0xFFFFFFFF | 是/是 | 0xFFFFFFFF | 0xCBF43926 |
前三个共享多项式 0x1021,check 是三个毫不相干的数;中间两个只差一个 init(0x0000 与 0xFFFF),结果就分道扬镳。网上抄的 crc16.c,十份里能抄出五种参数组合,而且多半不写注释。
顺带一提,第二行的 CCITT-FALSE 在标准目录里的正式名叫 CRC-16/IBM-3740——连"叫什么名字"都有历史官司,更说明按参数说话,别按名字说话。
业界早有正解:CRC 目录
这个问题业界早就定过标准答案。CRC 参数目录(Greg Cook 维护,源头就是 Williams 论文附录那张表)收录了几十种变体:8 位宽 20 个、16 位宽 31 个、32 位宽 12 个,更宽的还有一整页。每一行就是一个变体的完整身份:五参数 + check + residue(余数,第二个官方测试向量)。
地址:https://reveng.sourceforge.io/crc-catalogue/,上面那张表就是从目录里挑的常用款。
它的正确用法不是扔进收藏夹吃灰,而是联调开场仪式:双方各自用自己的实现算 "123456789" 的 CRC,报一个数。数对上了,参数就对上了;对不上,当场翻目录,十分钟收工。
医疗器械行业是怎么对待"完整性"的
顺着"校验"这个词往外多想一层。CRC 校验的完整性,防的是信道误码——数据在传输里被噪声翻了几位,没有对手,纯属物理。它防不了篡改:攻击者改完数据,把 CRC 重算一遍就是了。防篡改要靠密码学哈希和签名,那是另一种威胁模型。
分清这两件事,在规范行业是写在纸上的。医疗器械软件的生命周期里就有明确的哈希要求:FDA 2023 年定稿的上市前网络安全指南(Cybersecurity in Medical Devices,有 FD&C Act 524B 条款撑腰,对 cyber device 是硬要求)要求设备的软件更新必须验证完整性与真实性,工程上的落法就是升级包带加密哈希或签名,设备验算通过才允许写入。而软件生命周期标准 IEC 62304 本身不指定算法,但它的配置管理和发布验证环节,实践中同样靠哈希把"这版就是这版"锁死。
对照之下有点扎心:规范行业连"用 SHA-256 还是 SHA-1"都要写进文件,而我们的协议文档里连 CRC 初值都没问过一句。CRC 和哈希防的东西不同,但"算法 + 参数 + 测试向量必须白纸黑字"这个道理是同一个。
避坑清单
- 协议文档里,校验字段不许只写算法名。 标准写法长这样:
CRC-16/MODBUS:poly=0x8005 init=0xFFFF refin=true refout=true xorout=0x0000
check("123456789") = 0x4B37;结果低字节在前;
覆盖范围 SOF..DATA(不含 CRC 自身);每帧重置初值。
名字用目录里的规范名,再补两个工程细节:结果字节序(低字节在前还是高字节在前)和覆盖范围(从哪算到哪、初值是否跨帧累积)——这两处是参数对齐之后最常见的二次翻车点。
2. 联调第一件事:交换 check 值。 双方各自跑 "123456789",报数。成本一分钟的仪式,省掉几天受难。
3. 评审时看到没有参数注释的 crc16.c,直接打回。 实现无所谓查表还是位循环,参数必须写明。
4. 上位机别"找个库调一下"就完事。 crcmod、System.IO.Hashing 这类库要么显式传参数、要么显式选变体,选哪个得和下位机对过。
5. 分清威胁模型。 防误码用 CRC,防篡改用哈希/签名。Modbus 用 CRC 防线噪,医疗器械固件升级用签名防攻击者,各自有各自的岗位,别混岗。
写在最后
协议文档里每一个和"校验"有关的名词,都应该能落到一个可执行的测试向量上;落不下去的就不是规格,是社交辞令。“加个 CRC"和"吃辣"一样——不上参数,全靠缘分。
备注:文中参数与 check 值引自 CRC 目录;医疗器械部分参考 FDA 上市前网络安全指南。