生成多项式的代数机制:从错误检测问题到异或实现

很多工程里的“校验”看起来只是几个异或、一次移位、一个固定常数表。可一旦追问为什么这样做能发现错误,为什么某些常数比另一些常数更好,为什么同一个多项式还会有正向、反向、初值、最终异或这些变体,表面上的位操作就会迅速露出背后的代数结构。

这篇文章从一个更基础的问题开始:如果一串数据在传输或存储过程中被改坏了,接收端凭什么只看一小段校验字段就能判断“这不是原来的东西”?生成多项式正是为这个问题服务的。它不是代码里的神秘常数,而是一条代数规则:规定哪些比特串是合法码字,哪些扰动会被排除在合法集合之外。

沿着这条线往下走,有限域、位串多项式、整除、余数、移位寄存器和异或会自然连在一起。最终会看到:CRC 这类循环校验码并不是“恰好用了异或”,而是因为它选择在 $GF(2)$ 上做多项式运算,所以加法、减法和消项都必然落到异或上。


1 生成多项式要解决什么问题

错误检测为什么需要一点代数?可以先从最朴素的奇偶校验看起:把一串比特里 1 的个数压缩成一个校验位。它很便宜,也很直观,但它只能保证发现奇数个比特翻转。两个比特同时翻转时,奇偶性不变,错误就会漏掉。

这说明校验码真正要解决的问题不是“算出一个摘要”这么简单,而是要把原始消息嵌入一个更大的合法集合里。发送端发出的不只是消息本身,而是“消息 + 校验字段”构成的码字;接收端要判断收到的比特串是否仍然属于这组合法码字。

生成多项式解决的就是“如何定义合法码字集合”这个问题。循环校验码的思路是:不要只数 1 的个数,而是把整段比特看成一个多项式,再用一个固定的生成多项式 $g(x)$ 去约束它。只要接收端拿到的整体不能被 $g(x)$ 整除,就说明传输或存储过程中发生了错误。

这种约束有两个工程好处。第一,它把检错问题转化成整除和余数问题,分析起来足够明确;第二,它仍然只需要非常便宜的位运算,因为在二进制有限域里,加法没有进位,减法和加法相同,最后会自然落到异或。

下面这张图先把这个过程压成一条直观路径:原始比特串先被解释成 $GF(2)$ 上的多项式,再通过生成多项式求余,最后把余数作为校验字段并入发送码字。后面的章节会逐步拆开这条路径里的每个环节。

从比特串到多项式再到余数校验的概念路径


2 生成多项式如何定义合法码字集合

生成多项式到底生成了什么?$g(x)$ 是循环校验码的核心参数。它不是从某条消息临时推导出来的,也不是随手选的魔法常数,而是在设计编码方案或制定协议时预先选定的一条代数约束。

在一个具体系统里,发送端和接收端必须使用同一个 $g(x)$。否则,发送端构造出来的合法码字,接收端可能会判成非法;接收端认为合法的码字,发送端也未必真的按同一套规则产生。至于应该选哪个 $g(x)$,要看校验位宽、最大消息长度、目标错误模型和兼容要求。

2.1 从消息到合法码字

设原始消息多项式为 $m(x)$,生成多项式的次数为 $r$。如果要附加 $r$ 个校验比特,第一步通常是把消息左移 $r$ 位,也就是乘以 $x^r$:

\[x^r m(x)\]

然后用 $g(x)$ 去除它,得到余数 $p(x)$:

\[p(x) = x^r m(x) \bmod g(x)\]

最后构造发送多项式:

\[c(x) = x^r m(x) + p(x)\]

因为后面会看到,在 $GF(2)$ 中加法和减法相同,所以也可以把这一步理解为“把余数补到消息尾部,让整体余数归零”。于是:

\[c(x) \bmod g(x) = 0\]

这就是生成多项式生成的东西:所有能被 $g(x)$ 整除的合法码字集合。接收端并不需要恢复中间的商,只要检查收到的整体对 $g(x)$ 的余数是否为零。

2.2 生成多项式不是消息的一部分

这里容易混淆的一点是:$m(x)$ 来自待发送数据,$g(x)$ 来自编码规则。消息变了,$m(x)$ 会变;协议不变,$g(x)$ 通常不会变。

例如一个协议规定使用某个 32 位 CRC 多项式,那么所有消息都用这同一个 $g(x)$ 求余。不同消息会得到不同余数,但这些余数的目的相同:把“消息 + 校验字段”拼成一个能被 $g(x)$ 整除的码字。

这正是生成多项式比奇偶校验更系统的地方。奇偶校验只把所有位置压成一个“奇偶性”;生成多项式则给每个位置分配不同的多项式权重,并通过整除关系约束整个序列。


3 为什么比特串可以变成多项式

上面一直在写 $m(x)$、$g(x)$ 和 $c(x)$,但工程里真正拿到的是 0 和 1。位串为什么可以看成多项式?原因是我们可以把一串比特解释为多项式的系数。

例如比特串 101101 可以写成:

\[1 \cdot x^5 + 0 \cdot x^4 + 1 \cdot x^3 + 1 \cdot x^2 + 0 \cdot x + 1\]

省略系数为 $0$ 的项后得到:

\[x^5 + x^3 + x^2 + 1\]

