对接硬件协议时最常遇到的一幕:
对方说「用 CRC16 校验」,你写了段代码算出来是 0x8F3A,
对方收到却是 0x3A8F。或者干脆完全是另一个数。
问题出在:CRC 不是一个算法,而是一族算法。 「CRC16」只说明了结果有 16 位,其余什么都没说。
四个参数,缺一不可
一套完整的 CRC 参数包含四项:
- poly(多项式):核心参数,决定移位时异或什么值
- init(初值):寄存器的起始值,常见 0x0000 或 0xFFFF
- refin / refout(反射):数据位序是否倒置
- xorout(结果异或):最后再异或一次,常见 0x0000 或 0xFFFF
任意一项不同,结果就完全不同。所以「CRC16/MODBUS」和「CRC16/CCITT」虽然都是 16 位, 算出来毫无关系。
多项式怎么写
CRC-7 用的多项式是 x⁷ + x³ + 1,代码里却写成 0x09。为什么?
把系数按幂次排列,从 x⁷ 到 x⁰:
- x⁷ 的系数是 1
- x⁶、x⁵、x⁴ 的系数是 0
- x³ 的系数是 1
- x²、x¹ 的系数是 0
- x⁰ 的系数是 1
写成二进制是 10001001,也就是 9 位。但最高位(x⁷)是隐含的——
只要看到「7 位 CRC」,就知道最高位一定是 1,不需要存。所以实际只存低 8 位:
00001001 = 0x09。
这和浮点数的隐含前导 1 是同一个思路:把必然为 1 的那一位省掉,多留一位精度。
反射(reflect)到底在反什么
串行通信里,数据经常是低位先发(LSB first)。 但 CRC 的标准算法是按高位先处理的(MSB first)。
如果发送端低位先发,接收端却按高位先算,位序就完全对不上。
解法是把每个字节的 8 个位倒过来再算,这就是 refin。
refout 同理,只是作用在最终结果上。
常见组合有两种:两个都 false(如 CRC-16/CCITT-FALSE),
或者两个都 true(如 MODBUS、CRC-32)。
一个实用判断:如果对方算出来的结果和你的字节序正好相反 (比如 0x8F3A 对 0x3A8F),大概率是反射设置不同,而不是算法写错了。
怎么验证自己算得对
这个行业有一个约定俗成的测试串:字符串 "123456789"
(ASCII 是 31 32 33 ... 39,共 9 个字节)。
每套标准参数都有一份公认的校验值,叫 check:
- CRC-7/MMC →
0x75 - CRC-8 →
0xF4 - CRC-16/MODBUS →
0x4B37 - CRC-16/CCITT-FALSE →
0x29B1 - CRC-32 →
0xCBF43926 - CRC-32C →
0xE3069283
自己写的 CRC 实现,先拿这个测试串跑一遍。对上了,说明算法和参数都正确; 对不上,就不用往下查了,先修算法。
工具里每套预设都内置了这个校验,点一下就能看到当前参数算出的 check 值, 以及是否和标准一致。
CRC-7 用在哪
SD 卡和 eMMC 的命令帧用 CRC-7 校验。一帧命令是 48 位,最后 8 位里, 低 1 位是结束位,高 7 位就是 CRC。
有个细节容易忽略:初始化阶段(CMD0、CMD8)的 CRC 是固定值 (0x95 和 0x87),因为此时卡还没进入可校验状态。从 CMD8 之后才开始真正校验。
如果你在写 SD 卡驱动,握手阶段一直失败,先确认这两个固定 CRC 有没有写对。
对接时该问什么
下次对方说「用 CRC 校验」,别只问位数,把这四个问题问全:
- 多项式是多少?(给出十六进制值)
- 初值是多少?
- 输入和输出需要反射吗?
- 结果要异或什么值?
再加一句「能不能给我一组测试数据和对结果」,基本上就不会再有争议了。
顺带一句:CRC 不是加密
CRC 是线性的,构造碰撞非常容易——理论上可以让任意数据算出指定的 CRC 值。 它的作用只有检测传输错误,不能防篡改。
需要防篡改就用 HMAC 或数字签名,别拿 CRC 当校验和以外的用途。