校验码 码距 · 奇偶 · 海明 · CRC

软考 · 计算机组成原理 | 1.1.4 | 从「为什么要校验」讲到能自己算海明码和 CRC

⓪ 三种校验码,先有个印象

数据在传输/存储时可能出错(0 变 1、1 变 0)。校验码就是在原始数据里掺入一些“校验位”,让接收方能发现错误、甚至把错误改回来。

校验码能做什么开销典型用途
奇偶校验只能发现奇数个位出错,不能纠错1 位简单场合、串口
海明码能发现并纠正 1 位错(能定位到哪一位)k 位校验内存 ECC
CRC检错能力很强,但不能纠错r 位网络通信、磁盘存储
🔑 三个都要用到一个根本概念:码距。先搞懂它,后面全都顺。

① 码距:合法编码之间“隔多远” 最核心的概念

先学会一件事:怎么比较两个码? 把它们上下对齐,一位一位比,数出不同的位数。

例:比较 0001 和 0101 0 0 0 1 0 1 0 1 ↑ 只有第 2 位不同 → 差 1 位 例:比较 0001 和 0010 0 0 0 1 0 0 1 0 ↑ ↑ 第 3、4 位都不同 → 差 2 位

那“码距”又是什么?

定义:一个编码系统里,任意两个合法编码之间,最少差多少位。

🔑 关键在“最少”两个字:不是随便挑一对,而是要把所有合法编码两两比较一遍,找出最像的那一对,那个最小的差值才是码距。

看一个具体例子。假设某个编码系统规定,只有这 4 个编码是合法的:

合法编码: 0000 0011 1100 1111 两两比较,全部列出来: 0000 vs 0011 → 差 2 位 0000 vs 1100 → 差 2 位 0000 vs 1111 → 差 4 位 0011 vs 1100 → 差 4 位 0011 vs 1111 → 差 2 位 1100 vs 1111 → 差 2 位 最像的一对(0000/0011)差 2 位 → 这套编码的码距 = 2

对比:如果合法编码是全部 16 个(就是普通二进制),那 0000 和 0001 只差 1 位,所以码距 = 1。教材说的“4 位 8421 码码距为 1”就是这个意思。

🔑 为什么码距 = 1 很危险?
因为任意一位出错,结果都会变成另一个合法编码——接收方一看“这是个正常码”,以为没错,完全发现不了。
🔑 码距决定检错、纠错能力(背这 3 条):
· 要检测出 e 个位出错 → 码距 ≥ e + 1 (码距2→检1位;码距3→检2位)
· 要纠正 t 个位出错 → 码距 ≥ 2t + 1 (纠1位至少码距3)
· 要同时检测 e 个、纠正 t 个 → 码距 ≥ e + t + 1

直观理解:两个合法编码离得越远(中间隔着越多的“错误点”),出错后就越不容易误撞到另一个合法编码。要纠错 1 位,码距至少得 3——这样即使偏了 1 位,离原来那个也还是最近的,能认回来。

自查:某编码系统码距为 4,最多能检测几位错?最多能纠正几位错?
点击看答案
检测:d ≥ e+1 → 4 ≥ e+1 → 最多检 3 位错。
纠正:d ≥ 2t+1 → 4 ≥ 2t+1 → 2t ≤ 3 → t ≤ 1.5 → 最多纠 1 位错。
(口诀:检错能力 = 码距 − 1;纠错能力 = 码距 ÷ 2 向下取整)

② 奇偶校验:最简单,只会“报错”不会“治病”

做法:在数据后面加 1 位校验位,让整个码里 1 的个数变成奇数(奇校验)或偶数(偶校验)。