这里的 $x$ 不是某个具体数值,而是一个形式变量。把位串看成多项式,并不是为了真的求 $x=2$ 时的整数值,而是为了使用多项式的除法、余数、因式和整除关系。

这个视角有一个非常重要的工程含义:左移一位相当于乘以 $x$。例如:

\[x^5 + x^3 + x^2 + 1\]

左移后变成:

\[x^6 + x^4 + x^3 + x\]

也就是比特串末尾补一个 $0$。这正好对应校验计算中常见的移位寄存器行为。


4 为什么多项式运算最终变成异或

二进制校验为什么天然落在 $GF(2)$ 上?有限域可以先不从抽象定义理解。对工程师来说,最重要的是它提供了一套封闭的算术规则:在这个集合里做加、减、乘、除,结果仍然留在这个集合里。二进制校验最常用的有限域是 $GF(2)$,它只有两个元素:$0$ 和 $1$。

4.1 GF(2) 加法与异或

加法为什么会变成异或?在 $GF(2)$ 中,加法规则如下:

\[0 + 0 = 0\] \[0 + 1 = 1\] \[1 + 0 = 1\] \[1 + 1 = 0\]

这正好就是 XOR。关键点在最后一行:两个 $1$ 相加不会产生进位,而是回到 $0$。因此,在这个世界里不存在十进制或普通二进制整数加法里的“进位传播”。这让校验计算可以被写成并行的异或网络,也能被软件用机器字快速处理。

减法也没有额外规则,因为在 $GF(2)$ 中:

\[a - b = a + b\]

例如:

\[1 - 1 = 0\] \[1 + 1 = 0\]

所以多项式长除法里原本的“减去某一项”,在实现里也只是异或掉某一组比特。

4.2 有限域和普通整数运算的差异

很多误解来自把 CRC 一类算法当作普通整数除法。二者的外形相似,但算术规则不同。

对比项 普通整数/普通多项式 $GF(2)$ 上的多项式
系数范围 整数、实数等 只有 $0$ 和 $1$
加法 可能产生进位或系数累加 异或,无进位
减法 与加法不同 与加法相同
左移含义 整数乘以 2 多项式乘以 $x$
除法结果 商和余数按普通减法得到 商和余数按异或消项得到

因此,看到“多项式除法”时,不要把它想成 CPU 的整数除法指令。它更接近一个按最高项对齐、用异或不断消去最高项的过程。

4.3 为什么除法步骤会变成异或

说“多项式除法用异或实现”容易让人误解,好像除法本身突然变成了某种按位操作。更准确的说法是:多项式除法仍然是在反复用除式的倍数消掉当前最高项,只是每一步原本要做的“减法”,在 $GF(2)$ 里等价于加法,而加法又等价于 XOR。

普通多项式除法的一步可以抽象成:

\[\text{当前多项式} - x^k g(x)\]

其中 $x^k g(x)$ 表示把生成多项式移动到和当前最高项对齐的位置。这个动作的目的不是得到最终商,而是先把当前最高项消掉,让余下多项式的次数逐步降低。

但在 $GF(2)$ 中:

\[a-b=a+b\]

所以这一步变成:

\[\text{当前多项式} + x^k g(x)\]

而 $GF(2)$ 的加法逐位看就是 XOR。因此,落到比特串上,就是:

当前比特串 XOR 对齐后的生成多项式

例如当前多项式是:

\[x^6+x^4+x^3\]

生成多项式是:

\[g(x)=x^3+x^2+1\]

为了消掉当前最高项 $x^6$,需要把 $g(x)$ 乘以 $x^3$:

\[x^3g(x)=x^6+x^5+x^3\]

然后做消项:

\[(x^6+x^4+x^3)+(x^6+x^5+x^3)=x^5+x^4\]

其中 $x^6$ 和 $x^3$ 都各出现两次,在 $GF(2)$ 中会抵消掉:

\[1+1=0\]

写成比特就是:

1011000
1101000
-------
0110000

所以,除法和异或之间的关系可以概括为一条链:

多项式除法 = 反复用除式的倍数做减法消最高项
GF(2) 中减法 = 加法
GF(2) 中加法 = XOR
所以 GF(2) 多项式除法的每一步消项 = XOR

生成多项式决定“要拿哪一组位置参与消项”,异或则是这个消项动作在二进制有限域里的实现方式。

4.4 生成多项式与异或的手算示例

为了避免一开始就陷入 32 位多项式,可以先看一个很小的手算例子。这里的消息和生成多项式都不是从真实协议里摘出来的,而是为了演示机制而人为选定。

先选一段 4 bit 消息 1011。消息多项式 $m(x)$ 来自待校验数据本身,所以这里有:

\[m(x) = x^3 + x + 1\]

因为 1011 从左到右分别对应 $x^3$、$x^2$、$x$、常数项的系数,也就是:

\[1\cdot x^3 + 0\cdot x^2 + 1\cdot x + 1\]

再选一个便于手算的玩具生成多项式 1101

\[g(x) = x^3 + x^2 + 1\]

选择它的理由很朴素:最高项是 $x^3$,所以它是三次多项式;常数项为 $1$,不会退化成单纯移位;中间有一个 $x^2$ 项,反馈关系也不会太空。它不是从 1011 推导出来的,而是编码方案预先选定的参数。

