多项式除法、模2运算与生成多项式
说实话,很多工程师做CRC调试时,上来就查表、调库。一旦出问题,数学原理就成了最大的盲区。我自己带团队做通信协议栈时,遇到过整整两周定位不了CRC错——最后发现,连模2除法和多项式除法的根本区别都没搞清楚。
这一章,咱们就把CRC的数学根子刨出来。不需要多深,但一定要准。
---模2运算:CRC的算术基础
CRC加减运算,用的是模2加减。听起来玄乎?其实就是异或。
核心要点:模2加法 = 异或(XOR),不进位不借位。
举个例子:
1011 + 1101 = 0110(模2加法)1011 - 1101 = 0110(模2减法,结果相同)
你想想看,模2加法里,1+1=0,没有进位。1+0=1,0+0=0。这个特性和硬件电路的异或门完全对应,所以CRC特别适合用FPGA或ASIC实现。
咱们再看模2乘法和除法:
| 运算 | 普通算术结果 | 模2运算结果 | 备注 |
|---|---|---|---|
| 1 + 1 | 2 | 0 | 就是异或 |
| 1 + 0 | 1 | 1 | 无变化 |
| 1 × 1 | 1 | 1 | 和普通乘法一致 |
| 1 × 0 | 0 | 0 | 乘法规则不变 |
个人经验:我在项目里遇到过一位同事,CRC计算始终和硬件不一致。查了三天,最后发现是把模2减法当成了普通减法。模2减法和加法是一回事,千万别混。
多项式表示:把数据变成数学对象
为什么CRC叫循环冗余校验?因为它把二进制数据看成多项式。
一个二进制串 1011,可以映射成多项式:
1·x³ + 0·x² + 1·x¹ + 1·x⁰ = x³ + x + 1
明白了吧?二进制数中第 n 位对应多项式中 xⁿ 的系数。系数只能是0或1——这就是模2域里的元素。
⚠️ 容易混淆的地方:多项式的次数是从高位算起的。比如 1011 是4位,最高次却是3。计算位移时,一定要对齐次数。
为什么要用多项式表示?因为多项式操作的数学性质好。移位操作对应乘以 x 或除以 x,这正是硬件实现时最擅长的。
---生成多项式:CRC的灵魂
每个CRC算法都有个生成多项式,简称 G(x)。它是通信双方约定好的、固定不变的多项式。
常见生成多项式举例:
| CRC标准 | 二进制表示(高位省略) | 多项式 |
|---|---|---|
| CRC-8 | 100011101 | x⁸ + x⁴ + x³ + x² + 1 |
| CRC-16-IBM | 1000000000000101 | x¹⁶ + x¹⁵ + x² + 1 |
| CRC-32 | 100000100110000010001110110110111 | x³² + x²⁶ + x²³ + x²² + x¹⁶ + x¹² + x¹¹ + x¹⁰ + x⁸ + x⁷ + x⁵ + x⁴ + x² + x + 1 |
我建议你记住一个原则:生成多项式的次数决定校验比特数。CRC-8生成多项式次数为8,校验码也是8位;CRC-32次数32,校验码32位。
避坑指南:我曾经使用过一个国产协议栈,CRC-32计算结果始终和参考实现不对。后来发现,双方使用的生成多项式顺序不同——一个从左到右,一个从右到左。生成多项式在内存中的字节序必须统一。
多项式除法:CRC计算的核心操作
CRC计算的核心,就是用待发送数据对应的多项式 M(x),除以生成多项式 G(x),取余数作为校验码。
具体步骤:
- 将数据 M(x) 左移 r 位(r = G(x) 的次数),即乘以 xʳ
- 用 xʳ·M(x) 除以 G(x),做模2除法
- 余数 R(x) 就是CRC校验码
- CRC编码 = M(x)·xʳ + R(x)
下面是个手工计算示例:
待发送数据:1101011011(10位)
生成多项式:10011(5位,次数4)
第一步:数据左移4位 → 11010110110000
第二步:模2除法
1100001010(商)
──────────────
10011 ) 11010110110000
10011
─────
10011
10011
─────
00001 (余数前5位全是0,下一步用下一数据位)
00000
─────
00011
00000
─────
00110
00000
─────
01100
00000
─────
11000
10011
─────
10110
10011
─────
0101 ← 最终余数
第三步:CRC校验码 = 0101(4位)
最终发送码字:1101011011 0101
关键洞察:模2除法中,只要被除数当前最高位是1,就异或G(x);高位为0,就异或全0。这个过程就是硬件的移位寄存器+异或门。
我自己带新人时,最常强调的就是这个除法。手动演算一次,胜过看十遍理论。
下面是用Python实现的多项式除法函数:
def crc_remainder(data, generator):
"""
data: 二进制字符串,如 '1101011011'
generator: 二进制字符串,如 '10011'
返回:余数(二进制字符串)
"""
data_len = len(data)
gen_len = len(generator)
# 左移 r 位(补0)
dividend = data + '0' * (gen_len - 1)
dividend = list(dividend)
for i in range(data_len):
if dividend[i] == '1':
for j in range(gen_len):
dividend[i + j] = str(int(dividend[i + j]) ^ int(generator[j]))
remainder = ''.join(dividend[-(gen_len - 1):])
return remainder
# 使用示例
print(crc_remainder('1101011011', '10011'))
# 输出:0101
嗯,这里要注意:实际工程中,很多CRC实现会使用查表法、反射法等手段优化速度,但数学原理不变。
知识体系结构图
咱们把这一章的核心逻辑画成一张图,方便你回顾:
---本章核心要点
- 模2加减就是异或,是CRC的算术基础
- 多项式表示将二进制数据转化为数学形式,便于移位和除法运算
- 生成多项式是双方约定,其次数决定了校验码位数
- 多项式除法取余数,得到CRC校验码
实战提醒:手动演算一次多项式除法,能帮你省下调试CRC问题时的无数时间。我曾经在项目中遇到CRC计算和硬件实现的余数始终对不上——就是因为忘记了左移数据补0的位数要等于生成多项式次数减1。