首页 / 资讯中心 / 文章详情

BCH码编码与解码完整源码:有限域、BM迭代与Chien搜索实战解析

BCH码编码与解码完整源码:有限域、BM迭代与Chien搜索实战解析 ★ FEATURED ARTICLE
简介BCH码是一种广泛应用于闪存等数据存储场景的纠错编码技术。压缩包内的源代码完整展示了从编码到解码的全过程主要面向通信存储、嵌入式系统方向的开发人员同时适合高校相关专业学生深入学习。代码允许自行修改码长、纠错能力等参数便于理解生成多项式选择、伽罗华域运算、伴随式计算、错误定位和错误校正等核心机制。包内共两个文件包含一个头文件和一个实现文件总体积约三KB结构紧凑功能聚焦实现过程采用模块化设计便于查阅和修改单个环节。目前已有两千七百八十七人学习下载。通过分析源码读者能够掌握信息位扩展、多项式乘法与取模完成编码以及基于伴随式进行错误定位与修正的解码流程为闪存坏块管理、数据可靠传输等实际项目提供可信的代码参考也可作为学习BCH码理论知识的辅助材料。1. BCH码的编码与解码一套能直接改参数的完整源码第一次拿到这份源码时我正被某存储系统的随机多位错误折腾得焦头烂额。CRC只能检错不能纠错RS码又过于沉重而BCH码恰好卡在两者之间以GF(2^m)有限域为数学底座把码字长度、纠错能力全部参数化。这套BCH码的编码与解码源代码把从有限域建表、生成多项式计算、系统码编码到伴随式、BM迭代、Chien搜索的完整链路全部暴露出来你只需要改m、t、本原多项式三个参数就能适配不同码长和纠错能力。对有无线信道模拟、存储介质纠错、POC验证需求的开发者来说这几乎是现成的工具箱。2. 技术底座GF(2^m)有限域与生成多项式2.1 为什么BCH码离不开有限域BCH码本质上是定义在GF(2^m)上的循环码。码长n 2^m - 1纠错能力为t意味着能纠正任意t个随机错误。和CRC那种纯多项式取模不同BCH的校验位不是随便选的生成多项式必须是α、α²、……、α^{2t}这些元素的极小多项式的最小公倍式。这里的α是GF(2^m)的本原元而本原多项式就是你传入的prim_poly参数。如果把GF(2^m)的乘法和加法表先建好后面编码和解码的所有运算都能转化成查表和异或效率非常高。这也是为什么源码第一步永远是初始化有限域而不是直接去算多项式。我拆这份源码时第一感觉就是作者把数学部分收敛得很干净GF的exp/log表一旦建立后续伴随式计算、Chien搜索全部复用同一套工具。2.2 源码里的有限域表与生成多项式计算先看有限域表怎么建。这个函数就是整个BCH工程的“地基”地基歪了后面全是错的def _init_gf_tables(self): n self.n self.exp [0] * (n 1) self.log [0] * (n 1) x 1 for i in range(n): self.exp[i] x self.log[x] i x 1 if x (1 self.m): x ^ self.prim_poly self.exp[n] self.exp[0]这段代码的本质是从1开始每次左移一位模拟多项式乘以x一旦溢出到第m位就异或掉本原多项式从而回到域内。exp[i]存的是α^i对应的整数log[x]存的是逆映射。建表之后GF(2^m)上的乘法就变成了查表加取模def gf_mul(self, a, b): if a 0 or b 0: return 0 return self.exp[(self.log[a] self.log[b]) % self.n]注意这里的prim_poly必须是本原多项式不能随便填。比如m4可以用0b11001x⁴x1m8可以用0x11dx⁸x⁴x³x²1。如果传入的不是本原多项式exp表会提前出现重复导致整个有限域塌陷编码结果直接是错的。生成多项式的计算是BCH的核心。我一开始以为源码会硬编码一个多项式表没想到它真的是动态算出来的def _generator_poly(self): seen set() g [1] for i in range(1, 2 * self.t 1): beta self.exp[i] if beta in seen: continue mp self._min_poly(beta) cur beta while cur not in seen: seen.add(cur) cur self.gf_mul(cur, cur) g self._poly_mul(g, mp) return g这里对每一个α^i求极小多项式然后全部乘起来。因为极小多项式互不相同且不可约所以最小公倍式就等于乘积。_min_poly会先找到β的共轭系β、β²、β⁴……直到回到β再把所有(x c)乘到一起得到GF(2)上的极小多项式。代码里有一个细节由于BCH码的极小多项式系数必然落在{0,1}上所以_min_poly返回前会把系数规约到0或1避免GF(2^m)元素混进生成多项式。3. 编码模块拆解从信息位到系统码字3.1 系统码的编码原理BCH编码最常用的形式是系统码也就是码字前k位直接是原始信息后面跟着n-k位校验。数学上就一句话把信息多项式m(x)左移n-k位然后用生成多项式g(x)做模二除法余数就是校验位。这套源码的编码函数非常直白没有任何绕弯def encode(self, data_bits): msg_poly list(data_bits) [0] * (self.n - self.k) remainder self._poly_mod(msg_poly, self.g) codeword list(data_bits) remainder return codeword_poly_mod做的是GF(2)上的多项式长除逐位异或def _poly_mod(self, dividend, divisor): res dividend[:] dlen len(divisor) for i in range(len(res) - dlen 1): if res[i] 0: continue for j in range(dlen): res[i j] ^ divisor[j] return res[len(res) - dlen 1:]这里唯一需要注意的是输入长度。data_bits的长度必须严格等于k也就是n减去生成多项式的次数。如果传错长度_poly_mod算出来的余式长度不匹配整个码字就错位了。3.2 参数m、t、n、k是怎么联动的我拆这份源码时最在意的就是k值怎么算。源码在__init__里写得很清楚self.g self._generator_poly() self.k self.n - (len(self.g) - 1)生成多项式g的次数就是校验位长度所以k n - deg(g)。比如m4、t3时n15g的次数是10k就等于5构成BCH(15,5)可纠3位错误。这里的t不是随便选的它必须满足2t 2^m否则生成多项式的次数会超过码长。编码时的代码逻辑是先把信息位左移n-k位空出来的位置留给余数做模二除法最后拼接。整个过程不涉及任何有限域乘法纯粹是二进制多项式运算所以编码速度非常快。我实测手写几百行信息位单次编码耗时可以忽略不计。这里有个容易踩的坑_poly_mod里res[i j] ^ divisor[j]divisor的高位是在__init__里算好的没有像某些教材那样先左移再异或。这个实现里每次把整个divisor直接砸到res对应位置上天然就是模二除法。如果你打算改成别的生成多项式要确保首位一定是1否则除法会出错。4. 解码模块拆解伴随式计算、BM迭代与Chien搜索4.1 伴随式把错误“投影”到有限域解码的第一步是算伴随式。原理很简单接收码字r(x)带入x α^j如果传输无错r(α^j) 0有错则不为0。因为码字是生成多项式的倍式而生成多项式的根包含α、α²、……、α^{2t}所以不管错哪几位都会被这2t个根“照出来”。源码里的实现用了Horner方法逐位代入避免幂运算def syndrome(self, received): syn [] for j in range(1, 2 * self.t 1): s 0 for coeff in received: s self.gf_mul(s, self.exp[j]) if coeff: s ^ coeff syn.append(s) return syn这里coeff是0或1所以s ^ coeff相当于加上x^i在α^j处的取值。算完2t个伴随式后如果全为0说明没有错误直接返回信息位。这在实际信道里是最大概率路径所以这个判断放得很靠前。4.2 Berlekamp-Massey迭代求错误位置多项式伴随式只是“看出来有错”但不知道哪几位错了。BM算法就是干这件事的根据伴随式序列反推一个最短的线性反馈移位寄存器也就是错误位置多项式σ(x) 1 σ₁x σ₂x² ... σ_Lx^L。这个多项式的根就是错误位置的倒数。def berlekamp_massey(self, syndromes): C [1] B [1] L 0 m 1 b 1 for r in range(1, len(syndromes) 1): d syndromes[r - 1] for i in range(1, L 1): d ^ self.gf_mul(C[i], syndromes[r - 1 - i]) if d 0: m 1 elif 2 * L r: T C[:] scale self.gf_div(d, b) C self._poly_sub(C, self._poly_scale_shift(B, scale, m)) L r - L B T b d m 1 else: scale self.gf_div(d, b) C self._poly_sub(C, self._poly_scale_shift(B, scale, m)) m 1 return C这段代码看起来有点绕但核心逻辑只有两条分支2L r时更新L和B否则只更新C。_poly_scale_shift做的是把多项式B乘以scale再左移m位_poly_sub做的是异或相减GF(2)上减就是加。如果你要改成Euclid算法求错误位置多项式BM这个模块是唯一需要替换的地方。注意gf_div这个函数源码里没有直接暴露它本质上是gf_mul的逆运算用log表相减即可def gf_div(self, a, b): if a 0: return 0 return self.exp[(self.log[a] - self.log[b]) % self.n]4.3 Chien搜索定位错误并纠错σ(x)求出来了怎么知道错在第几位Chien搜索的做法很暴力也很优雅把x α^i逐个代入σ(x)如果结果为0α^i是根对应的错误位置就是n-i。因为GF(2^m)总共就n个非零元素穷举一遍也就15次或255次运算比多项式求根快得多。def chien_search(self, sigma): error_positions [] degree len(sigma) - 1 for i in range(self.n): x self.exp[i] val 0 xk 1 for j in range(degree 1): if sigma[j] ! 0: val ^ self.gf_mul(sigma[j], xk) xk self.gf_mul(xk, x) if val 0: error_positions.append((self.n - i) % self.n) return error_positions拿到错误位置后纠错就一句话把对应位的比特翻转。但这里有个安全校验不能省——纠正完之后必须重新算一遍伴随式确认全部归零。如果BM迭代时错误数超过t解出来的σ(x)是假的即使Chien搜索找到了根纠完依然是错的。源码里的decode函数就是按这个流程走的def decode(self, received): syn self.syndrome(received) if all(s 0 for s in syn): return received[:self.k], True, [] sigma self.berlekamp_massey(syn) positions self.chien_search(sigma) corrected received[:] for pos in positions: corrected[pos] ^ 1 if self.syndrome(corrected) ! [0] * len(syn): return None, False, positions return corrected[:self.k], True, positions5. 参数改动与常见问题排查四个坑和对应解法5.1 解码后信息位对不上原数据现象编码、解码都跑通了但decode返回的前k位和原始信息不一致。原因绝大多数情况是k值算错了。如果你手动指定了信息位长度却没有用self.n - (len(self.g) - 1)去校验很容易把校验位当信息位截取。解决强制用生成多项式的次数推导k不要硬编码。另外检查一下_poly_mod返回的余数长度源码里res[len(res) - dlen 1:]取的是低dlen-1位如果你的divisor次数不是期望值这里会截出错误长度。5.2 错误数超过t解码器“翻车”现象人为翻转t1个比特后decode返回False甚至纠正出来的码字伴随式归零了但内容完全不是原信息。原因BCH的纠错能力上限是t当错误数超过t时BM算法解出的错误位置多项式可能是错的但恰好满足部分校验方程。这种“假阳性”是BCH码的固有特性不是代码bug。解决把错误注入控制在t以内。如果应用场景需要更强的纠错能力要么增大t要么改用更长的码增大m。我通常会在decode返回False时直接丢弃该帧并请求上层重传而不是强行使用可能错误的纠错结果。5.3 prim_poly填错导致整个GF表崩溃现象填了一个自认为“差不多”的本原多项式编码结果看起来有规律但解码永远失败甚至_min_poly里出现非0/1系数。原因prim_poly必须是GF(2^m)上的本原多项式不是随便一个不可约多项式就行。非本原多项式会导致α的阶不是nexp表提前循环。解决先单独验证prim_poly。最保险的方法是检查exp表从1生成到第n个时表里n个元素是否不重复。也可以直接打印self.exp[:self.m]看前m个值是否为[1, 2, 4, 8, ...]如果不是这个形态就说明参数不对。5.4 Chien搜索找到的错误位置越界现象Chien搜索返回的位置里出现大于n-1的值或者位置数组长度和BM解出的deg(σ)不一致。原因接收向量的长度不是n位或者BM算法里的L更新逻辑没跑对。我在自己改代码时遇到过把received长度从15截成了14ch搜索直接越界。解决在decode入口加一个断言len(received) self.n。至于BM的L不匹配多半是伴随式输入顺序反了BM要求syndromes[0]对应S₁也就是r(α)不是r(α^{2t})。6. 二次开发把BCH源码改造成你的业务模块这套源码最省心的地方是参数全部集中在构造函数里二次开发基本不用动算法本体。我拿到手后第一件事就是把m、t、prim_poly全部改成外部传入的配置纯参数然后封装成两个对外函数encode_frame(bytes)和decode_frame(bytes)。如果你的数据是字节流可以把字节拆成bit再按k位分组编码。这里有个实用的技巧BCH(15,5)的k是5字节是8位直接分组会有大量浪费。我一般会做一个适配层把输入bit流按最小公倍数对齐比如k5时每40个bit可以切出8个码字通过填充让总长度对齐。想要提升解码性能的话优先优化Chien搜索。这个函数是解码耗时大头尤其是m13、t8这种长码场景。我一般会用查表法避免重复计算σ的幂次或者把GF(2^m)的乘法表预计算成二维数组牺牲内存换速度。BM迭代也可以替换成Euclid算法二者结果等价但Euclid在某些错误分布下迭代次数更稳定。BCH和RS码的关系也值得注意。这套源码的GF工具和BM算法稍作扩展就能升级成RS码把信息位按符号分组编码时不再对bit做模二除法而是对GF(2^m)符号做多项式除法。如果你业务上需要处理突发错误连续多位错直接改成RS码比加大t更划算。最后说一个我从这次拆源码养成的习惯每次改完编码器参数我都会写一个自动注入错误的测试脚本随机挑0到t个位置翻转跑满一万次确认decode全部返回正确结果且信息位完全一致。从那以后我每次把BCH参数交到别人手里都会强制走一遍这个流程再没被“我以为改对了”坑过。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站