生成多项式的次数由最高非零项决定。1101 的最高非零项是 $x^3$,所以次数为 $3$,也就是:

\[r=3\]

因此,计算校验位时先把消息左移三位:

\[x^3 m(x) = x^6 + x^4 + x^3\]

接着求余:

\[x^3 m(x) \bmod g(x) = p(x)\]

对应的被除数比特串是 1011000,除数是 1101。接下来做的不是普通整数除法,而是“最高位对齐后用异或消项”:

1011000
1101
----
0110000
 1101
 ----
0000100

这里每一次横线下方的结果,都是上一行与对齐后的 1101 做 XOR。例如第一步:

1011
1101
----
0110

写成多项式就是:

\[(x^6+x^4+x^3)+(x^6+x^5+x^3)=x^5+x^4\]

同一位置上两个 $1$ 相加会消掉,因为:

\[1+1=0\]

这就是生成多项式和异或之间最直接的关系:生成多项式决定“哪些位置要参与消项”,而在 $GF(2)$ 中消项的实现就是 XOR。多项式里某一项系数为 $1$,对应硬件或软件里那个位置需要异或;系数为 $0$,对应那个位置不参与异或。

做完 $GF(2)$ 上的多项式除法后,余数为:

\[p(x) = x^2\]

因为这里 $r=3$,余数字段要固定写成 3 位,对应 $x^2$、$x$、常数项三个系数。所以 $p(x)=x^2$ 写成校验字段就是 100,而不是只写一个 1。那么发送多项式为:

\[c(x) = x^6 + x^4 + x^3 + x^2\]

对应比特串就是 1011100。接收端不需要知道中间的商,只需要检查:

\[c(x) \bmod g(x) = 0\]

如果不为 $0$,就说明收到的比特串不属于这个生成多项式定义的合法集合。

4.5 余数长度与校验位宽

上面的例子里,消息 1011 后面追加的是 3 位校验字段 100,最终得到 1011100。这里的“3 位”不是随意决定的,而是由生成多项式的次数决定:如果生成多项式 $g(x)$ 的次数是 $r$,那么任何多项式除以 $g(x)$ 后,余数次数都小于 $r$:

\[\deg p(x) < r\]

因此余数最多需要 $r$ 个比特表示。这也是为什么一个 $r$ 次生成多项式对应 $r$ 位校验值。常见的“32 位校验”背后,就是一个次数为 $32$ 的生成多项式。


5 错误检测的整除模型

错误检测为什么会变成整除问题?设发送端发出的合法码字为 $c(x)$,传输或存储过程中出现错误模式 $e(x)$。接收端实际拿到的是:

\[r(x) = c(x) + e(x)\]

接收端检查:

\[r(x) \bmod g(x)\]

由于 $c(x)$ 本身能被 $g(x)$ 整除,有:

\[c(x) \bmod g(x) = 0\]

所以:

\[r(x) \bmod g(x) = e(x) \bmod g(x)\]

这条式子非常关键。它说明检错能力只取决于错误模式 $e(x)$ 是否会被 $g(x)$ 整除。如果错误模式不能被 $g(x)$ 整除,就一定能被发现;如果错误模式刚好也是 $g(x)$ 的倍数,就会漏检。

5.1 单比特错误的检测条件

单比特错误为什么容易发现?因为它可以写成:

\[e(x) = x^k\]

这里的 $x^k$ 只有一个非零项,在数学上叫单项式。单项式是指只包含一项的多项式,例如 $x^3$、$x^7$ 或 $1$。前文说的生成多项式仍然是算法预先选定的 $g(x)$,例如 $x^3+x^2+1$ 就有多个非零项,因此不是单项式。

只有形如 $x^a$ 这样的单项式,才可能整除另一个单项式 $x^k$。如果生成多项式 $g(x)$ 含有多个非零项,它就不可能整除单比特错误对应的 $x^k$。实际使用的生成多项式都至少包含最高项和常数项,通常还包含若干中间项,因此单比特错误通常都能被检测出来。

5.2 突发错误的代数保证

突发错误为什么是循环校验码擅长的场景?因为很多真实错误不是均匀散布的,而是集中在一段连续比特里。例如磁盘、链路、电气噪声、串行传输中的干扰,常常会造成一段连续位置上的比特异常。这类错误可以抽象成突发错误。

长度不超过 $r$ 的突发错误,其错误多项式可以写成:

\[e(x) = x^i b(x)\]

其中 $b(x)$ 的次数小于 $r$,且首尾项非零。若生成多项式 $g(x)$ 的次数为 $r$,并且包含常数项,那么 $g(x)$ 不会整除这种低于自身次数的突发模式。因此,次数为 $r$ 的生成多项式可以保证检测长度不超过 $r$ 的突发错误。

这就是循环校验码在通信和存储系统中长期有生命力的原因之一:它不是平均意义上“看起来不错”,而是对某些常见错误形态有明确的代数保证。

5.3 奇数个比特错误和因子 $x+1$

如果生成多项式包含因子 $x+1$,那么它可以检测所有奇数个比特翻转。原因是把一个错误多项式代入 $x=1$:

\[e(1)\]

在 $GF(2)$ 中,这个值等于错误比特个数的奇偶性。如果错误比特个数为奇数,则:

\[e(1) = 1\]

