汉明码看信道编码纠错原理)
信道编码这名字听起来很学术但只要调过一条动不动就误码的无线链路或者研究过SSD里为什么存储要留校验位很快就会撞上它。我去年调一套短波数传设备时发射功率提到标称极限误码率还徘徊在10^-3量级换了个编码方案后功率没动误码率直接掉到10^-6以下——这个体感强烈到让人想给信道编码写感谢信。而所有信道编码里最值得先吃透的就是线性分组码。这篇文章是写给三类人的刚接触信息论的在校生、做通信基带或存储系统的工程师、以及准备面试时想快速理清纠错编码脉络的自学者。我不会堆一堆抽象定理而是从一个具体的(7,4)汉明码入手把生成矩阵、校验矩阵、伴随式译码这些核心概念一个不落地讲明白最后再聊聊实际工程里那些文档不会写、但一定踩得上的坑。你只需会一点矩阵乘法和二进制异或就能跟着推完整个流程。1. 为什么说线性分组码是信道编码的必修第一课1.1 信道编码到底在干什么先回答一个听起来简单但很多人说不清的问题信道编码为什么能纠错根本原因只有一句话——发送端往数据里加入了受控的冗余接收端利用这份冗余来发现甚至纠正传输中发生的错误。没有冗余就没有纠错能力这是整个编码理论的底层逻辑。举个例子你要发送4比特信息直接发4比特接收端拿到什么就是什么错一位也没人知道。但如果我在4比特后面附加3个校验比特构成7比特的码字只要这7比特之间满足某种预设的约束关系接收端就可以拿收到的7比特去验算这些约束是否成立。约束被破坏就说明传输过程中出了错甚至可以定位是哪一位错了。问题变成这个约束关系应该怎么设计才能在增加冗余和纠错能力之间取得最优平衡线性分组码就是回答这个问题的第一套系统化方案。1.2 线性分组码在整个信道编码版图里的位置要理解线性分组码的分量先看一张我在学习时自己整理的关系表。信道编码按结构可以粗略分成几大类编码类型基本思路典型代表译码方式线性分组码每k比特映射成n比特码字码字集合构成线性空间汉明码、BCH码、RS码、CRC伴随式译码、代数译码卷积码用移位寄存器持续输出编码比特有记忆性卷积码、Turbo码分量码Viterbi译码、BCJR稀疏图码用稀疏校验矩阵定义长分组码LDPC码、Turbo码、Polar码置信传播迭代译码、SC译码这里有一条很多人忽略的暗线LDPC码和Polar码本质上也属于线性分组码它们的区别主要在于校验矩阵的构造方式和译码算法而不是是不是分组码。所以你把线性分组码吃透了后面学LDPC、Turbo、Polar时很多概念是直接平移过去的。这也是我为什么强烈建议先学线性分组码它数学结构简洁又足够支撑起对现代编码的完整理解。别一上来就啃Polar码的极化权重计算先搞清楚c mG里的G矩阵为什么是那个样子比什么都重要。2. 搭一个(7,4)汉明码生成矩阵、校验矩阵和第一次编码2.1 设计三个校验方程从需求反推码字结构(7,4)汉明码是最经典的线性分组码输入4比特信息输出7比特码字其中3比特是校验位。它是最小的能纠正单比特错误的完备码也是理解一切线性分组码的最佳切片。先定义输入信息向量为m [m1 m2 m3 m4]输出码字为c [m1 m2 m3 m4 p1 p2 p3]前4位直接放原始信息后3位是校验位。这种信息位原样保留的码叫系统码工程上非常好用因为接收端纠错后直接把前几位抠出来就是原始数据不用额外变换。现在设计校验方程。校验位必须是信息位的某种线性组合我实际使用的是下面这组约束p1 m1 ⊕ m3 ⊕ m4 p2 m1 ⊕ m2 ⊕ m3 p3 m2 ⊕ m3 ⊕ m4其中⊕表示模2加法也就是异或运算。为什么选这组而不是别的因为它们满足一个关键性质任意两个校验方程之间的信息位系数向量都不同且不会由另一对组合冗余推出。这保证了后面生成的码有足够的最小距离。初学阶段你不用纠结这个选择先把方程组抄下来等看到后面H矩阵列的性质就明白了。2.2 生成矩阵G把k比特映射成n比特的线性变换有了校验方程生成矩阵G几乎是白送的。G是一个4×7矩阵每一行对应一个信息位置mi 1、其余信息位为0时的完整码字。以第一行为例m [1 0 0 0]时校验位计算为p1 1 ⊕ 0 ⊕ 0 1 p2 1 ⊕ 0 ⊕ 0 1 p3 0 ⊕ 0 ⊕ 0 0所以第一个基向量映射成的码字是[1 0 0 0 1 1 0]。同理可以写出另外三行得到G [ 1 0 0 0 | 1 1 0 0 1 0 0 | 0 1 1 0 0 1 0 | 1 1 1 0 0 0 1 | 1 0 1 ]左边4×4是单位阵保证系统码结构右边4×3来自于校验方程的系数一般记为P矩阵。任意4比特信息向量m编码后的码字就是c mG只不过所有加法都是模2加法。这简洁得令人舒适整个编码过程退化成了一个矩阵乘法。线性分组码的线性二字指的就是全部合法码字集合在GF(2)上构成一个线性子空间而G的4行恰好是这个子空间的一组基。2.3 校验矩阵H接收端用来找茬的方程组生成矩阵负责编码校验矩阵H负责在接收端验算。H是一个3×7矩阵它与G满足一个核心关系G × H^T 0 全零矩阵对于我上面用的这组约束对应的校验矩阵是H [ 1 0 1 1 | 1 0 0 1 1 1 0 | 0 1 0 0 1 1 1 | 0 0 1 ]它的左边3×4部分就是P矩阵的转置右边3×3是单位阵。为什么H要长这样因为任何一个合法码字c都满足cH^T 0而c mG所以G × H^T 0是必要条件。H矩阵的行本质上是三组独立的奇偶校验方程把接收向量乘以H的转置看结果是不是全0就能判断这个向量是不是合法码字。我建议你自己动手验一次取上面G的第一行[1 0 0 0 1 1 0]与H的三行分别做点积再取模2第一行1*1 0*1 0*0 0*1 1*1 1*0 0*0 11 0 第二行1*1 0*1 0*1 0*0 1*0 1*1 0*0 11 0 第三行1*0 0*1 0*1 0*1 1*0 1*0 0*1 0这个零向量的结果就是合法码字的数学签名。2.4 完整手算一个编码过程拿m [1 0 1 1]实操一遍。按照校验方程p1 1 ⊕ 1 ⊕ 1 1 p2 1 ⊕ 0 ⊕ 1 0 p3 0 ⊕ 1 ⊕ 1 0所以输出码字为c [1 0 1 1 1 0 0]。用矩阵乘法验证c mG也就是把G的第1行、第3行、第4行做模2相加[1 0 0 0 1 1 0] ⊕ [0 0 1 0 1 1 1] ⊕ [0 0 0 1 1 0 1] [1 0 1 1 1 0 0]结果一致。所有16个4比特信息向量都能这样映射成16个合法7比特码字而这16个码字只是128个7比特向量里非常稀疏的一小撮。3. 最小汉明距离决定纠错上限一个数字看清能力边界3.1 汉明距离与最小距离的定义两个等长向量之间汉明距离就是它们对应位置上不同比特的个数。比如[1 0 1 1 0]和[1 0 0 1 1]在第3位和第5位不同距离就是2。对一个分组码来说把所有合法码字两两之间的距离都算出来取最小值就是最小汉明距离d_min。这个数字是衡量一个码纠错能力的唯一核心指标。为什么纠错能力和d_min挂钩想象接收端拿到一个向量r它可能是在某个合法码字c的基础上翻了t个比特得到的。只要t足够小r与发送码字c的距离是t但r与其他任何合法码字c的距离至少是d_min - t。如果t (d_min - t)即2t d_min那么r离c最近离所有其他码字都更远按最近距离判决就能正确还原。于是就有了那条所有资料都会出现的核心公式纠错能力 t floor((d_min - 1) / 2) 检错能力 e d_min - 13.2 (7,4)汉明码的d_min为什么刚好等于3那么我上面构造的这个(7,4)汉明码d_min是多少答案是3。推导过程很漂亮而且直接揭示了H矩阵设计的奥秘。一个码字c合法等价于cH^T 0而c的汉明重量非零比特的个数等于它对应H矩阵中哪些列被模2加成了零向量。若存在重量为1的码字意味着H矩阵中有一列全零若存在重量为2的码字意味着H矩阵中有两列相同若存在重量为3的码字意味着H矩阵中有三列模2相加为零。看我的H矩阵没有任何一列是全零任意两列都不相同所以不可能有重量为1或2的合法码字。但存在三列之和为零例如第1列、第5列、第6列[1 1 0] ⊕ [1 0 0] ⊕ [0 1 0] [0 0 0]对应的码字就是G的第一行[1 0 0 0 1 1 0]重量正好为3。于是d_min 3恰好满足2t 2 3所以t 1这个码能纠正任意单比特错误。这也是汉明码设计时列向量两两不同且非零这一要求的本质来源。理解了这一步再看很多教材里H矩阵的排列方式就不再是死记硬背了。3.3 别把纠错和检错混在一起算d_min 3时能纠1个错能检2个错但很多人误以为既能纠1个错又能检2个错这是不对的。如果要求同时纠t个错并检测到s个错约束条件要升级为d_min ≥ t s 1当d_min 3t 1时最多只能同时做到s 1。也就是说你可以纠正1个错的同时检出1个错但如果你想让接收端发现2个错误并报告而不是错误地纠正成另一个码字这个码做不到。因为在出现2个错误的情况下接收向量可能与某个合法码字距离为1最近距离译码器会把它纠正成另一个码字反而制造了一个未检出的错误。我在实际通信链路里就犯过这个错误天真地以为汉明码能纠正1个错检测出所有2个错直到用蒙特卡洛仿真数误帧率时发现有一批恰好翻转2比特的帧被静默误纠了。这个边界条件一定要记牢。4. 伴随式译码从接收向量反推错误位置4.1 伴随式的定义为什么它只和错误有关编码讲完了现在进入译码。设发送码字为c信道引入的错误图样为e错误位置为1其余为0接收向量r c ⊕ e。接收端不知道c也不知道e它只能算一个东西——伴随式S rH^T因为cH^T 0所以S (c ⊕ e)H^T cH^T ⊕ eH^T eH^T这是整个硬判决译码的基石伴随式S与发送了哪个码字无关只与错误图样e有关。换句话说不管发送的是16个合法码字中的哪一个只要错误图样相同接收端算出来的伴随式就相同。这意味着译码器可以先把错误图样 → 伴随式的对应关系做成一张表收到r后只要算一下S查表得到错误图样再翻转对应比特就算完成纠错。4.2 单比特错误时伴随式就是H矩阵的对应列这节单独拿出来讲是因为它在手算和调试时特别有用。如果错误发生在第i位即e只有第i位是1那么S eH^T H矩阵的第i列所以(7,4)汉明码的译码规则可以极度简化算出3比特伴随式S看它等于H矩阵的哪一列那一列的列索引就是出错位置直接翻转即可。H矩阵越规则查列就越方便。很多人喜欢把H矩阵的列排列成十进制1~7的二进制形式就是为了让伴随式直接读成出错的比特位置。当前构造的H矩阵列向量与位置对照如下错误比特位置H矩阵对应列伴随式S1col1[1 1 0]2col2[0 1 1]3col3[1 1 1]4col4[1 0 1]5col5[1 0 0]6col6[0 1 0]7col7[0 0 1]注意伴随式向量写成行形式还是列形式取决于H矩阵的排布习惯本质是同一回事。调试代码时最容易出错的就在这个转置关系上后面会专门说。4.3 一个完整的单比特错误译码实例继续用前面的例子。发送码字c [1 0 1 1 1 0 0]假设信道把第5位翻转错误图样e [0 0 0 0 1 0 0]接收向量r c ⊕ e [1 0 1 1 0 0 0]接收端计算伴随式。因为cH^T 0我直接算eH^T也就是取H矩阵第5列S [1 0 0]^T查表对应第5位。把r的第5位从0翻回1得到[1 0 1 1 1 0 0]然后取前4位[1 0 1 1]译码完成。为了让你信服这个流程真的能直接用r算我也可以从r出发硬算一遍。r的非零位在第1、3、4位所以S col1 ⊕ col3 ⊕ col4 [1 1 0] ⊕ [1 1 1] ⊕ [1 0 1] [1 0 0]和eH^T的结果完全一致。这就是线性性质带来的红利发送码字的那部分贡献永远被零空间吸收接收端的计算从来不需要知道发送码字本身。4.4 用十几行Python实现一个完整译码器(7,4)汉明码的查表译码代码用numpy写其实很短。下面这个版本我故意写得直白方便你对照上面的理论看import numpy as np # 校验矩阵 3x7行形式 H np.array([ [1, 0, 1, 1, 1, 0, 0], [1, 1, 1, 0, 0, 1, 0], [0, 1, 1, 1, 0, 0, 1] ], dtypeint) # 预计算单比特错误图样对应的伴随式表 syndrome_table {} for i in range(7): e np.zeros(7, dtypeint) e[i] 1 s H e % 2 # 矩阵乘法后取模2 syndrome_table[tuple(s)] i def decode_hamming(r): 输入7比特接收向量输出4比特信息向量 r np.asarray(r, dtypeint) s H r % 2 if np.all(s 0): return r[:4].copy() # 无错前4位就是信息位 idx syndrome_table.get(tuple(s)) if idx is not None: r_hat r.copy() r_hat[idx] ^ 1 # 翻转出错比特 return r_hat[:4].copy() # 取出信息位 # 超出单比特纠错能力比如2比特错误这里只做告警 return r[:4].copy() if __name__ __main__: # 测试发送 [1 0 1 1]得到码字 [1 0 1 1 1 0 0] c np.array([1, 0, 1, 1, 1, 0, 0]) # 信道翻转第5位 r np.array([1, 0, 1, 1, 0, 0, 0]) m_hat decode_hamming(r) print(译码结果:, m_hat) # 输出 [1 0 1 1]这段代码里最需要注意的就是H r % 2这一步。numpy的是标准矩阵乘法得到的是整数结果如果不取模2伴随式会变成2、3这类值。取模2之后才是GF(2)上的伴随式。我在调试时还遇到过把H定义成7×3形状导致维度对不上报错的情况记住H的行是校验方程、列是码字位置就不会搞反。5. 译码实现中的几个翻车点模2运算、查表边界与同步5.1 GF(2)里的加法是全宇宙最容易踩的坑线性分组码一切运算都在GF(2)有限域上进行在这个域里加法和减法本质都是异或乘法就是逻辑与。很多第一次写译码程序的人会下意识地按整数运算处理结果在天真的地方翻车。比如校验位计算p1 m1 ⊕ m3 ⊕ m4如果在真实代码里用m[0] m[2] m[3]这种普通加法会得到0、1、2、3这些整数取模2之后2就会变成01就会变成1结果碰巧对。但一旦你把逻辑误写成判断结果是否为0/1而忘了取模2或者把加法写成了逻辑或结果就会彻底错乱。最典型的是把两个1相加用or得到1而正确的1 ⊕ 1 0。我的建议是所有涉及码字的运算一律用异或位运算而不是数学加法。Python里直接bit1 ^ bit2numpy里用np.bitwise_xor或干脆% 2兜底。宁可多写一个取模也千万别省。5.2 查表译码的适用范围错误图样不是随便选的前面代码里的syndrome_table只有8个条目全零图样加上7个单比特错误图样。这是因为(7,4)汉明码只能纠正单比特错误所以把伴随式空间里出现的8种可能值全映射到无错或单比特错的图样上恰好够用。但是对于更长、更复杂的线性分组码比如码长为31的汉明码单比特错误图样有31种但伴随式只有2^(31-26)32种仍然刚好够用。这就是汉明码被称为完美码的原因它的伴随式空间被无错和所有单比特错误图样恰好填满一个不剩。如果你自己设计一个d_min更大的线性分组码比如能纠2个错的BCH码查表范围就要扩大到所有重量≤2的错误图样。这种情况下标准阵列译码的查表规模会随n迅速爆炸所以工程上才会转向代数译码或者迭代译码。初学阶段做仿真先让查表范围覆盖目标纠错能力内的所有错误图样这是最稳妥的写法。5.3 码组同步、突发错误和交织器的关系这是我在实调链路时付出过代价的一课。线性分组码的一切前提是接收端必须知道每个码字的起始边界。如果接收到的比特流相对真实码字错位了1个比特那么你切出来的每个7比特块都不是合法码字伴随式会乱掉译码器会把正常数据也当成错误去纠正。我当时在短波链路上遇到的现象是加编码后误码率指标看起来更差了查了半天H矩阵和G矩阵都没问题最后发现是帧同步字在级联系统里被另一个模块提前剥离导致译码器拿到的数据没有对齐到码字边界。解决方式很朴实在帧结构里加入固定的同步头接收端先扫同步头锁定码字起点再送进译码器。另外线性分组码擅长对付随机独立错误但无线信道里常见的突发错误比如一阵干扰连续打掉好几个比特会让单比特纠错码完全失效。工程上应对突发错误的标准做法是交织发送端把多个码字的比特按一定顺序打乱重排让连续的突发错误在解交织后被分散到不同码字里每个码字只承担一两个错误再交给单比特纠错码处理。这个思路和线性分组码本身无关但凡是做实际链路的人迟早会碰到。6. 学完(7,4)汉明码之后下一步应该往哪走6.1 为什么长分组码不能照搬查表思路查表译码在n7时看起来很美好但这个美好是假象。假设你设计一个码长n256、能纠8个错误的线性分组码理论上可能的错误图样数量是所有重量≤8的组合数这个数字远超任何存储设备能承受的量级。更直接的问题是一般线性分组码的最大似然译码在码长增长时是NP难的不存在多项式时间的通用最优算法。所以编码理论后来的演进本质上都是在回答一个问题**如何构造出纠错性能接近香农极限、同时译码复杂度可控的码**为了做到这一点有两条路给码字加代数结构循环码、BCH码、RS码用多项式因式分解等数学工具快速译码或者给校验矩阵加稀疏结构LDPC码用图上的消息传递算法迭代逼近最优译码。这两条路都建立在线性分组码的基础上并没有推翻它。LDPC码依然是线性分组码只是它的H矩阵做得非常稀疏RS码依然是线性分组码只是把运算域从GF(2)扩展到了GF(2^m)。6.2 现代系统里你已经在用的线性分组码很多人觉得汉明码是教科书里的老古董但实际上它和它的亲戚们遍布现代硬件。DDR内存颗粒里的ECC普遍使用扩展汉明码的变体比如(72,64) SEC-DED码能在纠正单比特错误的同时检测双比特错误。SSD主控里的BCH码、LDPC码处理NAND闪存越来越严重的比特错误。5G NR的物理层控制信道里用了Reed-Muller码和CRC校验这些都是线性分组码家族的成员。深空通信经典的RS码与卷积码级联方案更是把线性分组码推到过工程应用的巅峰。换句话说你手机里、电脑里、数据中心里几乎每一块存储芯片和每一段无线帧结构背后都藏着线性分组码的影子。学这个不是学屠龙之技而是学所有后续编码算法的共同起点。6.3 一条不绕弯的进阶路径如果你吃透了(7,4)汉明码我建议按下面的顺序继续循环码与CRC理解码字循环移位后仍是合法码字这一性质学会用生成多项式表示码这是通向BCH、RS码的必经之路。卷积码与Viterbi译码接触有记忆的编码结构理解网格图和路径度量。LDPC码与置信传播译码把校验矩阵当作二分图理解软信息在变量节点和校验节点之间传递的过程。Polar码与信道极化理解5G编码方案的底层思想以及SC译码、SCL译码的基本流程。每走一步都保持手推仿真的习惯。学着写蒙特卡洛仿真把编码前后的BER曲线画出来。没有仿真曲线的编码理论永远是纸上谈兵。6.4 我的仿真体会编码增益要这样看最后分享一个我实测(7,4)汉明码时的体会。第一版仿真我用BPSK调制加AWGN信道硬判决后送进汉明译码器横轴一开始用了信噪比SNR结果发现加了编码之后性能反而更差了。原因很直白7/4的码率意味着带宽扩展每个信息比特消耗的符号更多如果横轴是SNR而不是每比特能量Eb/N0编码带来的冗余代价就被算漏了。换成Eb/N0做横轴再比才能看到真实的编码增益。(7,4)汉明码在硬判决下的编码增益并不大大概1到2 dB量级指望它能带来质的飞跃不现实。它的价值在教育和工程铺垫不在性能极限。真正逼近香农极限的是Turbo、LDPC、Polar这些现代码。但如果你连(7,4)汉明码的编码增益曲线都画不对后面那些码的仿真大概率也会在更隐蔽的地方出错。我到现在调试新编码方案时还是会先用(7,4)汉明码当回归测试用例。它简单到可以手算任何一个环节出错都能立刻暴露出来是比任何调试器都可靠的基准。这大概就是线性分组码在工程实践里送给我的最后一份礼物。