数据 1011(数一数:1、0、1、1 → 有 3 个 1) 偶校验:要凑成偶数个 1 → 3 是奇数,还差 1 → 校验位补 1 → 码 = 10111(3+1 = 4 个 1,偶数)✓ 奇校验:要凑成奇数个 1 → 3 已经是奇数 → 校验位补 0 → 码 = 10110(3+0 = 3 个 1,奇数)✓
⚠️ 最容易搞混的一点:“偶校验”是让【数据 + 校验位】加起来 1 的总数是偶数,不是让校验位本身是偶数。
所以 1011 的偶校验位是 1(因为 3 是奇数,要补 1 才变偶数),数字看起来是 1,但总数 4 是偶数。

加了这个校验位,码距就从 1 变成了 2。按公式 d≥e+1,码距 2 能检测 1 位错。

⚠️ 能力边界:只能发现“奇数个”位出错(1、3、5…位),发现不了“偶数个”位出错(2 位同时错,1 的个数奇偶性不变)。而且不能纠错——只知道“有错”,不知道错在哪。

⚠️ 别和“二维奇偶校验”搞混(一个常被问懵的点)

“奇偶校验”其实有两种形态,能力完全不同:

类型校验位码距能纠错吗
简单奇偶(一维)只有 1 个2❌ 不能纠错(本文讲的就是这个)
二维奇偶(水平垂直)每行每列都有4✅ 能纠 1 位错

二维为什么能纠错?行、列两个方向交叉定位:

列1 列2 列3 行校验 行1: 1 0 1 | 0 行2: 0 0* 1 | 0 ← 行2 校验对不上 行3: 1 1 0 | 0 ----------------- 列校验: 0 1* 0 ← 列2 校验对不上 ↑ 行2报警 + 列2报警 → 交叉点 = 行2列2 = 出错位置 → 翻回来即可 ✅
🔑 记法:考试说“奇偶校验码距=2”,默认指一维简单奇偶 → 结论是“不能纠错”。
只有明确说“水平垂直/二维奇偶”,码距才是 4、才能纠 1 位。
输入数据(0/1):

③ 海明码:能发现、还能定位纠正 1 位错 考试计算重点

思路:在数据位之间插入 k 个校验位,让每个校验位“管”一群数据位(做异或)。出错时,几个校验位的结果组合起来,就能指出出错位置。

先搞懂公式 2k − 1 ≥ n + k(一步一步推)

① 每个校验位 = 一个侦探。它盯着自己管的那群位,最后报告一位结果:管的位都正常报 0,有问题报 1。

② k 个侦探 → k 位报告 → 共 2k 种组合。(k=4 时就是 16 种:0000~1111)

③ 为什么减 1?因为 0000 这一种被“没出错”占用了,不能再拿去表示“某位错”。所以只剩 2k − 1 种地址可用。

④ 要覆盖多少种“某位错”?一共 n+k 个位置(校验位自己也会错,也要能被定位),所以需要 n+k 个地址。

⑤ 两边对上:2k − 1 ≥ n + k。

代入 n = 8 试算: k=3: 2³−1 = 7 要覆盖 8+3 = 11 个位 → 7 ≥ 11 ? ✗ 不够 k=4: 2⁴−1 = 15 要覆盖 8+4 = 12 个位 → 15 ≥ 12 ? ✓ 够 → 所以 8 位数据需要 4 个校验位(16 种报告,扣掉全 0,剩 15 个地址盯 12 个位子,还多 3 个)
🔑 提炼成两句:① 全 0 留给“没出错”,所以要 −1;② 校验位自己也会错,所以是 n+k 不是 n。

再搞懂:校验位为什么放在 2 的幂位置?

因为 任何位置编号,都能拆成 2 的幂之和。拆出来的那几个 2 的幂,正好就是要管它的校验位。