而任何包含因子 $x+1$ 的多项式,在 $x=1$ 处取值为 $0$。因此奇数权重的错误多项式不可能被这样的生成多项式整除。

这个结论也提醒我们:生成多项式的因子结构会直接影响它能系统性排除哪些错误模式。


6 循环结构的引入

用 $g(x)$ 定义合法码字集合之后,还要看清楚另一个细节:校验计算处理的是有顺序的 bit 流,而不是一堆无序的 0 和 1。不同位置上的 bit 对余数的影响不同,所以算法必须在读入数据时保留这种先后关系。

最自然的做法是让状态随输入向前推进。每来一个新 bit,已经读过的内容就在状态里挪到更高的位置,新 bit 进入空出来的低位。状态宽度可以保持不变,但各个 bit 所处的位置和权重会改变,这就是移位在校验计算里的意义。

可以把第 4 章里的小例子继续拿来观察这件事。消息仍然选 1011,生成多项式仍然选:

\[g(x)=x^3+x^2+1\]

也就是二进制 1101。因为它是三次多项式,所以余数寄存器宽度为 $3$。为了计算校验字段,需要先在消息后面补 3 个零,得到待除的比特流:

1011 000

如果按手算长除法看,每一步都是“把下一个 bit 带下来,然后看当前临时多项式是否已经出现 $x^3$ 项”。如果出现,就用 1101 异或消掉最高项;如果没有,就继续读下一个 bit。右边则用一个 3 位移位寄存器表示同一件事:先把新 bit 移入最低位,如果最高位被移出为 1,就把生成多项式的低 3 位抽头 101 异或进寄存器。

输入 bit 手算长除法视角 移位寄存器视角
初始 余数 000 寄存器 000
1 带下后为 001,次数还小于 3,不消项,余数 001 000 左移并移入 1001,无移出最高位,状态 001
0 带下后为 010,不消项,余数 010 001 左移并移入 0010,无移出最高位,状态 010
1 带下后为 101,不消项,余数 101 010 左移并移入 1101,无移出最高位,状态 101
1 带下后临时为 1011,出现 $x^3$ 项;1011 XOR 1101 = 0110,余数 110 101 左移并移入 1 得低 3 位 011,移出最高位为 1011 XOR 101 = 110
0 带下后临时为 11001100 XOR 1101 = 0001,余数 001 110 左移并移入 0 得低 3 位 100,移出最高位为 1100 XOR 101 = 001
0 带下后为 010,不消项,余数 010 001 左移并移入 0010,无移出最高位,状态 010
0 带下后为 100,不消项,余数 100 010 左移并移入 0100,无移出最高位,状态 100

两列最后都得到余数 100,也就是前面手算出的校验字段。左边是在纸面上维护一个“当前余数”,右边是在电路里维护一个“寄存器状态”;它们不是两套算法,而是同一个 $GF(2)$ 多项式模除过程的两种表达。

这个例子也能看出 LFSR 里的“反馈”到底来自哪里。1101 的最高位表示除式最高项 $x^3$,它负责判断何时需要消项;真正反馈进 3 位状态的是低阶部分 101,对应 $x^2$ 和常数项。当某一步移出了最高位 1,说明临时多项式中出现了需要被消掉的 $x^3$ 项,于是寄存器就异或 101,这正是手算里用 1101 对齐后异或消项的硬件化写法。

6.1 状态推进与乘以 $x$

在多项式表示里,这种“往前挪一格”正好对应乘以 $x$。例如当前已经读到的状态是:

\[a_3x^3+a_2x^2+a_1x+a_0\]

再读入一个新 bit $b$ 时,新的未约简状态可以写成:

\[x(a_3x^3+a_2x^2+a_1x+a_0)+b\]

也就是旧状态整体乘以 $x$,再把新 bit 放到最低次项。这样,每个 bit 会因为读入顺序不同而落到不同次数的位置上,顺序信息就被保留下来。

如果考虑固定长度 $n$ 的码字,普通左移会让最高位被移出这个长度;如果把最高位再绕回最低位,就得到循环移位。在多项式上,这相当于在下面这个关系下做运算:

\[x^n = 1\]

也就是在模 $x^n - 1$ 的多项式世界里工作:

\[GF(2)[x] / (x^n - 1)\]

这解释了为什么循环码和生成多项式天然联系在一起:一个循环码可以看作由某个生成多项式 $g(x)$ 生成的倍数集合,而 $g(x)$ 通常是 $x^n - 1$ 的因子。

6.2 从数学结构到移位寄存器

多项式模除法每一步都在做同一件事:

  1. 看当前最高项是否需要消去。
  2. 如果需要,就把生成多项式对齐到这个最高项。
  3. 用异或消去这一组项。
  4. 继续向低次项推进。

这正好可以用线性反馈移位寄存器表示。寄存器保存当前余数,输入比特逐个进入;当反馈位为 $1$ 时,把生成多项式中对应的抽头异或到寄存器里。所谓“抽头”,本质上就是生成多项式里哪些系数为 $1$。

需要注意的是,最高项用来决定寄存器宽度,通常不再画成一个额外反馈抽头。比如 $g(x)=x^4+x+1$ 的系数是 10011,其中 $x^4$ 表示 4 位余数宽度,真正参与反馈的是低阶项里的 $x$ 和 $1$,也就是低 4 位 0011

