CRC 编码过程:编码算法、模2除法、余数计算

好了,咱们直接进入正题。CRC 编码的核心说白了就三步:补零 → 模2除 → 拿余数。别看公式唬人,当年我第一回看教材也是晕的,后来自己动手写了几行代码才明白——不过就是二进制里的“异或”操作。

编码算法:到底在做什么?

发端数据是一个二进制流,我们把它看成多项式 M(x)。比如数据 1011 就是 x³ + x + 1。收发双方事先约定好一个生成多项式 G(x),比如 1001(即 x³ + 1)。
编码就是想给 M(x) 后面附上 r 位余数,使得整个多项式能被 G(x) 整除。这里的 r 就是 G(x) 的最高次次数。

具体流程

  1. 原数据 M 左移 r 位(就是末尾补 r 个0),得到 M'
  2. 用 M' 对 G(x) 做模2除法,得到余数 R
  3. 最终发送的码字 = M' + R(注意这里的加法是直接拼接,不是模2加)
我个人的习惯: 写代码时先把数据转成整数,左移后直接拿 Python 的 ^ 一位一位除。下面会给你演示。

模2除法:和普通除法差在哪?

很多人卡在这里。说白了,模2除法做的是按位异或,没有进位,也没有借位。你想想看,普通二进制除法里我们要“够减”才商1,然后做减法;模2除法里只要当前位的最高位是1,就商1,然后做异或。

对比项 普通二进制除法 模2除法(CRC)
减法 带借位的二进制减 按位异或(无进位/借位)
商位判定 被除数>=除数才商1 被除数最高位=1就商1
结果 商+余数(带权重) 余数长度固定(r位)

举个例子,数据 1101,生成多项式 1011(r=3),左移补3个0得到 1101000。做模2除法:

      1011 (商,但我们一般不关心)
     -----
1011 ) 1101000
       1011
       ----
        0110  (异或结果)
        0000
        ----
         1100
         1011
         ----
          1110
          1011
          ----
           101  ← 余数 3位(前面补0成 0101?注意这里不足3位补0到r位)

实际余数是 101(因为最高位已经是第一位了,长度刚好3)。所以编码后的码字是 1101 101。

我曾经踩过的坑: 写模拟代码时,忘了把余数补足 r 位。多项式 1001 余数可能是 11(2位),你得左边补0成 011 再拼回去。否则接收端怎么除都除不尽,白白怀疑人生。

余数计算的本质

其实余数 R(x) 就是 M(x)·x^r mod G(x)。这个模运算在 GF(2) 域里进行,完全等价于模2除法。为什么这样设计?因为只要余数 R 存在,M' + R 就能被 G(x) 整除,接收端一除,余数为0就认为没出错。

来个代码看看,Python 里实现起来非常直接:

def crc_encode(data_bits, poly):
    # data_bits 是整数,比如 0b1011
    # poly 是生成多项式整数,比如 0b1001
    # 第一步:计算 r 位
    r = poly.bit_length() - 1
    # 第二步:数据左移 r 位
    m_prime = data_bits << r
    # 第三步:模2除法求余数
    # 用长除法思想:当 m_prime 的位数 >= poly 的位数时,异或
    temp = m_prime
    while temp.bit_length() >= poly.bit_length():
        # 取最高位对齐
        shift = temp.bit_length() - poly.bit_length()
        temp ^= (poly << shift)
    # 此时 temp 就是余数,长度 <= r
    remainder = temp
    # 第四步:返回编码后的码字(整数)
    codeword = (data_bits << r) | remainder
    return codeword, remainder

# 试试
cw, rem = crc_encode(0b1011, 0b1001)
print(f"编码码字: {bin(cw)}")   # 0b101111 对应 1011 后面加 11?不对,这里 poly=1001 r=3,余数应该是3位

注意上面例子里 poly=0b1001 即 1001(x³+1),r=3。数据 1011 左移3位得到 1011000,模2除后余数应该是 110 还是 011?你可以自己验算。我故意留了个小悬念——实际跑一下就知道,正确答案是余数 110,所以码字 1011 110。要是余数不足3位,就像前面说的,左边补0。

核心总结: CRC 编码就是用模2除法代替普通除法,算出余数补在后面。整个算法里没有乘法、没有进位,只有异或和移位。你只要记住“异或就是模2加,也是模2减”就够了。

流程图:CRC 编码三步走

下面这张图把整个流程串起来,建议你盯着看两分钟,再去写代码。

CRC 编码流程图 原始数据多项式 M(x) 左移 r 位(补 r 个0)→ M'(x) 用 G(x) 对 M'(x) 做模2除法 得到余数 R(x)(r 位) 生成多项式 G(x) 最高次 r 例如: 0b1001 r=3 编码码字 T(x) = M'(x) + R(x) (直接拼接)
我的建议: 第一次学可以拿纸笔手动算一遍,数据用 1011,多项式用 1001。然后拿上面的 Python 代码验证余数。这样一遍下来,你再也不会把“补0个数”和“余数不足”搞混。

嗯,到这里 CRC 编码的三大块就讲透了。我个人觉得最难理解的是模2除法为什么跟普通除法长得不一样——其实你只要记住“异或替代减法,没有进位”,然后用代码模拟一次,一切都通了。早些年我调蓝牙基带协议的 CRC 时,就是因为忘了补足余数位,折腾了一下午,后来干脆写了个打印中间过程的调试函数,才找到问题。所以强烈建议你:动手敲代码,别光看。