位置 3 = 1 + 2 → 由 P1、P2 管 位置 5 = 1 + 4 → 由 P1、P3 管 位置 6 = 2 + 4 → 由 P2、P3 管 位置 7 = 1 + 2 + 4 → 由 P1、P2、P3 共同管 位置 11 = 1 + 2 + 8 → 由 P1、P2、P4 共同管 换成二进制看得更直观(位置 6): 6 = 0 1 1 0 ↑ ↑ bit 为 1 的位 → 由 P2、P3 管
🔑 一句话:位置编号的二进制里,哪几位是 1,就由哪几个校验位管。
正因为校验位放在 2 的幂位置,它才能“对上号”——P1 管所有编号 bit0=1 的位,P2 管 bit1=1 的位……
🔑 这也解释了纠错原理:位置 7 = 1+2+4,参与它的只有 P1/P2/P3 三个组。所以一旦位置 7 出错,只有这三个组会算出 1,拼起来 0111 = 7 —— G 值正好指回位置 7。
🔑 公式:2k − 1 ≥ n + k(n = 数据位数,k = 校验位数)。
即:总共 n+k 个位置,用 k 个校验位能区分出「没出错 + 每一位出错」共 n+k+1 种情况。

三条规则(背下来)

规则内容
① 位置校验位 P 放在编号是 2 的幂的位置:第 1、2、4、8… 位;数据位按顺序填剩下的位置
② 管辖第 i 个校验位,管所有「位置编号的二进制第 i 位是 1」的位
③ 校验偶校验:让每个校验位管的那些位异或结果为 0;按此反推校验位的值

以 8 位数据举例(k=4,因为 2⁴−1=15 ≥ 8+4=12)。位置对应关系:

📝 手把手:用 5 位数据走完整流程(比 8 位好算)

设数据 D4 D3 D2 D1 D0 = 1 0 1 1 0(即 D0=0, D1=1, D2=1, D3=0, D4=1)。跟着 5 步走:

第 1 步 · 定 k 和位置:n=5 → 2k−1≥5+k → k=4。校验位占 1、2、4、8 号,数据填其余:

位置: 1 2 3 4 5 6 7 8 9 内容: P1 P2 D0 P3 D1 D2 D3 P4 D4

第 2 步 · 每个位置号转二进制,拆出"谁管谁"(第 1 位=1→P1,第 2 位=1→P2,第 3 位=1→P3,第 4 位=1→P4):

位置 3 = 0 0 1 1 → P1,P2 管(D0) 位置 5 = 0 1 0 1 → P1,P3 管(D1) 位置 6 = 0 1 1 0 → P2,P3 管(D2) 位置 7 = 0 1 1 1 → P1,P2,P3 管(D3) 位置 9 = 1 0 0 1 → P1,P4 管(D4) 汇总(去掉校验位自己那几行): P1 管:D0, D1, D3, D4 ← 出现 P1 的那些行 P2 管:D0, D2, D3 P3 管:D1, D2, D3 P4 管:D4 ← 位置9=1001,第4位=1→P4

第 3 步 · 校验位 = 它管的数据位异或(异或:相同得0,不同得1):

P1 = D0⊕D1⊕D3⊕D4 = 0⊕1⊕0⊕1 = (0⊕1)⊕0⊕1 = 1⊕0⊕1 = 1⊕1 = 0 P2 = D0⊕D2⊕D3 = 0⊕1⊕0 = 1 P3 = D1⊕D2⊕D3 = 1⊕1⊕0 = (1⊕1)⊕0 = 0⊕0 = 0 P4 = D4 = 1

第 4 步 · 填回座位,得到海明码:

位置: 1 2 3 4 5 6 7 8 9 内容: P1 P2 D0 P3 D1 D2 D3 P4 D4 值: 0 1 0 0 1 1 0 1 1 海明码(位置1→9)= 0 1 0 0 1 1 0 1 1

第 5 步 · 接收方验证(把每组重新异或,G 值十进制 = 出错位置,全 0 = 没错):