生成多项式与 LFSR 抽头之间的对应关系


7 多项式长度与检测能力

生成多项式是不是越长越好?校验位宽越大,随机错误漏检概率通常越低,但这不等于“位宽越长就自动更适合”。生成多项式的选择要同时看错误模型、消息长度、协议约束和实现成本。

7.1 汉明距离关心的是最小不可区分差异

汉明距离先从两串等长 bit 的比较理解:如果两个 bit 串在某些位置不同,不同位置的个数就是它们的汉明距离。例如:

101100
100110

这两串有两个位置不同,所以汉明距离是 $2$。放到校验码里,更关心的是所有合法码字之间的最小汉明距离,也就是任意两个合法码字至少相差多少个 bit。这个最小距离越大,越能保证发现更多低重量错误。

如果一个码的最小汉明距离为 $d$,那么它可以检测任意少于 $d$ 个比特的错误:

\[w(e) < d\]

其中 $w(e)$ 表示错误模式中非零项的个数。生成多项式会影响这个距离,但距离还与消息长度有关。同一个生成多项式,在短帧上可能具有很高的检测能力,到了很长的数据块上保证就会下降。

7.2 常见多项式不代表所有场景最优

工程上常见的多项式往往来自标准、历史兼容和实现生态。例如很多系统使用 32 位校验,但不同协议可能选用不同的 32 位生成多项式。选择时需要回答几个问题:

问题 影响
最大消息长度是多少 决定目标汉明距离能保持到多长
错误更像随机翻转还是突发错误 决定该优先优化哪类错误模型
是否必须兼容既有协议 决定能不能更换多项式
软件还是硬件实现 影响查表、并行展开和寄存器抽头设计
是否需要检测全零前缀、尾部填充等异常 影响初值、终值异或和数据预处理

因此,生成多项式不是一个孤立常数,而是一组工程假设的压缩表达。

7.3 多项式表示也会带来混淆

同一个生成多项式可能有不同写法。例如有的文档省略最高项,因为最高项必然为 $1$;有的文档使用正常位序,有的文档使用反射位序;有的实现把多项式写成适合右移寄存器的形式。

以常见 32 位校验多项式为例,其正常表示常写为 0x04C11DB7,反射实现中常见的表示则是 0xEDB88320。这两个数看起来不同,但常常描述的是同一组代数约束在不同位序下的实现形态。

所以阅读协议或代码时,至少要同时确认:

  1. 多项式宽度。
  2. 多项式十六进制表示是否省略最高项。
  3. 输入比特是否反射。
  4. 输出余数是否反射。
  5. 初始寄存器值。
  6. 最终是否异或固定值。

只看一个 poly 参数,很容易把数学对象和实现约定混在一起。


8 从代数到实现的工程约束

从代数走向实现时会遇到哪些约束?一旦进入工程实现,核心问题就从“能不能算”变成“怎样在给定资源下算得足够快、足够稳定、足够容易验证”。CRC 的实现形态很多,bit 串行、byte 查表、slicing-by-N、FPGA 并行 XOR 网络、ASIC 流水线,本质上都在实现同一个状态转移,只是把“多轮移位与异或”折叠到不同的时间和空间里。

理解这件事,最好从最朴素的串行电路开始。串行电路最接近前文的多项式长除法;并行电路则是把若干次串行转移提前展开,化简成一组组合 XOR 方程。两者不是两套算法,而是同一个线性状态机的不同实现。

8.1 串行实现:最接近多项式长除法

按 bit 串行处理最容易和数学过程对齐。每来一个输入比特,寄存器移位一次,根据反馈位决定是否异或生成多项式抽头。这种方式面积小、逻辑简单、适合低速链路、调试模型或资源受限场景。

串行 CRC 电路通常由三部分组成:

结构 作用
余数寄存器 保存当前多项式除法的中间余数,也就是 CRC 状态
反馈 XOR 把输入 bit 和寄存器最高位组合起来,决定本轮是否需要按生成多项式消项
抽头 XOR 根据生成多项式低阶项中哪些系数为 $1$,把反馈位异或到对应寄存器路径上

以前面提到的低阶结构为例,若选择:

\[g(x)=x^4+x+1\]

那么余数寄存器宽度为 $4$,可以记为 $s_3,s_2,s_1,s_0$。最高项 $x^4$ 不对应一个额外抽头,它只说明余数宽度是 4;真正决定反馈异或位置的是低阶项 $x$ 和 $1$。

串行 LFSR 结构与生成多项式抽头的对应关系

如果采用高位先输入的一种常见写法,设当前输入 bit 为 $d$,反馈位为 $f$,则一次状态更新可以写成:

\[f=d+s_3\] \[s_3'=s_2\] \[s_2'=s_1\] \[s_1'=s_0+f\] \[s_0'=f\]

这里的加号仍然是 $GF(2)$ 加法,也就是 XOR。这个状态方程比电路图更重要:电路图会因为左移、右移、反射、多项式表示方式而变形,但状态方程明确说明了每一拍的旧状态、输入和新状态之间的关系。

它的限制也很明显:每个周期只处理一个 bit,吞吐率直接受时钟和接口宽度限制。如果数据路径本身是 8 bit、32 bit 或 512 bit 宽,逐 bit 处理就会成为瓶颈。

8.2 串行状态如何展开成并行状态

