☰
模2运算从入门到实战:异或、CRC校验、LFSR与汉明码原理
2026/9/29 6:27:29 网站建设 项目流程

1. 模2运算的编码本质:什么时候你会真的需要它

做通信、写底层驱动、做存储校验、甚至玩点嵌入式开发的朋友,模2运算这四个字一定不陌生。它还有一个更常见的名字——二进制多项式运算。我第一次接触模2运算,是当年调一个CRC16校验模块,对着数据手册怎么都算不对,后来才意识到,问题出在我用普通十进制除法的思路去理解模2除法,从一开始就错了。

模2运算不是普通四则运算的“二进制版本”,它是有限域GF(2)上的运算。这里的“2”指的是模数,整个运算体系只有0和1两个元素。它最核心、最反直觉的一条规则是:加法等价于减法,也就是按位异或(XOR)。所有进位和借位全部被丢掉,1+1=0,1-1=0。如果你以前没接触过有限域,会觉得这简直是在胡闹,但硬件中最省门的加法器、最常用的校验算法、最经典的纠错编码,全建立在这套看似荒唐的规则之上。

这篇文章想做的事,是把模2加法、模2减法、模2乘法、模2除法这四种运算彻底讲透。我会从数学定义、手算过程、多项式视角、工程应用四个维度拆开,把每一步为什么这么算说清楚,顺便附上一些我踩过的坑。适合刚接触CRC、LFSR、汉明码的学生,也适合需要亲手实现算法但总被资料绕晕的工程师。

2. 模2加法与模2减法:为什么它们在二进制世界里是同一件事

2.1 按位异或的三种理解方式

模2加法用符号⊕表示,规则极其简单:0⊕0=0,0⊕1=1,1⊕0=1,1⊕1=0。你把这组规则展开成真值表,会发现它和逻辑门电路里的异或门(XOR gate)完全一致。所以很多场合下,模2加法直接就叫“按位异或”。

第一种理解方式是把它当“二进制无进位加法”。普通加法里1+1=10,会产生进位,模2加法里这个进位被直接扔进垃圾桶,只剩下最低位的0。这正是数字电路中半加器输出端“和位”的算法:两个一位二进制数相加,不计算进位输出,结果就是异或。

第二种理解方式是把它当“奇偶统计器”。把两个比特相加,结果是1就说明这一位上有奇数个1,结果是0说明偶数个1。这个性质是后面汉明码、奇偶校验的基础,你不用记住复杂公式,只记住“异或就是在数1的个数是奇数还是偶数”就够了。

第三种理解方式则是多项式系数相加(mod 2)。把二进制串a3a2a1a0写成多项式a3x³+a2x²+a1x+a0,模2加法就是两个多项式对应项系数相加再模2。例如1011⊕0110=1101,用多项式写就是(x³+x+1)⊕(x²+x)=x³+x²+1。这种视角的价值在于,它能解释模2运算和普通多项式除法之间的关联,是后面理解CRC的一把钥匙。

2.2 模2减法:瞧,它和加法长得一模一样

模2减法规则:0-0=0,1-0=1,1-1=0,0-1=1。你仔细看一下,除了符号不同,结果和加法真值表完全一样,差异只在“按位来看时加法和减法都等价于异或”。因为GF(2)中,-1等于+1(因为1+1=0,移项可得-1=1),所以减法和加法是同一种运算。

这个“减法等于加法”的性质,在实际工程中带来的直接收益是:设计校验电路时,接收端不需要区分“加法校验”还是“减法校验”,统统用一个异或门阵列搞定。我第一次算CRC时,看着手册里发送端“用0补位然后模2除”和接收端“直接对接收到的码字做模2除”的两种描述,绕了很久才明白,其实接收端做模2除法时,每一步的中间减法全部可以换成加法,结果不变。

有人会问:那模2减法有什么独立存在的必要?其实它更多的是数学形式上的需要。在推导汉明码校验矩阵、描述线性分组码时,我们经常写“校验方程”,用加号还是减号表达都不影响计算结果,但习惯上人们会按代数风格写成减法形式。你只要记住“模2减法就是异或”,所有相关计算立刻变得简单。

2.3 实际手算注意事项:位数对齐与符号

手算多位数模2加/减时,最大的坑就是进位和借位直觉。普通十进制加法看到1+1会下意识进位,模2运算必须忍住,每列独立异或。举个具体例子:1101⊕1011=0110,计算过程是:第3位1⊕1=0,第2位1⊕0=1,第1位0⊕1=1,第0位1⊕1=0。任何一位上出现“两个1”,结果直接归零,别往前一位送东西。

省略前导0的规则也要注意。很多人算完高位为零后直接去掉,这在数学上没错,但做校验时数位长度的约定很讲究:被除数的位长决定除法结果的位长。我建议手算时先把高位0补齐再操作,避免漏位。