G1 = P1⊕D0⊕D1⊕D3⊕D4 = 0⊕0⊕1⊕0⊕1 = 0 G2 = P2⊕D0⊕D2⊕D3 = 1⊕0⊕1⊕0 = 0 G3 = P3⊕D1⊕D2⊕D3 = 0⊕1⊕1⊕0 = 0 G4 = P4⊕D4 = 1⊕1 = 0 → G4G3G2G1 = 0000 → 无错 ✓
自己练一个:数据 D4 D3 D2 D1 D0 = 0 1 0 1 1(D0=1, D1=1, D2=0, D3=1, D4=0),求 P1~P4。
点击看答案
P1 = D0⊕D1⊕D3⊕D4 = 1⊕1⊕1⊕0 = 1
P2 = D0⊕D2⊕D3 = 1⊕0⊕1 = 0
P3 = D1⊕D2⊕D3 = 1⊕0⊕1 = 0
P4 = D4 = 0
(异或从左到右一路算,顺序无所谓)
输入 8 位数据 D7~D0:

怎么检出并纠正错误?接收方把每个校验组重新异或一遍,得到 G4 G3 G2 G1:

🔑 G4G3G2G1 的十进制值 = 出错的位置;若全为 0,说明没出错。找到位置后,把那一位取反,就纠正了。
点一位模拟“传输中出错”(再点一次取消):

④ CRC:检错能力最强,通信/存储的常客

做法:选一个生成多项式 G(x),用它在数据后面算出若干个校验位,拼成 CRC 码。

记号:生成多项式 G(x) = x³ + x + 1 ←→ 二进制 1011(有几项,对应位就是 1) 数据 k 位 + 校验 r 位 = 总共 n 位,记作 (n, k) 码 本例 r=3(多项式最高次是 x³),所以校验位 3 位

关键:模 2 运算(就是异或)

🔑 模 2 加减 = 按位异或,不进位、不借位:
0⊕0=0 1⊕1=0 0⊕1=1 1⊕0=1。所以“减”和“加”在这里是一回事。

编码三步

  1. 补 0:数据后面补 r 个 0(r = 生成多项式位数 − 1)
  2. 做模 2 除法:补 0 后的数 ÷ 生成多项式(用异或,不借位)
  3. 取余数:余数就是校验位;把它替换掉刚才补的那 r 个 0 → 得到 CRC 码

🐣 小白版手把手:数据 1101 到底怎么算出 CRC 码

先建立直觉:CRC 本质就是一次「带余数的除法」,只不过和小学除法有一点不同。

小学带余数除法: 13 ÷ 4 = 3 …… 余 1 (用「减法」) CRC 除法:形状一样,但「减法」换成「异或」 1 1 0 1 (异或:相同得0,不同得1) 1 0 1 1 ─────── 0 1 1 0
🔑 为什么用异或?因为电路做异或特别简单——一位一位算,不用管进位/借位。

第 1 步 · 数据后面补 3 个 0(校验位几位?看多项式最高次:G(x)=x³+x+1 最高 3 次 → 补 3 个 0):

数据: 1 1 0 1 补0: 1 1 0 1 0 0 0 ← 先占好 3 个位置,等会儿用余数填

第 2 步 · 异或长除法(规则:看当前最左位,是 1 就把除数对齐异或一次;是 0 就跳过往右挪):

第1次 最左位是1 → 异或 1 1 0 1 0 0 0 1 0 1 1 ───────────── 0 1 1 0 0 0 0 (1101⊕1011=0110) 第2次 最左的1在第2位 → 对齐异或 0 1 1 0 0 0 0 1 0 1 1 ───────────── 0 0 1 1 1 0 0 (1100⊕1011=0111) 第3次 最左的1在第3位 → 对齐异或 0 0 1 1 1 0 0 1 0 1 1 ───────────── 0 0 0 1 0 1 0 (1110⊕1011=0101) 第4次 最左的1在第4位 → 对齐异或 0 0 0 1 0 1 0 1 0 1 1 ───────────── 0 0 0 0 0 0 1 (1010⊕1011=0001) 停下!剩下的位数不足 4 位(除数是4位),不能再除了

第 3 步 · 取余数 = 最后 3 位:

剩下的: 0 0 0 0 0 0 1 └─┬─┘ 最后 3 位 = 0 0 1 ← 这就是校验位