并行 CRC 的关键不是重新发明一种算法,而是把串行状态转移连续代入多次。可以把一次串行更新抽象成一个函数:

\[s_{t+1}=T(s_t,d_t)\]

如果每个周期要处理 $k$ 个 bit,那么目标就是直接得到:

\[s_{t+k}=T(T(\cdots T(s_t,d_0),d_1)\cdots,d_{k-1})\]

由于 CRC 在 $GF(2)$ 上是线性的,连续代入以后,新状态的每一位仍然只是“旧状态若干位”和“输入若干位”的 XOR。中间状态不需要真的保存成寄存器,它们可以被代入、消去,最后变成一片组合 XOR 网络。

把串行状态转移展开成 k bit 并行更新

先看两拍展开,仍沿用上一节的状态方程。第一个输入 bit 记为 $d_0$:

\[f_0=d_0+s_3\]

第一拍后:

\[s_{3,1}=s_2\] \[s_{2,1}=s_1\] \[s_{1,1}=s_0+d_0+s_3\] \[s_{0,1}=d_0+s_3\]

第二个输入 bit 记为 $d_1$。此时反馈位不再是 $d_1+s_3$,而是 $d_1+s_{3,1}$,也就是:

\[f_1=d_1+s_2\]

第二拍后得到:

\[s_{3,2}=s_1\] \[s_{2,2}=s_0+d_0+s_3\] \[s_{1,2}=d_0+s_3+d_1+s_2\] \[s_{0,2}=d_1+s_2\]

这已经能看出并行化的本质:处理两个 bit 后的新状态,全部可以直接由旧状态 $s_3,s_2,s_1,s_0$ 和输入 $d_0,d_1$ 算出。继续代入到 4 bit、8 bit、32 bit,本质上只是方程规模变大。

形式上,也可以把一次状态更新写成矩阵变换:

\[s_{t+1}=A s_t+B d_t\]

连续处理 $k$ 个 bit 后变成:

\[s_{t+k}=A^k s_t+\sum_{i=0}^{k-1}A^{k-1-i}B d_i\]

这里所有乘法和加法都发生在 $GF(2)$ 上。矩阵里的 $1$ 表示某个状态位或输入位会参与 XOR,矩阵里的 $0$ 表示不参与。硬件生成器、脚本工具或综合前的参数化 RTL,通常就是在自动完成这件事。

8.3 并行硬件:把多轮状态转移折叠成 XOR 网络

把串行状态展开以后,硬件里看到的就不再是一条每拍移动一次的短反馈链,而是一组组合 XOR 方程。仍以 $g(x)=x^4+x+1$、一次处理 4 bit 为例,按上一节的高位先输入约定,展开 4 拍后可得到:

\[n_3=s_3+s_2+d_1+d_0\] \[n_2=s_2+s_1+d_2+d_1\] \[n_1=s_3+s_1+s_0+d_3+d_2+d_0\] \[n_0=s_3+s_0+d_3+d_0\]

其中 $n_3,n_2,n_1,n_0$ 是一次并行更新后的新余数状态。把这些加号换成 XOR,就得到硬件实现中的组合逻辑。

并行 CRC 的 XOR 网络与流水线切分

并行度从 1 bit 提高到 $k$ bit 后,每个周期的数据吞吐率提高了,但代价也很直接:

变化 影响
输入位数增加 每个新状态位可能依赖更多输入位
XOR 扇入增加 单级 XOR 过深会拉长组合延迟
连线更多 FPGA 上可能受布线和 LUT 映射影响,ASIC 上可能受线延迟和功耗影响
方程更复杂 参数化生成、形式验证和测试向量覆盖更重要

实际设计中通常不会让一个很大的 XOR 直接堆成一条长链,而是会做平衡 XOR 树,必要时插入流水线寄存器。流水线能改善时序,但会改变延迟:吞吐率可能保持一拍一个数据字,结果却要晚若干拍出来。因此接口协议还要一起处理 validlast、包尾 CRC 对齐等控制信号。

8.4 查表实现:软件里的并行展开

软件中常见的 byte-at-a-time 查表法,本质上也是并行展开,只不过它把连续 8 次移位和条件异或预先算好,存进表里。处理一个字节时,不再逐 bit 做 8 轮状态转移,而是用当前余数的一部分和输入字节形成索引,再查表更新余数。

如果把串行状态转移记为 $T$,那么一张 256 项的表可以理解为预存了所有可能 8 bit 输入对余数的影响:

\[T^8(\text{state}, \text{byte})\]

更激进的 slicing-by-N 方法会同时使用多张表,把多个字节的影响并行折叠进余数。例如 slicing-by-4 可以一次合并 4 个字节的影响,减少循环次数。它的代价是表空间增加、缓存压力上升,并且更容易暴露端序、反射输入和平台字节序之间的差异。

所以,查表法和硬件并行 CRC 的思想是相通的:都在提前合并多轮串行状态转移。区别在于,软件用内存表保存预计算结果,硬件用组合 XOR 网络直接算出结果。

8.5 参数化实现最容易错在边界

很多校验错误不是多项式本身错了,而是边界约定不一致。尤其当同一个模块要支持不同位宽、不同多项式、不同输入位序时,下面这些点比 XOR 方程本身更容易出错:

易错点 典型后果
bit 输入顺序不一致 与标准测试向量不匹配
初值设错 空消息或短消息结果异常
反射处理重复或遗漏 软件和硬件结果互为位反转变体
末尾补零规则理解错误 余数计算偏移一个校验宽度
数据字节序和 bit 序混淆 多字节接口下结果随平台或总线定义变化
并行展开的输入顺序写反 低速 bit 串行模型正确,高速并行模块错误
流水线延迟没有对齐控制信号 CRC 值正确但出现在错误的数据包边界

严肃实现通常会保留三个层次的参照物:第一是数学定义,也就是 $g(x)$、初值、反射和最终异或;第二是 bit 串行黄金模型,用来描述最小一步状态转移;第三是并行实现,用自动脚本或形式等价检查证明它等价于连续多拍串行模型。

8.6 工程取舍:面积、时序、吞吐率和可验证性

从工程角度看,CRC 实现的选择通常不是“串行好”或“并行好”,而是看数据路径、时钟目标和验证成本。

实现方式 适合场景 主要代价
bit 串行 LFSR 低速链路、小面积控制逻辑、黄金模型 吞吐率低
byte 查表 CPU 软件、通用库、协议栈 表空间、缓存行为、端序处理
slicing-by-N 高吞吐软件 CRC 表更大,代码和数据布局更复杂
k bit 并行 XOR 网络 FPGA/ASIC 高速数据通路 XOR 扇入、布线、时序收敛
流水线并行 CRC 超高频或超宽数据路径 延迟增加,控制信号对齐更复杂

对 FPGA 来说,宽并行 CRC 往往受 LUT 映射和跨区域布线影响。一个看起来只是“很多 XOR”的逻辑,如果扇入很大、跨越很宽的数据总线,可能会比预期更难收敛。对 ASIC 来说,问题更多落在门级延迟、线延迟、功耗和可测试性上。

因此,比较稳妥的实现路径通常是:

  1. 先写一个清晰的 bit 串行参考模型。
  2. 用脚本或矩阵方法从参考模型生成 $k$ bit 并行方程。
  3. 用随机测试和标准测试向量比较串行模型与并行模型。
  4. 对很宽的并行网络做 XOR 树平衡和必要的流水线切分。
  5. 把数据、validlast、初始化、输出异或等控制路径一起验证。

只要抓住“并行实现等价于连续多次串行状态转移”这条线,查表法、硬件展开和流水线优化就不会显得像彼此无关的技巧。它们都是在同一个生成多项式约束下,把时间上的多次 XOR 换成空间上的预计算或组合逻辑。


9 常见误解与边界

理解有限域和生成多项式后,很多流行说法可以被更精确地限定。

9.1 “校验值越长越安全”并不完整

更长的校验值通常降低随机漏检概率,也能覆盖更长的突发错误。但如果多项式选择不合适,或者消息长度远超该多项式的设计区间,实际汉明距离可能并不理想。位宽是重要变量,但不是唯一变量。

9.2 “生成多项式只是一个常数”会遮蔽设计目标

生成多项式的因子、权重、阶、与消息长度的关系,都会影响检错性质。把它只当作代码里的 POLY 常量,会让人忽视它背后的错误模型和协议假设。

9.3 “结果不一致就是多项式错了”经常误判

同一个数学多项式,在不同实现约定下会产生不同中间值和不同十六进制常数。排查时应先确认参数全集,而不是立刻更换多项式。

9.4 “循环校验码能保证数据正确”是过度表述

校验码只能检测它能够区分的错误模式。它不提供密码学意义上的抗伪造能力,也不能替代认证码或数字签名。对偶发传输错误和存储错误,它很有效;对主动攻击者,它不是安全边界。


10 一个具体 CRC 算法的定义过程

前面一直在讨论 $g(x)$、$m(x)$ 和 $r$,但在真实协议里,一个 CRC 算法并不只写一句“用某个多项式”。它通常会被定义成一组参数:校验宽度、生成多项式、输入位序、初始值、输出处理方式和测试向量。下面用常见的 CRC-32/IEEE 作为例子,说明这些量各自从哪里来。

10.1 参数表定义了算法边界

一个典型 CRC-32/IEEE 参数集可以写成:

参数 常见取值 含义
Width 32 余数寄存器宽度,也是最终 CRC 宽度
Poly 0x04C11DB7 生成多项式的十六进制表示,通常省略最高项
Init 0xFFFFFFFF 余数寄存器初始值
RefIn true 输入字节内 bit 是否按反射顺序处理
RefOut true 输出余数是否反射
XorOut 0xFFFFFFFF 最终输出前异或的固定值
Check 0xCBF43926 字符串 123456789 的标准校验结果

这张表里的 Poly 只决定代数约束的一部分;InitRefInRefOutXorOut 则决定工程实现和协议兼容约定。两个实现只有在这些参数全部一致时,才应该得到同一个最终 CRC 值。

10.2 生成多项式的确定

CRC-32/IEEE 的正常多项式表示常写作 0x04C11DB7。这个数不是从某一条消息算出来的,也不是随手拍脑袋写下来的。CRC-32 需要 32 位校验值,因此候选生成多项式首先被限定为 32 次二进制多项式:

\[g(x)=x^{32}+a_{31}x^{31}+a_{30}x^{30}+\cdots+a_1x+a_0\]

