简介: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必须是本原多项式,不能随便填。比如m=4可以用0b11001(x⁴+x+1),m=8可以用0x11d(x⁸+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)。比如m=4、t=3时,n=15,g的次数是10,k就等于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,解码器“翻车”
现象:人为翻转t+1个比特后,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)上的本原多项式,不是随便一个不可约多项式就行。非本原多项式会导致α的阶不是n,exp表提前循环。
解决:先单独验证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截成了14,ch搜索直接越界。
解决:在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流按最小公倍数对齐,比如k=5时,每40个bit可以切出8个码字,通过填充让总长度对齐。
想要提升解码性能的话,优先优化Chien搜索。这个函数是解码耗时大头,尤其是m=13、t=8这种长码场景。我一般会用查表法避免重复计算σ的幂次,或者把GF(2^m)的乘法表预计算成二维数组,牺牲内存换速度。BM迭代也可以替换成Euclid算法,二者结果等价,但Euclid在某些错误分布下迭代次数更稳定。
BCH和RS码的关系也值得注意。这套源码的GF工具和BM算法稍作扩展就能升级成RS码:把信息位按符号分组,编码时不再对bit做模二除法,而是对GF(2^m)符号做多项式除法。如果你业务上需要处理突发错误(连续多位错),直接改成RS码比加大t更划算。
最后说一个我从这次拆源码养成的习惯:每次改完编码器参数,我都会写一个自动注入错误的测试脚本,随机挑0到t个位置翻转,跑满一万次,确认decode全部返回正确结果且信息位完全一致。从那以后我每次把BCH参数交到别人手里,都会强制走一遍这个流程,再没被“我以为改对了”坑过。希望帮到你。
本文还有配套的精品资源,点击获取