第 4 步 · 把余数填回去,拼成 CRC 码:

数据 : 1 1 0 1 校验位 : 0 0 1 ───────────────────────── CRC 码 : 1 1 0 1 0 0 1 = 1101001

接收方怎么查错?拿整串再除以同一个多项式:

1101001 ÷ 1011(异或除法) 余数 = 000 → 能整除 → 没出错 ✓ 余数 ≠ 000 → 除不尽 → 出错了 ✗

🐣 验证的「异或除法」到底怎么除?(一步步)

规则只有一个动作,反复做:瞄当前最左位,是 1 就把除数对齐异或;是 0 就跳过往右挪一位。

被除数:1 1 0 1 0 0 1 除数:1 0 1 1 第1步 最左位是1 → 对齐异或 1 1 0 1 0 0 1 1 0 1 1 ───────────── 0 1 1 0 0 0 1 (1⊕1=0, 1⊕0=1, 0⊕1=1, 1⊕1=0) 第2步 最左是0,跳过;下一个1在第2位 → 除数右移1格异或 0 1 1 0 0 0 1 1 0 1 1 ───────────── 0 0 1 1 1 0 1 (1⊕1=0, 1⊕0=1, 0⊕1=1, 0⊕1=1) 第3步 前两位是0,跳过;1在第3位 → 右移异或 0 0 1 1 1 0 1 1 0 1 1 ───────────── 0 0 0 1 0 1 1 (1⊕1=0, 1⊕0=1, 1⊕1=0, 0⊕1=1) 第4步 前三位是0,跳过;1在第4位 → 右移异或 0 0 0 1 0 1 1 1 0 1 1 ───────────── 0 0 0 0 0 0 0 (1⊕1=0, 0⊕0=0, 1⊕1=0, 1⊕1=0) 停!除数4位,已到第4位,再往右不够对齐了 最后 3 位(校验位位数)= 0 0 0 → 余数 = 000 → 能整除,没出错 ✓

对比:如果某一位真的出错了,余数就会非 0(脚本实测):

正确码 1101001 ÷ 1011 → 余数 000 → 没出错 ✓ 第3位错 → 1111001 ÷ 1011 → 余数 110 → 出错 ✗ 第1位错 → 0101001 ÷ 1011 → 余数 101 → 出错 ✗
🔑 「整除」的准确含义:不是数学上除尽,而是做异或长除法,最后余数全为 0。
发送方造的码天生余数为 0;接收方一除,余数变非 0 就说明中途被破坏了。
🔑 为什么「补0」这招管用?因为 数据补0 = 商×除数 ⊕ 余数,移项得 数据补0 ⊕ 余数 = 商×除数。
而「数据补0 ⊕ 余数」正好就是「把补的 0 换成余数」→ 所以「数据+余数」一定能被整除。
⚠️ CRC 只能检错,不能纠错——除不尽只告诉你「坏了」,不告诉你是哪一位。(这点和海明码相反)
数据: 生成多项式:
自查:数据 1101、G(x)=x³+x+1(1011),CRC 码是多少?
点击看答案
① 补 3 个 0 → 1101000
② 模 2 除法(异或)得余数 001
③ CRC 码 = 数据 + 校验位 = 1101 + 001 = 1101001
验证:1101001 ÷ 1011 模 2 余数为 000(能整除)✓
(点上面的“逐步计算”能看到每一步)

⑤ 一页总结

考点记住这一句
码距任意两个合法编码最少差几位
检错 e 位码距 ≥ e + 1
纠错 t 位码距 ≥ 2t + 1
奇偶校验(一维)码距 2,检 1 位错,不能纠错(偶数个错检不出);二维(水平垂直)码距 4、可纠 1 位
海明码2k−1 ≥ n+k;校验位在 2 的幂位置;G 值 = 出错位置
CRC补 r 个 0 → 模 2 除(异或)→ 余数即校验位