其中每个系数 $a_i$ 只能是 $0$ 或 $1$。在这个候选空间里,设计者会根据目标消息长度、突发错误检测能力、低重量错误检测能力、硬件实现复杂度和标准兼容性等条件筛选多项式。0x04C11DB7 可以理解为被 CRC-32/IEEE 这一路标准化路径选中并固定下来的那个 32 次生成多项式。

由于最高项 $x^{32}$ 是隐含的,参数表里通常只记录从 $x^{31}$ 到 $x^0$ 的 32 个系数。0x04C11DB7 对应的完整生成多项式是:

\[g(x)=x^{32}+x^{26}+x^{23}+x^{22}+x^{16}+x^{12}+x^{11}+x^{10}+x^8+x^7+x^5+x^4+x^2+x+1\]

这里的 $g(x)$ 不是从某一条消息 $m(x)$ 推导出来的,而是在算法定义时预先选定。它的作用是规定合法码字必须满足的整除关系。协议或文件格式一旦规定了这个多项式,发送端、接收端、软件库、硬件模块都要围绕同一个 $g(x)$ 工作。

如果使用反射实现,代码里还常看到 0xEDB88320。这不是另一个随意替换的多项式,而是同一个代数约束在低位先处理时的表示形式。也因此,讨论“用了哪个多项式”时,必须同时说明位序约定。

10.3 校验位宽 $r$ 的确定

$r$ 来自生成多项式的次数。CRC-32/IEEE 的生成多项式最高项是 $x^{32}$,所以:

\[r=32\]

这意味着计算过程中维护的是 32 位余数,最终输出也是 32 位校验值。抽象公式里的:

\[p(x)=x^r m(x) \bmod g(x)\]

在这个例子里就变成:

\[p(x)=x^{32}m(x) \bmod g(x)\]

不过工程实现未必真的显式把消息后面补 32 个零再做长除法。很多反射算法、查表算法和硬件 LFSR 会把补零、初值和最终异或折叠进状态转移里,但它们仍然是在实现同一个余数约束。

10.4 消息多项式 $m(x)$ 的确定

$m(x)$ 来自待校验的数据本身。给定一段字节序列,先按照算法规定的 bit 顺序把它展开成比特流,再把这些比特当作 $GF(2)$ 上的多项式系数。

例如消息字节是 0x31,也就是字符 1。如果按正常高位先处理,它对应的 bit 序列是:

00110001

可以表示为:

\[m(x)=x^5+x^4+1\]

如果算法规定输入反射,那么同一个字节会按低位先进入状态机。此时实现层面的处理顺序变了,代码里看到的移位方向、查表索引和多项式常数也会随之变化。也就是说,$m(x)$ 不是一个脱离位序约定的裸字节数组,而是“数据内容 + bit 进入顺序”共同确定的多项式对象。

10.5 初值、反射和最终异或的作用

InitRefInRefOutXorOut 不改变生成多项式本身,但会改变最终呈现出来的校验值。

参数 主要作用
Init 让初始状态不是全零,可影响短消息、前导零和协议兼容行为
RefIn 规定输入 bit 进入余数状态机的方向
RefOut 规定输出余数是否按位反射后再呈现
XorOut 对最终余数做固定后处理,常用于历史兼容或增强边界可区分性

因此,一个完整 CRC 算法可以理解成下面这组定义:

\[\text{CRC Algorithm}=(r, g(x), \text{Init}, \text{RefIn}, \text{RefOut}, \text{XorOut})\]

其中 $g(x)$ 决定合法码字集合,$r$ 决定余数宽度,$m(x)$ 由输入数据和 bit 顺序确定,其他参数则定义状态机如何初始化、如何读入数据以及如何输出结果。很多实现不一致的问题,本质上不是“多项式算错了”,而是这组参数里有一项没有对齐。


11 继续深入的学习主线

继续深入时应该抓住哪条主线?如果只保留一条主线,可以这样记:

  1. 比特不是普通整数,而是 $GF(2)$ 上的系数。
  2. 位串可以表示为多项式。
  3. 生成多项式定义了合法码字集合。
  4. 校验过程是在检查接收多项式是否能被生成多项式整除。
  5. 错误检测能力取决于错误多项式是否会落入生成多项式的倍数集合。
  6. 工程实现是把多项式模除折叠成移位、异或、查表或并行 XOR 网络。

进一步学习时,可以沿着三条路径展开。

第一条是数学路径:研究循环码、有限域扩展、BCH 码和 Reed-Solomon 码。它能解释为什么生成多项式的根、因子和最小距离之间存在深层关系。

第二条是实现路径:从 bit 串行算法推导 byte 查表,再推导 slicing-by-N 和并行矩阵展开。它能解释为什么同一个校验规则在软件、FPGA 和 ASIC 中会呈现完全不同的形态。

第三条是工程选择路径:根据消息长度、错误模型、兼容要求和资源约束选择多项式与参数。它能帮助我们判断什么时候沿用标准,什么时候需要重新评估校验方案。

有限域和生成多项式看起来抽象,但它们真正的价值在于给低成本位运算提供了可分析的保证。只要抓住“余数”和“整除”这两个核心概念,很多校验算法的复杂表象都会收束到同一套清晰的机制上。


12 参考资料


Back to Archive
WeChat QR Code

Scan to connect