3. 模2乘法:不产生进位的多项式相乘到底在算什么

3.1 手算竖式的变种

模2乘法规则可以归结为两句话:中间结果按位乘(就是逻辑与),部分积累加时用异或而非普通加法。以101×011为例:

1 0 1 × 0 1 1 ---------- 1 0 1 1 0 1 0 0 0 ---------- 1 1 1 1 (模2加法合并)

普通乘法里第二行要左移一位,第三行左移两位,最后把所有行加起来。模2乘法里左移策略完全一样,区别只在最后合并时用异或:第一行101、第二行1010、第三行0000,三者异或得到1111。这里有个看起来诡异的结果:11×11=101,对应十进制3×3=9,你们自己感受下,这已经不是十进制乘法能解释的范畴,它本质是多项式相乘。

多项式解释最优雅:101代表x²+1,011代表x+1,相乘得到x³+x²+x+1,对应二进制1111。你能看到,多项式乘法里的系数合并用了模2加法,这就是“无进位乘法”的由来。

3.2 模2乘法的用途:BCH码、加密与通信干扰设计

模2乘法看起来“算不准”,却在代数和工程上撑起了不少重要算法:

  • BCH码和Reed-Solomon码:这类纠错码的编码过程需要对信息多项式乘一个生成多项式,这个乘法就是模2(更一般地说,GF(2^m)上)的乘法。
  • LFSR的跳跃计算:当线性反馈移位寄存器需要一次性跳过多拍、生成某个位置的序列时,本质上是把状态寄存器视为多项式,做模2乘。
  • 经典加密中的混淆扩散:很多轻量级密码算法,比如AES内部的某些操作,底层的有限域乘法扩展自模2乘法的思想,只是它把每个字节当作GF(2^8)元素,规则更复杂。

初学者最大的误区是试图把模2乘法的结果和十进制乘法对应起来。它俩没任何数值上的等价关系,模2乘法关心的是“多项式相乘后的系数分布”,不是“数值相乘的积”。理解到这一层,你再去看BCH码的编码公式,会顺畅很多。

3.3 一个容易忽视的细节:乘法结果的最高位

普通乘法里,n位乘m位结果最多n+m位;模2乘法同样如此,但当你把乘法用在编码算法里时,通常会对结果做一个截断。比如某些生成多项式设计里,乘完后会“丢弃最高项”,只保留低n位,这实际上是模一个特定多项式的操作,已经进阶到“模2多项式乘法取余数”的范畴。我提醒你:很多资料里写“乘”其实是“乘后取模”,不仔细看上下文,会拿错中间数据。

这种“乘法 + 取模”的组合,和普通算术里的同余运算非常像。你可以把它理解为:先做一个无进位多项式乘法,然后除以一个固定的生成多项式,只留下余数。若没理解这层,后面看NTT(数论变换)或者CRC计算都会犯迷糊。

4. 模2除法:CRC校验码的计算基石

4.1 长除法的每一步为什么是“异或”

模2除法更像普通长除法,但规则变成了“被除数当前位对齐除数,用异或代替减法”。核心步骤如下:

  1. 在信息位后面补r个0,r是除数的位数减1(对于CRC,这个除数叫生成多项式)。
  2. 从被除数最高位开始,找到第一个为1的位,与除数最高位对齐。
  3. 对这一段做模2减法(等价于异或)。
  4. 把结果写下来,继续下一步。

举个例子:被除数110101,除数1011(4位,r=3),求余数。

1 0 1 1 (商,其实我们不关心) ---------- 1011 ) 1 1 0 1 0 1 1 0 1 1 --------- 1 1 0 0 1 0 1 1 --------- 1 1 1 1 1 0 1 1 --------- 1 0 0 0 1 0 1 1 --------- 1 1 <- 余数

但这里有个严重的问题:上面的写法容易让人误以为和普通除法一样,用“试商法”,实际模2除法里商的每一位由当前被除数最高位直接决定:当前最高位是1,商位就为1;是0,商位就为0(这时直接把除数左移成对应长度的0,等同于跳过)。所以实现时,通常用移位寄存器逐位处理,而不是像上面这样写完整的长除法。

4.2 模2除法手算的完整CRC例子

假设要发送的信息为1011,生成多项式为10011(对应x⁴+x+1),则补4个0后得到10110000。除的过程:

  • 第一步:10110的前四位1011 vs 10011,异或得到00101,剩余位0。
  • 第二步:有效位从00101开始,最高位为0则跳过,直到遇到1,相当于对应的商位为0,继续下拉。
  • 继续处理直到被除数全部消耗,最后的余数就是CRC校验值。

我算出来余数为1001。那么发送的码字就是1011 1001(信息位+余数)。接收端把整个10111001除以同一个生成多项式,如果余数为0,说明没有检测到错误;余数非0,则说明数据被篡改或发生传输错误。这一步是所有CRC校验算法的基础逻辑。

4.3 余数的位数陷阱:不是所有余数都叫CRC

初学最常见的错误之一,是补0个数没弄清楚。生成多项式r位时,补0的个数是r-1,不是r。以生成多项式10011为例,它是5位,所以补4个0。很多手册直接说“生成多项式为0x13,多项式最高次为4,因此r=4”,这里的r是指多项式的次数,而不是二进制位长度。如果你按最高位补齐,就会多补1位,导致整个校验码完全错误。

另外,余数是r-1位还是r位?当被除数补了r-1个0后,最终余数最多是r-1位。以上面的例子,生成多项式5位,余数最多4位,正好可以和信息位拼接成“信息位+4位校验位”的完整码字。这个“发送码字长度 = 原信息长度 + 生成多项式次数”的关系,是整个CRC编码里最重要的一条准则。

5. 从手算到程序:实现模2运算的代码思路与查表优化

5.1 基本位运算实现

不要以为必须写一个复杂的多项式计算库,模2运算用C语言位运算十几行就搞定了。

模2乘法:

uint32_t gf2_mul(uint32_t a, uint32_t b) { uint32_t result = 0; while (b) { if (b & 1) result ^= a; // 异或累加 a <<= 1; // 左移一位 b >>= 1; } return result; }

这段代码模拟了手算竖式:b的最低位决定要不要把当前a累加到结果,a每次左移相当于竖式里的“部分积左移一位”。

模2除法(取余):

uint32_t gf2_mod(uint32_t dividend, uint32_t divisor) { int shift = 0; uint32_t tmp = divisor; while (tmp < dividend) { tmp <<= 1; shift++; } for (; shift >= 0; shift--) { if (dividend & (1u << (shift + __builtin_clz(divisor) ... ))) dividend ^= (tmp >> shift) ??? } }

这段看得头大?没关系,因为工程上根本没人每次手写逐位异或。标准做法是表驱动:预先把被除数的一个字节(8位)和生成多项式组合算好256种余数,存成表,然后每个字节查表处理。这就是CRC查表法的核心思路。我实测下来,同样是在STM32上算CRC32,逐位法要几毫秒,查表法只需几十微秒,差距非常大。

5.2 两种常见实现风格的对比

实现方式核心思想优点缺点
逐位算法模拟长除法,每次处理1比特代码简单,适合教学和验证慢,数据量大时消耗CPU
查表法预计算256个表项,每次处理1字节快,适合嵌入式实时场景需要额外RAM存表,需注意反射/非反射问题

不夸张地说,我见过至少三个工程师被CRC的反射(reflected)和非反射模式坑过。同一份查表代码,参数里多了一个“输入是否反射”的开关,结果就完全不同。这些参数本质上不影响模2运算的数学定义,但影响你按什么顺序把字节喂进除法器。新手手算的时候可以忽略,一旦上代码调通信协议,就得仔细核对规范里的CRC参数模型。

5.3 从手算到程序的验证技巧

我每次写完CRC或LFSR相关代码,不会直接上板测。我的习惯是:

  1. 先找一份已知正确结果的测试向量,比如IEEE 802.3 CRC32的标准测试:输入123456789,结果是0xCBF43926。
  2. 用脚本或计算器手算一遍小数据,比如前面提到的信息位1011、生成多项式10011,验证输出1001。
  3. 再把代码里所有中间状态打印出来,和手算步骤逐位比对。

这一步虽然费时间,但能快速定位实现里“左移方向”“初始值”“异或顺序”的问题。别问我怎么知道的,当年调CRC16时整整卡了一个礼拜,最后发现只是初始寄存器应该填0xFFFF而不是0x0000。

6. 那些藏在模2运算背后的知名应用:LFSR、汉明码与生成多项式

6.1 LFSR:一条异或链就能生成伪随机序列

线性反馈移位寄存器(LFSR)看起来是数字电路,核心其实就是一个反复做模2除法的机器:把寄存器的某些位抽出来做反馈,本质上是构造一个多项式除法器。你选定一个本原多项式作为反馈系数,寄存器每移位一次,等效于状态多项式乘以x后对生成多项式取模。这样生成的0/1序列具有伪随机性,被广泛用在扩频通信、测试码生成、白噪声模拟中。

LFSR和CRC有极强的亲缘关系:很多CRC硬件模块,内部就是一个LFSR。换句话说,CRC编码器就是一个时序化的模2除法电路。理解了这个共通性,从软件CRC到FPGA实现的调试思路就能互相借鉴。

6.2 汉明码:用一组异或方程定位哪一位错了

汉明码是历史上第一个实用的纠错码,它用一组校验位对信息位进行监督。校验位的计算就是模2加法——即便这种计算方式被包装成“校验方程”,本质仍然是数一数该组数据里有奇数个还是偶数个1。

举个例子,经典的(7,4)汉明码:4位信息d1~d4,3位校验p1~p3。校验位计算公式为:

  • p1 = d1 ⊕ d2 ⊕ d4
  • p2 = d1 ⊕ d3 ⊕ d4
  • p3 = d2 ⊕ d3 ⊕ d4

接收端收到7位后,重新计算三组校验方程,根据哪几组校验失败,就能定位出是第几位出了差错。这套定位逻辑本质上是在解一组GF(2)上的线性方程,矩阵形式就是校验矩阵H。如果你熟悉模2乘法,还能看到汉明码的码字生成其实也是一个生成矩阵G作用在信息向量上的过程,也就是模2矩阵乘法。

6.3 生成多项式的选择:好的多项式决定了算法好不好用

整个模2除法在CRC中最关键的变量,就是生成多项式本身。为什么有的多项式被广泛采用,比如CRC-16/IBM的0x8005、CRC-32/IEEE的0x04C11DB7?因为它们经过了大量数学分析和工程验证,错误检测能力、碰撞概率、最大可检测突发错误长度都做得比较好。

现场调试时,很多人遇到“CRC偶发校验不过”,第一反应是代码有bug,其实问题经常出在生成多项式不匹配:对端设备用的多项式和你用的不一样,或者初始值/结果异或值不同。这些参数在模2运算里不显眼,但在具体协议里是必须严格对齐的元数据。我的建议是:工程开头就把“多项式、初值、输入反射、输出反射、结果异或值”这五项统统写进设计文档,别让后人拿代码猜来猜去。

7. 进阶思考:有限域GF(2)与GF(2^m)的关系,以及更广的模2应用

7.1 把模2运算扩展成字节级运算

你可能已经发现,上面所有的模2运算都是针对单个bit的。但工程中的RS码、AES算法,经常提到的却是“字节在GF(2^8)上的运算”。它和本文的模2运算什么关系?

我先说结论:GF(2^8)里的每个元素本身就是一个8比特二进制数,可以看作一个多项式;元素之间的加法就是逐比特异或,就是本文的模2加法;但乘法不再是单纯的模2多项式乘法,而是“模2多项式乘法再模一个8次不可约多项式”。换句话说,GF(2^8)乘法是广义的模2运算——加法和减法仍然是异或,乘法多了一个“取余不可约多项式”的环节。这就是为什么你会看到AES加密里字节相乘的结果十六进制下看起来“不按常理出牌”。

7.2 经典算法中的模2思想

  • 奇偶校验码:最简单的错误检测,发送前算所有数据位异或结果,接收端再算一遍。如果你能接受“异或就是模2和”的观念,会发现奇偶校验本质上就是在GF(2)上求和。
  • 线性反馈移位寄存器(LFSR):前面说过,状态转移本质上是有限域上的乘法。
  • 无线通信中的加扰/解扰:很多标准里的扰码器就是LFSR,原因之一是模2运算在硅片上实现极便宜。
  • ZUC、SNOW这类序列密码:其线性部分大多建立在GF(2)或GF(2^32)上,用的还是模2加法和GF上的乘法。

看多了你会发现,模2运算是一座很基础的桥梁。从简单的校验位到复杂的密码算法,最底层无非“异或、移位、取模”这三个动作在组装。理解了它,再去学那些看似高深的东西,会有一种“原来还是老朋友”的爽快感。

7.3 我自己对学习顺序的建议

如果你现在刚入门,我的建议是先别急着背公式。

  • 第一步,把本文的手算例子自己各算一遍,感受“无进位加法”和“异或减法”。
  • 第二步,用C语言写一个最朴素的逐位CRC函数,不查表、不优化,纯粹模拟长除法。
  • 第三步,找一个标准协议(比如Modbus CRC16)参数,用咱们前面提的测试向量验证代码。
  • 第四步,再回头学LFSR和汉明码,你会发现它们的数学骨架你已经全部打好了。

这个顺序是我自己走了弯路后总结出来的。当初我直接从查表法抄代码,结果遇到反射参数、初始值这类问题就完全懵了,因为我不懂底层原理。后来静下心从手算和逐位模拟开始,再回头看查表法,所有参数的意义一目了然。

技术这行,越是基础的东西越值得砸时间。模2运算就是这样一个“基础但不简单”的存在。希望这篇长文能帮你们绕开那些我曾经踩过的坑,需要讨论细节的朋友,评论区见。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询